Skip to content

Commit f644373

Browse files
authored
Add SetVerboseDatabaseStatements and Enrich options to the Redis instrumentation (#2198)
1 parent 67b6a83 commit f644373

File tree

10 files changed

+300
-25
lines changed

10 files changed

+300
-25
lines changed
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions
2+
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.Enrich.get -> System.Action<System.Diagnostics.Activity, StackExchange.Redis.Profiling.IProfiledCommand>
3+
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.Enrich.set -> void
24
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.FlushInterval.get -> System.TimeSpan
35
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.FlushInterval.set -> void
6+
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.SetVerboseDatabaseStatements.get -> bool
7+
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.SetVerboseDatabaseStatements.set -> void
48
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.StackExchangeRedisCallsInstrumentationOptions() -> void
59
OpenTelemetry.Trace.TracerProviderBuilderExtensions
6-
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
10+
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
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions
2+
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.Enrich.get -> System.Action<System.Diagnostics.Activity, StackExchange.Redis.Profiling.IProfiledCommand>
3+
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.Enrich.set -> void
24
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.FlushInterval.get -> System.TimeSpan
35
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.FlushInterval.set -> void
6+
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.SetVerboseDatabaseStatements.get -> bool
7+
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.SetVerboseDatabaseStatements.set -> void
48
OpenTelemetry.Instrumentation.StackExchangeRedis.StackExchangeRedisCallsInstrumentationOptions.StackExchangeRedisCallsInstrumentationOptions() -> void
59
OpenTelemetry.Trace.TracerProviderBuilderExtensions
6-
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
10+
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

src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

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

src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,64 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515
// </copyright>
16+
using System;
1617
using System.Collections.Generic;
1718
using System.Diagnostics;
1819
using System.Net;
20+
using System.Reflection;
21+
using System.Reflection.Emit;
1922
using OpenTelemetry.Trace;
2023
using StackExchange.Redis.Profiling;
2124

2225
namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation
2326
{
2427
internal static class RedisProfilerEntryToActivityConverter
2528
{
26-
public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfiledCommand command)
29+
private static readonly Lazy<Func<object, (string, string)>> MessageDataGetter = new Lazy<Func<object, (string, string)>>(() =>
30+
{
31+
var redisAssembly = typeof(IProfiledCommand).Assembly;
32+
Type profiledCommandType = redisAssembly.GetType("StackExchange.Redis.Profiling.ProfiledCommand");
33+
Type messageType = redisAssembly.GetType("StackExchange.Redis.Message");
34+
Type scriptMessageType = redisAssembly.GetType("StackExchange.Redis.RedisDatabase+ScriptEvalMessage");
35+
36+
var messageDelegate = CreateFieldGetter<object>(profiledCommandType, "Message", BindingFlags.NonPublic | BindingFlags.Instance);
37+
var scriptDelegate = CreateFieldGetter<string>(scriptMessageType, "script", BindingFlags.NonPublic | BindingFlags.Instance);
38+
var commandAndKeyFetcher = new PropertyFetcher<string>("CommandAndKey");
39+
40+
if (messageDelegate == null)
41+
{
42+
return new Func<object, (string, string)>(source => (null, null));
43+
}
44+
45+
return new Func<object, (string, string)>(source =>
46+
{
47+
if (source == null)
48+
{
49+
return (null, null);
50+
}
51+
52+
var message = messageDelegate(source);
53+
if (message == null)
54+
{
55+
return (null, null);
56+
}
57+
58+
string script = null;
59+
if (message.GetType() == scriptMessageType)
60+
{
61+
script = scriptDelegate.Invoke(message);
62+
}
63+
64+
if (commandAndKeyFetcher.TryFetch(message, out var value))
65+
{
66+
return (value, script);
67+
}
68+
69+
return (null, script);
70+
});
71+
});
72+
73+
public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfiledCommand command, StackExchangeRedisCallsInstrumentationOptions options)
2774
{
2875
var name = command.Command; // Example: SET;
2976
if (string.IsNullOrEmpty(name))
@@ -43,6 +90,8 @@ public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfi
4390
return null;
4491
}
4592

93+
activity.SetEndTime(command.CommandCreated + command.ElapsedTime);
94+
4695
if (activity.IsAllDataRequested == true)
4796
{
4897
// see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md
@@ -62,7 +111,25 @@ public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfi
62111

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

65-
if (command.Command != null)
114+
if (options.SetVerboseDatabaseStatements)
115+
{
116+
var (commandAndKey, script) = MessageDataGetter.Value.Invoke(command);
117+
118+
if (!string.IsNullOrEmpty(commandAndKey) && !string.IsNullOrEmpty(script))
119+
{
120+
activity.SetTag(SemanticConventions.AttributeDbStatement, commandAndKey + " " + script);
121+
}
122+
else if (!string.IsNullOrEmpty(commandAndKey))
123+
{
124+
activity.SetTag(SemanticConventions.AttributeDbStatement, commandAndKey);
125+
}
126+
else if (command.Command != null)
127+
{
128+
// Example: "db.statement": SET;
129+
activity.SetTag(SemanticConventions.AttributeDbStatement, command.Command);
130+
}
131+
}
132+
else if (command.Command != null)
66133
{
67134
// Example: "db.statement": SET;
68135
activity.SetTag(SemanticConventions.AttributeDbStatement, command.Command);
@@ -100,20 +167,43 @@ public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfi
100167
activity.AddEvent(new ActivityEvent("Sent", send));
101168
activity.AddEvent(new ActivityEvent("ResponseReceived", response));
102169

103-
activity.SetEndTime(command.CommandCreated + command.ElapsedTime);
170+
options.Enrich?.Invoke(activity, command);
104171
}
105172

106173
activity.Stop();
107174

108175
return activity;
109176
}
110177

111-
public static void DrainSession(Activity parentActivity, IEnumerable<IProfiledCommand> sessionCommands)
178+
public static void DrainSession(Activity parentActivity, IEnumerable<IProfiledCommand> sessionCommands, StackExchangeRedisCallsInstrumentationOptions options)
112179
{
113180
foreach (var command in sessionCommands)
114181
{
115-
ProfilerCommandToActivity(parentActivity, command);
182+
ProfilerCommandToActivity(parentActivity, command, options);
116183
}
117184
}
185+
186+
/// <summary>
187+
/// Creates getter for a field defined in private or internal type
188+
/// repesented with classType variable.
189+
/// </summary>
190+
private static Func<object, TField> CreateFieldGetter<TField>(Type classType, string fieldName, BindingFlags flags)
191+
{
192+
FieldInfo field = classType.GetField(fieldName, flags);
193+
if (field != null)
194+
{
195+
string methodName = classType.FullName + ".get_" + field.Name;
196+
DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new[] { typeof(object) }, true);
197+
ILGenerator generator = getterMethod.GetILGenerator();
198+
generator.Emit(OpCodes.Ldarg_0);
199+
generator.Emit(OpCodes.Castclass, classType);
200+
generator.Emit(OpCodes.Ldfld, field);
201+
generator.Emit(OpCodes.Ret);
202+
203+
return (Func<object, TField>)getterMethod.CreateDelegate(typeof(Func<object, TField>));
204+
}
205+
206+
return null;
207+
}
118208
}
119209
}

src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
44
<Description>StackExchange.Redis instrumentation for OpenTelemetry .NET</Description>
55
<PackageTags>$(PackageTags);distributed-tracing;Redis;StackExchange.Redis</PackageTags>
6+
<IncludeDiagnosticSourceInstrumentationHelpers>true</IncludeDiagnosticSourceInstrumentationHelpers>
67
<IncludeInstrumentationHelpers>true</IncludeInstrumentationHelpers>
78
</PropertyGroup>
89

src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ This instrumentation can be configured to change the default behavior by using
6767

6868
### FlushInterval
6969

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

7575
The following example shows how to use `FlushInterval`.
7676

@@ -83,6 +83,44 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder()
8383
.Build();
8484
```
8585

86+
### SetVerboseDatabaseStatements
87+
88+
StackExchange.Redis by default does not give detailed database statements like
89+
what key or script was used during an operation. The `SetVerboseDatabaseStatements`
90+
option can be used to enable gathering this more detailed information.
91+
92+
The following example shows how to use `SetVerboseDatabaseStatements`.
93+
94+
```csharp
95+
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
96+
.AddRedisInstrumentation(
97+
connection,
98+
options => options.SetVerboseDatabaseStatements = true)
99+
.AddConsoleExporter()
100+
.Build();
101+
```
102+
103+
## Enrich
104+
105+
This option allows one to enrich the activity with additional information from the
106+
raw `IProfiledCommand` object. The `Enrich` action is called only when
107+
`activity.IsAllDataRequested` is `true`. It contains the activity itself (which can
108+
be enriched), and the source profiled command object.
109+
110+
The following code snippet shows how to add additional tags using `Enrich`.
111+
112+
```csharp
113+
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
114+
.AddRedisInstrumentation(opt => opt.Enrich = (activity, command) =>
115+
{
116+
if (command.ElapsedTime < TimeSpan.FromMilliseconds(100))
117+
{
118+
activity.SetTag("is_fast", true);
119+
}
120+
})
121+
.Build();
122+
```
123+
86124
## References
87125

88126
* [OpenTelemetry Project](https://opentelemetry.io/)

src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public void Dispose()
120120

121121
internal void Flush()
122122
{
123-
RedisProfilerEntryToActivityConverter.DrainSession(null, this.defaultSession.FinishProfiling());
123+
RedisProfilerEntryToActivityConverter.DrainSession(null, this.defaultSession.FinishProfiling(), this.options);
124124

125125
foreach (var entry in this.Cache)
126126
{
@@ -132,7 +132,7 @@ internal void Flush()
132132
}
133133

134134
ProfilingSession session = entry.Value.Session;
135-
RedisProfilerEntryToActivityConverter.DrainSession(parent, session.FinishProfiling());
135+
RedisProfilerEntryToActivityConverter.DrainSession(parent, session.FinishProfiling(), this.options);
136136
this.Cache.TryRemove((entry.Key.TraceId, entry.Key.SpanId), out _);
137137
}
138138
}

src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
// </copyright>
1616

1717
using System;
18+
using System.Data;
1819
using System.Diagnostics;
20+
using OpenTelemetry.Trace;
21+
using StackExchange.Redis.Profiling;
1922

2023
namespace OpenTelemetry.Instrumentation.StackExchangeRedis
2124
{
@@ -28,5 +31,19 @@ public class StackExchangeRedisCallsInstrumentationOptions
2831
/// 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.
2932
/// </summary>
3033
public TimeSpan FlushInterval { get; set; } = TimeSpan.FromSeconds(10);
34+
35+
/// <summary>
36+
/// 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.
37+
/// </summary>
38+
public bool SetVerboseDatabaseStatements { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets an action to enrich an Activity.
42+
/// </summary>
43+
/// <remarks>
44+
/// <para><see cref="Activity"/>: the activity being enriched.</para>
45+
/// <para><see cref="IProfiledCommand"/>: the profiled redis command from which additional information can be extracted to enrich the activity.</para>
46+
/// </remarks>
47+
public Action<Activity, IProfiledCommand> Enrich { get; set; }
3148
}
3249
}

0 commit comments

Comments
 (0)