Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.Enrich.get -> System.Action<System.Diagnostics.Activity, StackExchange.Redis.Profiling.IProfiledCommand>
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.Enrich.set -> void
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.FlushInterval.get -> System.TimeSpan
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.FlushInterval.set -> void
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.SetVerboseDatabaseStatements.get -> bool
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.SetVerboseDatabaseStatements.set -> void
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.StackExchangeRedisCallsInstrumentationOptions() -> void
OpenTelemetry.Trace.TracerProviderBuilderExtensions
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddRedisInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, StackExchange.Redis.IConnectionMultiplexer connection = null, System.Action<OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions> configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddRedisInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, StackExchange.Redis.IConnectionMultiplexer connection = null, System.Action<OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions> configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.Enrich.get -> System.Action<System.Diagnostics.Activity, StackExchange.Redis.Profiling.IProfiledCommand>
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.Enrich.set -> void
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.FlushInterval.get -> System.TimeSpan
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.FlushInterval.set -> void
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.SetVerboseDatabaseStatements.get -> bool
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.SetVerboseDatabaseStatements.set -> void
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.StackExchangeRedisCallsInstrumentationOptions() -> void
OpenTelemetry.Trace.TracerProviderBuilderExtensions
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddRedisInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, StackExchange.Redis.IConnectionMultiplexer connection = null, System.Action<OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions> configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddRedisInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, StackExchange.Redis.IConnectionMultiplexer connection = null, System.Action<OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions> configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,64 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Reflection;
using System.Reflection.Emit;
using OpenTelemetry.Trace;
using StackExchange.Redis.Profiling;

namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation
{
internal static class RedisProfilerEntryToActivityConverter
{
public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfiledCommand command)
private static readonly Lazy<Func<object, (string, string)>> MessageDataGetter = new Lazy<Func<object, (string, string)>>(() =>
{
var redisAssembly = typeof(IProfiledCommand).Assembly;
Type profiledCommandType = redisAssembly.GetType("StackExchange.Redis.Profiling.ProfiledCommand");
Type messageType = redisAssembly.GetType("StackExchange.Redis.Message");
Type scriptMessageType = redisAssembly.GetType("StackExchange.Redis.RedisDatabase+ScriptEvalMessage");

var messageDelegate = CreateFieldGetter<object>(profiledCommandType, "Message", BindingFlags.NonPublic | BindingFlags.Instance);
var scriptDelegate = CreateFieldGetter<string>(scriptMessageType, "script", BindingFlags.NonPublic | BindingFlags.Instance);
var commandAndKeyFetcher = new PropertyFetcher<string>("CommandAndKey");

if (messageDelegate == null)
{
return new Func<object, (string, string)>(source => (null, null));
}

return new Func<object, (string, string)>(source =>
{
if (source == null)
{
return (null, null);
}

var message = messageDelegate(source);
if (message == null)
{
return (null, null);
}

string script = null;
if (message.GetType() == scriptMessageType)
{
script = scriptDelegate.Invoke(message);
}

if (commandAndKeyFetcher.TryFetch(message, out var value))
{
return (value, script);
}

return (null, script);
});
});

public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfiledCommand command, StackExchangeRedisCallsInstrumentationOptions options)
{
var name = command.Command; // Example: SET;
if (string.IsNullOrEmpty(name))
Expand All @@ -43,6 +90,8 @@ public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfi
return null;
}

activity.SetEndTime(command.CommandCreated + command.ElapsedTime);

if (activity.IsAllDataRequested == true)
{
// see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md
Expand All @@ -62,7 +111,25 @@ public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfi

activity.SetTag(StackExchangeRedisCallsInstrumentation.RedisFlagsKeyName, command.Flags.ToString());

if (command.Command != null)
if (options.SetVerboseDatabaseStatements)
{
var (commandAndKey, script) = MessageDataGetter.Value.Invoke(command);

if (!string.IsNullOrEmpty(commandAndKey) && !string.IsNullOrEmpty(script))
{
activity.SetTag(SemanticConventions.AttributeDbStatement, commandAndKey + " " + script);
}
else if (!string.IsNullOrEmpty(commandAndKey))
{
activity.SetTag(SemanticConventions.AttributeDbStatement, commandAndKey);
}
else if (command.Command != null)
{
// Example: "db.statement": SET;
activity.SetTag(SemanticConventions.AttributeDbStatement, command.Command);
}
}
else if (command.Command != null)
{
// Example: "db.statement": SET;
activity.SetTag(SemanticConventions.AttributeDbStatement, command.Command);
Expand Down Expand Up @@ -100,20 +167,43 @@ public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfi
activity.AddEvent(new ActivityEvent("Sent", send));
activity.AddEvent(new ActivityEvent("ResponseReceived", response));

activity.SetEndTime(command.CommandCreated + command.ElapsedTime);
options.Enrich?.Invoke(activity, command);
}

activity.Stop();

return activity;
}

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

/// <summary>
/// Creates getter for a field defined in private or internal type
/// repesented with classType variable.
/// </summary>
private static Func<object, TField> CreateFieldGetter<TField>(Type classType, string fieldName, BindingFlags flags)
{
FieldInfo field = classType.GetField(fieldName, flags);
if (field != null)
{
string methodName = classType.FullName + ".get_" + field.Name;
DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new[] { typeof(object) }, true);
ILGenerator generator = getterMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Castclass, classType);
generator.Emit(OpCodes.Ldfld, field);
generator.Emit(OpCodes.Ret);

return (Func<object, TField>)getterMethod.CreateDelegate(typeof(Func<object, TField>));
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<Description>StackExchange.Redis instrumentation for OpenTelemetry .NET</Description>
<PackageTags>$(PackageTags);distributed-tracing;Redis;StackExchange.Redis</PackageTags>
<IncludeDiagnosticSourceInstrumentationHelpers>true</IncludeDiagnosticSourceInstrumentationHelpers>
<IncludeInstrumentationHelpers>true</IncludeInstrumentationHelpers>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void Dispose()

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

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

ProfilingSession session = entry.Value.Session;
RedisProfilerEntryToActivityConverter.DrainSession(parent, session.FinishProfiling());
RedisProfilerEntryToActivityConverter.DrainSession(parent, session.FinishProfiling(), this.options);
this.Cache.TryRemove((entry.Key.TraceId, entry.Key.SpanId), out _);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
// </copyright>

using System;
using System.Data;
using System.Diagnostics;
using OpenTelemetry.Trace;
using StackExchange.Redis.Profiling;

namespace OpenTelemetry.Instrumentation.StackExchangeRedis
{
Expand All @@ -28,5 +31,15 @@ public class StackExchangeRedisCallsInstrumentationOptions
/// Gets or sets the maximum time that should elapse between flushing the internal buffer of Redis profiling sessions and creating <see cref="Activity"/> objects. Default value: 00:00:10.
/// </summary>
public TimeSpan FlushInterval { get; set; } = TimeSpan.FromSeconds(10);

/// <summary>
/// Gets or sets a value indicating whether or not the <see cref="StackExchangeRedisCallsInstrumentation"/> should use reflection to get more detailed <see cref="SemanticConventions.AttributeDbStatement"/> tag values. Default value: False.
/// </summary>
public bool SetVerboseDatabaseStatements { get; set; }

/// <summary>
/// Gets or sets.
/// </summary>
public Action<Activity, IProfiledCommand> Enrich { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void ProfilerCommandToActivity_UsesCommandAsName()
profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
profiledCommand.Setup(m => m.Command).Returns("SET");

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object, new StackExchangeRedisCallsInstrumentationOptions());

Assert.Equal("SET", result.DisplayName);
}
Expand All @@ -74,7 +74,7 @@ public void ProfilerCommandToActivity_UsesTimestampAsStartTime()
var profiledCommand = new Mock<IProfiledCommand>();
profiledCommand.Setup(m => m.CommandCreated).Returns(now.DateTime);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object, new StackExchangeRedisCallsInstrumentationOptions());

Assert.Equal(now, result.StartTimeUtc);
}
Expand All @@ -86,7 +86,7 @@ public void ProfilerCommandToActivity_SetsDbTypeAttributeAsRedis()
var profiledCommand = new Mock<IProfiledCommand>();
profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object, new StackExchangeRedisCallsInstrumentationOptions());

Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeDbSystem));
Assert.Equal("redis", result.GetTagValue(SemanticConventions.AttributeDbSystem));
Expand All @@ -100,7 +100,7 @@ public void ProfilerCommandToActivity_UsesCommandAsDbStatementAttribute()
profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
profiledCommand.Setup(m => m.Command).Returns("SET");

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object, new StackExchangeRedisCallsInstrumentationOptions());

Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeDbStatement));
Assert.Equal("SET", result.GetTagValue(SemanticConventions.AttributeDbStatement));
Expand All @@ -116,7 +116,7 @@ public void ProfilerCommandToActivity_UsesFlagsForFlagsAttribute()
CommandFlags.NoRedirect;
profiledCommand.Setup(m => m.Flags).Returns(expectedFlags);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object, new StackExchangeRedisCallsInstrumentationOptions());

Assert.NotNull(result.GetTagValue(StackExchangeRedisCallsInstrumentation.RedisFlagsKeyName));
Assert.Equal("PreferMaster, FireAndForget, NoRedirect", result.GetTagValue(StackExchangeRedisCallsInstrumentation.RedisFlagsKeyName));
Expand All @@ -134,7 +134,7 @@ public void ProfilerCommandToActivity_UsesIpEndPointAsEndPoint()
profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
profiledCommand.Setup(m => m.EndPoint).Returns(ipLocalEndPoint);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object, new StackExchangeRedisCallsInstrumentationOptions());

Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeNetPeerIp));
Assert.Equal($"{address}.0.0.0", result.GetTagValue(SemanticConventions.AttributeNetPeerIp));
Expand All @@ -152,7 +152,7 @@ public void ProfilerCommandToActivity_UsesDnsEndPointAsEndPoint()
profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
profiledCommand.Setup(m => m.EndPoint).Returns(dnsEndPoint);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object, new StackExchangeRedisCallsInstrumentationOptions());

Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeNetPeerName));
Assert.Equal(dnsEndPoint.Host, result.GetTagValue(SemanticConventions.AttributeNetPeerName));
Expand All @@ -170,7 +170,7 @@ public void ProfilerCommandToActivity_UsesOtherEndPointAsEndPoint()
profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
profiledCommand.Setup(m => m.EndPoint).Returns(unixEndPoint);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object, new StackExchangeRedisCallsInstrumentationOptions());

Assert.NotNull(result.GetTagValue(SemanticConventions.AttributePeerService));
Assert.Equal(unixEndPoint.ToString(), result.GetTagValue(SemanticConventions.AttributePeerService));
Expand Down
Loading