Skip to content

Commit

Permalink
Add SetVerboseDatabaseStatements and Enrich options to the Redis inst…
Browse files Browse the repository at this point in the history
…rumentation (#2198)
  • Loading branch information
ejsmith authored Aug 10, 2021
1 parent 67b6a83 commit f644373
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 25 deletions.
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 @@ -2,6 +2,10 @@

## Unreleased

* Adds SetVerboseDatabaseStatements option to allow setting more detailed database
statement tag values.
* Adds Enrich option to allow enriching activities from the source profiled command
objects.
* Removes upper constraint for Microsoft.Extensions.Options
dependency. ([#2179](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2179))

Expand Down
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
42 changes: 40 additions & 2 deletions src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ This instrumentation can be configured to change the default behavior by using

### FlushInterval

StackExchange.Redis has its own internal profiler. OpenTelmetry converts each
StackExchange.Redis has its own internal profiler. OpenTelemetry converts each
profiled command from the internal profiler to an Activity for collection. By
default, this conversion process flushes profiled commands on a 10 second
interval. The `FlushInterval` option can be used to adjust this internval.
interval. The `FlushInterval` option can be used to adjust this interval.

The following example shows how to use `FlushInterval`.

Expand All @@ -83,6 +83,44 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.Build();
```

### SetVerboseDatabaseStatements

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.

The following example shows how to use `SetVerboseDatabaseStatements`.

```csharp
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddRedisInstrumentation(
connection,
options => options.SetVerboseDatabaseStatements = true)
.AddConsoleExporter()
.Build();
```

## Enrich

This option allows one to enrich the activity with additional information from the
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 following code snippet shows how to add additional tags using `Enrich`.

```csharp
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddRedisInstrumentation(opt => opt.Enrich = (activity, command) =>
{
if (command.ElapsedTime < TimeSpan.FromMilliseconds(100))
{
activity.SetTag("is_fast", true);
}
})
.Build();
```

## References

* [OpenTelemetry Project](https://opentelemetry.io/)
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,19 @@ 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 an action to enrich an Activity.
/// </summary>
/// <remarks>
/// <para><see cref="Activity"/>: the activity being enriched.</para>
/// <para><see cref="IProfiledCommand"/>: the profiled redis command from which additional information can be extracted to enrich the activity.</para>
/// </remarks>
public Action<Activity, IProfiledCommand> Enrich { get; set; }
}
}
Loading

0 comments on commit f644373

Please sign in to comment.