diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..67a5e0497
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,21 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/docker-compose*
+**/Dockerfile*
+**/bin
+**/obj
+**/*.yaml
+**/*.yml
+**/*.md
+**/*.ps1
\ No newline at end of file
diff --git a/.github/workflows/integration-redis.yml b/.github/workflows/integration-redis.yml
new file mode 100644
index 000000000..e05159e1c
--- /dev/null
+++ b/.github/workflows/integration-redis.yml
@@ -0,0 +1,15 @@
+name: Redis Integration Tests
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build-compose-test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Run redis docker-compose.integration
+ run: docker-compose --file=test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.integration.yml --project-directory=. up --exit-code-from=redis_integration_tests --build
\ No newline at end of file
diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln
index 86ddf991d..aadc08641 100644
--- a/OpenTelemetry.sln
+++ b/OpenTelemetry.sln
@@ -9,6 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Tests", "test
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B7408D66-487A-40E1-BDB7-BC17BD28F721}"
ProjectSection(SolutionItems) = preProject
+ .dockerignore = .dockerignore
.editorconfig = .editorconfig
CHANGELOG.md = CHANGELOG.md
CONTRIBUTING.md = CONTRIBUTING.md
@@ -120,6 +121,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\dotnet-core-linux.yml = .github\workflows\dotnet-core-linux.yml
.github\workflows\dotnet-core-win.yml = .github\workflows\dotnet-core-win.yml
.github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml
+ .github\workflows\integration-redis.yml = .github\workflows\integration-redis.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C1542297-8763-4DF4-957C-489ED771C21D}"
diff --git a/build/Common.prod.props b/build/Common.prod.props
index a21292d0b..7c3b3a46b 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -2,7 +2,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/samples/Exporters/Console/Exporters.csproj b/samples/Exporters/Console/Exporters.csproj
index a7411ee95..00bac6b25 100644
--- a/samples/Exporters/Console/Exporters.csproj
+++ b/samples/Exporters/Console/Exporters.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/samples/Exporters/Console/Program.cs b/samples/Exporters/Console/Program.cs
index ddbf1284e..7ebea8c9a 100644
--- a/samples/Exporters/Console/Program.cs
+++ b/samples/Exporters/Console/Program.cs
@@ -38,7 +38,7 @@ namespace Samples
/// Arguments from command line.
public static void Main(string[] args)
{
- Parser.Default.ParseArguments(args)
+ Parser.Default.ParseArguments(args)
.MapResult(
(JaegerOptions options) => TestJaegerExporter.Run(options.Host, options.Port),
(ZipkinOptions options) => TestZipkinExporter.Run(options.Uri),
diff --git a/samples/Exporters/Console/TestRedis.cs b/samples/Exporters/Console/TestRedis.cs
index 1a7559f68..64f5b9863 100644
--- a/samples/Exporters/Console/TestRedis.cs
+++ b/samples/Exporters/Console/TestRedis.cs
@@ -17,9 +17,11 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
+
using OpenTelemetry.Instrumentation.StackExchangeRedis;
using OpenTelemetry.Trace;
using OpenTelemetry.Trace.Configuration;
+
using StackExchange.Redis;
namespace Samples
@@ -28,8 +30,19 @@ namespace Samples
{
internal static object Run(string zipkinUri)
{
- // connect to the server
- var connection = ConnectionMultiplexer.Connect("localhost:6379");
+ /*
+ * Setup redis service inside local docker.
+ * docker run --name opentelemetry-redis-test -d -p 6379:6379 redis
+ *
+ * If you face any issue with the first command, do the following ones:
+ * docker exec -it opentelemetry-redis-test sh
+ * redis-cli
+ * set bind 0.0.0.0
+ * save
+ */
+
+ // connect to the redis server. The default port 6379 will be used.
+ var connection = ConnectionMultiplexer.Connect("localhost");
// Configure exporter to export traces to Zipkin
using var openTelemetry = OpenTelemetrySdk.EnableOpenTelemetry(
@@ -39,13 +52,11 @@ namespace Samples
o.ServiceName = "redis-test";
o.Endpoint = new Uri(zipkinUri);
})
- // TODO: Uncomment when we change Redis to Activity mode
- // .AddInstrumentation(t =>
- // {
- // var instrumentation = new StackExchangeRedisCallsInstrumentation(t);
- // connection.RegisterProfiler(instrumentation.GetProfilerSessionsFactory());
- // return instrumentation;
- // })
+ .AddRedisInstrumentation(connection, options =>
+ {
+ // changing flushinterval from 10s to 5s
+ options.FlushInterval = TimeSpan.FromSeconds(5);
+ })
.AddActivitySource("redis-test"));
ActivitySource activitySource = new ActivitySource("redis-test");
@@ -88,8 +99,8 @@ namespace Samples
catch (ArgumentOutOfRangeException e)
{
// Set status upon error
- activity.AddTag("ot.status", SpanHelper.GetCachedCanonicalCodeString(Status.Internal.CanonicalCode));
- activity.AddTag("ot.status_description", e.ToString());
+ activity.AddTag(SpanAttributeConstants.StatusCodeKey, SpanHelper.GetCachedCanonicalCodeString(Status.Internal.CanonicalCode));
+ activity.AddTag(SpanAttributeConstants.StatusDescriptionKey, e.ToString());
}
// Annotate our activity to capture metadata about our operation
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
index 347a97078..c3ae85495 100644
--- a/src/Directory.Build.targets
+++ b/src/Directory.Build.targets
@@ -2,7 +2,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs
new file mode 100644
index 000000000..ccc8e0404
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs
@@ -0,0 +1,118 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net;
+using OpenTelemetry.Trace;
+using StackExchange.Redis.Profiling;
+
+namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation
+{
+ internal static class RedisProfilerEntryToActivityConverter
+ {
+ public static Activity ProfilerCommandToActivity(Activity parentActivity, IProfiledCommand command)
+ {
+ var name = command.Command; // Example: SET;
+ if (string.IsNullOrEmpty(name))
+ {
+ name = StackExchangeRedisCallsInstrumentation.ActivityName;
+ }
+
+ var activity = StackExchangeRedisCallsInstrumentation.ActivitySource.StartActivity(
+ name,
+ ActivityKind.Client,
+ parentActivity?.Context ?? default,
+ startTime: command.CommandCreated);
+
+ if (activity == null)
+ {
+ return null;
+ }
+
+ if (activity.IsAllDataRequested == true)
+ {
+ // see https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md
+
+ // 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
+
+ // Total:
+ // command.ElapsedTime; // 00:00:32.4988020
+
+ activity.AddTag(SpanAttributeConstants.StatusCodeKey, SpanHelper.GetCachedCanonicalCodeString(StatusCanonicalCode.Ok));
+ activity.AddTag(SpanAttributeConstants.DatabaseSystemKey, "redis");
+ activity.AddTag(StackExchangeRedisCallsInstrumentation.RedisFlagsKeyName, command.Flags.ToString());
+
+ if (command.Command != null)
+ {
+ // Example: "db.statement": SET;
+ activity.AddTag(SpanAttributeConstants.DatabaseStatementKey, command.Command);
+ }
+
+ if (command.EndPoint != null)
+ {
+ if (command.EndPoint is IPEndPoint ipEndPoint)
+ {
+ activity.AddTag(SpanAttributeConstants.NetPeerIp, ipEndPoint.Address.ToString());
+ activity.AddTag(SpanAttributeConstants.NetPeerPort, ipEndPoint.Port.ToString());
+ }
+ else if (command.EndPoint is DnsEndPoint dnsEndPoint)
+ {
+ activity.AddTag(SpanAttributeConstants.NetPeerName, dnsEndPoint.Host);
+ activity.AddTag(SpanAttributeConstants.NetPeerPort, dnsEndPoint.Port.ToString());
+ }
+ else
+ {
+ activity.AddTag(SpanAttributeConstants.PeerServiceKey, command.EndPoint.ToString());
+ }
+ }
+
+ activity.AddTag(StackExchangeRedisCallsInstrumentation.RedisDatabaseIndexKeyName, command.Db.ToString());
+
+ // TODO: deal with the re-transmission
+ // command.RetransmissionOf;
+ // command.RetransmissionReason;
+
+ var enqueued = command.CommandCreated.Add(command.CreationToEnqueued);
+ var send = enqueued.Add(command.EnqueuedToSending);
+ var response = send.Add(command.SentToResponse);
+
+ activity.AddEvent(new ActivityEvent("Enqueued", enqueued));
+ activity.AddEvent(new ActivityEvent("Sent", send));
+ activity.AddEvent(new ActivityEvent("ResponseReceived", response));
+
+ activity.SetEndTime(command.CommandCreated + command.ElapsedTime);
+ }
+
+ activity.Stop();
+
+ return activity;
+ }
+
+ public static void DrainSession(Activity parentActivity, IEnumerable sessionCommands)
+ {
+ foreach (var command in sessionCommands)
+ {
+ ProfilerCommandToActivity(parentActivity, command);
+ }
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToSpanConverter.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToSpanConverter.cs
deleted file mode 100644
index 4330621d6..000000000
--- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToSpanConverter.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-//
-// Copyright The OpenTelemetry Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-using System.Collections.Generic;
-using OpenTelemetry.Trace;
-using StackExchange.Redis.Profiling;
-
-namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation
-{
- internal static class RedisProfilerEntryToSpanConverter
- {
- public static TelemetrySpan ProfilerCommandToSpan(Tracer tracer, TelemetrySpan parentSpan, IProfiledCommand command)
- {
- var name = command.Command; // Example: SET;
- if (string.IsNullOrEmpty(name))
- {
- name = "name";
- }
-
- var span = tracer.StartSpan(name, parentSpan, SpanKind.Client, new SpanCreationOptions { StartTimestamp = command.CommandCreated });
- if (span.IsRecording)
- {
- // use https://github.com/opentracing/specification/blob/master/semantic_conventions.md for now
-
- // 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
-
- // Total:
- // command.ElapsedTime; // 00:00:32.4988020
-
- span.Status = Status.Ok;
- span.SetAttribute("db.type", "redis");
- span.SetAttribute("redis.flags", command.Flags.ToString());
-
- if (command.Command != null)
- {
- // Example: "db.statement": SET;
- span.SetAttribute("db.statement", command.Command);
- }
-
- if (command.EndPoint != null)
- {
- // Example: "db.instance": Unspecified/localhost:6379[0]
- span.SetAttribute("db.instance", string.Concat(command.EndPoint, "[", command.Db, "]"));
- }
-
- // TODO: deal with the re-transmission
- // command.RetransmissionOf;
- // command.RetransmissionReason;
-
- var enqueued = command.CommandCreated.Add(command.CreationToEnqueued);
- var send = enqueued.Add(command.EnqueuedToSending);
- var response = send.Add(command.SentToResponse);
-
- span.AddEvent(new Event("Enqueued", enqueued));
- span.AddEvent(new Event("Sent", send));
- span.AddEvent(new Event("ResponseReceived", response));
-
- span.End(command.CommandCreated.Add(command.ElapsedTime));
- }
-
- return span;
- }
-
- public static void DrainSession(Tracer tracer, TelemetrySpan parentSpan, IEnumerable sessionCommands)
- {
- foreach (var command in sessionCommands)
- {
- ProfilerCommandToSpan(tracer, parentSpan, command);
- }
- }
- }
-}
diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj
index 1ab63633c..800e2cb8c 100644
--- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj
+++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetryBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetryBuilderExtensions.cs
new file mode 100644
index 000000000..091957763
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetryBuilderExtensions.cs
@@ -0,0 +1,53 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using OpenTelemetry.Instrumentation.StackExchangeRedis;
+using StackExchange.Redis;
+
+namespace OpenTelemetry.Trace.Configuration
+{
+ ///
+ /// Extension methods to simplify registering of dependency instrumentation.
+ ///
+ public static class OpenTelemetryBuilderExtensions
+ {
+ ///
+ /// Enables the outgoing requests automatic data collection for Redis.
+ ///
+ /// being configured.
+ /// to instrument.
+ /// Redis configuration options.
+ /// The instance of to chain the calls.
+ public static OpenTelemetryBuilder AddRedisInstrumentation(
+ this OpenTelemetryBuilder builder,
+ ConnectionMultiplexer connection,
+ Action configureOptions = null)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ StackExchangeRedisCallsInstrumentationOptions options = new StackExchangeRedisCallsInstrumentationOptions();
+ configureOptions?.Invoke(options);
+
+ return builder
+ .AddInstrumentation((activitySourceAdapter) => new StackExchangeRedisCallsInstrumentation(connection, options))
+ .AddActivitySource(StackExchangeRedisCallsInstrumentation.ActivitySourceName);
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md
index 5041d6fd1..5112d156f 100644
--- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md
+++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md
@@ -5,22 +5,17 @@ Outgoing calls to Redis made using `StackExchange.Redis` library can be automati
1. Install package to your project:
[OpenTelemetry.Instrumentation.StackExchangeRedis](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.StackExchangeRedis)
-2. Configure Redis instrumentation
+2. Configure Redis instrumentation:
```csharp
- // connect to the server
- var connection = ConnectionMultiplexer.Connect("localhost:6379");
+ // Connect to the server.
+ using var connection = ConnectionMultiplexer.Connect("localhost:6379");
- using (TracerFactory.Create(b => b
- .SetSampler(new AlwaysSampleSampler())
- .UseZipkin(options => {})
- .SetResource(Resources.CreateServiceResource("my-service"))
- .AddInstrumentation(t =>
- {
- var instrumentation = new StackExchangeRedisCallsInstrumentation(t);
- connection.RegisterProfiler(instrumentation.GetProfilerSessionsFactory());
- return instrumentation;
- })))
- {
- }
- ```
\ No newline at end of file
+ // Pass the connection to AddRedisInstrumentation.
+ using var openTelemetry = OpenTelemetrySdk.EnableOpenTelemetry(b => b
+ .AddRedisInstrumentation(connection)
+ .UseZipkinExporter()
+ .SetResource(Resources.CreateServiceResource("my-service"));
+ ```
+
+For a more detailed example see [TestRedis](../../samples/Exporters/Console/TestRedis.cs).
\ No newline at end of file
diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs
index b1f738e8a..9147f1807 100644
--- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs
+++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs
@@ -16,12 +16,10 @@
using System;
using System.Collections.Concurrent;
-using System.Reflection;
+using System.Diagnostics;
using System.Threading;
-using System.Threading.Tasks;
using OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation;
-using OpenTelemetry.Trace;
-using OpenTelemetry.Trace.Configuration;
+using StackExchange.Redis;
using StackExchange.Redis.Profiling;
namespace OpenTelemetry.Instrumentation.StackExchangeRedis
@@ -29,42 +27,43 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis
///
/// Redis calls instrumentation.
///
- public class StackExchangeRedisCallsInstrumentation : IDisposable
+ internal class StackExchangeRedisCallsInstrumentation : IDisposable
{
- private readonly Tracer tracer;
+ internal const string RedisDatabaseIndexKeyName = "db.redis.database_index";
+ internal const string RedisFlagsKeyName = "db.redis.flags";
+ internal const string ActivitySourceName = "StackExchange.Redis";
+ internal const string ActivityName = ActivitySourceName + ".Execute";
+ internal static readonly Version Version = typeof(StackExchangeRedisCallsInstrumentation).Assembly.GetName().Version;
+ internal static readonly ActivitySource ActivitySource = new ActivitySource(ActivitySourceName, Version.ToString());
- private readonly CancellationTokenSource cancellationTokenSource;
- private readonly CancellationToken cancellationToken;
+ private readonly StackExchangeRedisCallsInstrumentationOptions options;
+ private readonly EventWaitHandle stopHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
+ private readonly Thread drainThread;
private readonly ProfilingSession defaultSession = new ProfilingSession();
- private readonly ConcurrentDictionary cache = new ConcurrentDictionary();
-
- private readonly PropertyInfo spanEndTimestampInfo;
+ private readonly ConcurrentDictionary> cache = new ConcurrentDictionary>();
///
/// Initializes a new instance of the class.
///
- /// Tracer to record traced with.
- public StackExchangeRedisCallsInstrumentation(Tracer tracer)
+ /// to instrument.
+ /// Configuration options for redis instrumentation.
+ public StackExchangeRedisCallsInstrumentation(ConnectionMultiplexer connection, StackExchangeRedisCallsInstrumentationOptions options)
{
- this.tracer = tracer;
-
- this.cancellationTokenSource = new CancellationTokenSource();
- this.cancellationToken = this.cancellationTokenSource.Token;
- var spanType = typeof(TracerFactory).Assembly.GetType("OpenTelemetry.Trace.SpanSdk");
-
- this.spanEndTimestampInfo = spanType?.GetProperty("EndTimestamp");
- if (this.spanEndTimestampInfo == null)
+ if (connection == null)
{
- throw new ArgumentException("OpenTelemetry.Trace.SpanSdk.EndTimestamp property is missing");
+ throw new ArgumentNullException(nameof(connection));
}
- if (this.spanEndTimestampInfo.PropertyType != typeof(DateTimeOffset))
- {
- throw new ArgumentException("OpenTelemetry.Trace.SpanSdk.EndTimestamp property is not of DateTimeOffset type");
- }
+ this.options = options ?? new StackExchangeRedisCallsInstrumentationOptions();
- Task.Factory.StartNew(this.DumpEntries, TaskCreationOptions.LongRunning, this.cancellationToken);
+ this.drainThread = new Thread(this.DrainEntries)
+ {
+ Name = "OpenTelemetry.Redis",
+ };
+ this.drainThread.Start();
+
+ connection.RegisterProfiler(this.GetProfilerSessionsFactory());
}
///
@@ -73,71 +72,70 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis
/// Session associated with the current span context to record Redis calls.
public Func GetProfilerSessionsFactory()
{
- // This implementation shares session for multiple Redis calls made inside a single parent Span.
- // It cost an additional lookup in concurrent dictionary, but potentially saves an allocation
- // if many calls to Redis were made from the same parent span.
- // Creating a session per Redis call may be more optimal solution here as sampling will not
- // require any locking and can redis the number of buffered sessions significantly.
return () =>
{
- var span = this.tracer.CurrentSpan;
+ if (this.stopHandle.WaitOne(0))
+ {
+ return null;
+ }
- // when there are no spans in current context - BlankSpan will be returned
- // BlankSpan has invalid context. It's OK to use a single profiler session
- // for all invalid context's spans.
- //
- // It would be great to allow to check sampling here, but it is impossible
- // with the current model to start a new trace id here - no way to pass it
- // to the resulting Span.
- if (span == null || !span.Context.IsValid)
+ Activity parent = Activity.Current;
+
+ // If no parent use the default session.
+ if (parent == null || parent.IdFormat != ActivityIdFormat.W3C)
{
return this.defaultSession;
}
- // TODO: As a performance optimization the check for sampling may be implemented here
- // The problem with this approach would be that ActivitySpanId cannot be generated here
- // So if sampler uses ActivitySpanId in algorithm - results would be inconsistent
- var session = this.cache.GetOrAdd(span, (s) => new ProfilingSession(s));
- return session;
+ // Try to reuse a session for all activities created under the same TraceId.
+ if (!this.cache.TryGetValue(parent.TraceId, out var session))
+ {
+ session = new Tuple(parent, new ProfilingSession());
+ this.cache.TryAdd(parent.TraceId, session);
+ }
+
+ return session.Item2;
};
}
///
public void Dispose()
{
- this.cancellationTokenSource.Cancel();
- this.cancellationTokenSource.Dispose();
+ this.stopHandle.Set();
+ this.drainThread.Join();
+
+ this.Flush();
+
+ this.stopHandle.Dispose();
}
- private void DumpEntries(object state)
+ private void DrainEntries(object state)
{
- while (!this.cancellationToken.IsCancellationRequested)
+ while (true)
{
- RedisProfilerEntryToSpanConverter.DrainSession(this.tracer, null, this.defaultSession.FinishProfiling());
-
- foreach (var entry in this.cache)
+ if (this.stopHandle.WaitOne(this.options.FlushInterval))
{
- var span = entry.Key;
- ProfilingSession session;
-
- // Redis instrumentation needs a hack to know that current span has ended (it's not tracing-friendly)
- var endTimestamp = (DateTimeOffset)this.spanEndTimestampInfo.GetValue(span);
- if (endTimestamp != default)
- {
- this.cache.TryRemove(span, out session);
- }
- else
- {
- this.cache.TryGetValue(span, out session);
- }
-
- if (session != null)
- {
- RedisProfilerEntryToSpanConverter.DrainSession(this.tracer, span, session.FinishProfiling());
- }
+ break;
}
- Thread.Sleep(TimeSpan.FromSeconds(1));
+ this.Flush();
+ }
+ }
+
+ private void Flush()
+ {
+ RedisProfilerEntryToActivityConverter.DrainSession(null, this.defaultSession.FinishProfiling());
+
+ foreach (var entry in this.cache)
+ {
+ var parent = entry.Value.Item1;
+ if (parent.Duration == TimeSpan.Zero)
+ {
+ // Activity is still running, don't drain.
+ continue;
+ }
+
+ RedisProfilerEntryToActivityConverter.DrainSession(parent, entry.Value.Item2.FinishProfiling());
}
}
}
diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs
index ac7989d13..3d8397d05 100644
--- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs
+++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs
@@ -13,6 +13,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
+using System;
+using System.Diagnostics;
namespace OpenTelemetry.Instrumentation.StackExchangeRedis
{
@@ -21,5 +23,9 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis
///
public class StackExchangeRedisCallsInstrumentationOptions
{
+ ///
+ /// Gets or sets the maximum time that should elapse between flushing the internal buffer of Redis profiling sessions and creating objects. Default value: 00:00:10.
+ ///
+ public TimeSpan FlushInterval { get; set; } = TimeSpan.FromSeconds(10);
}
}
diff --git a/src/OpenTelemetry/Trace/Configuration/OpenTelemetrySdk.cs b/src/OpenTelemetry/Trace/Configuration/OpenTelemetrySdk.cs
index 5a0cb4f32..233bdcd91 100644
--- a/src/OpenTelemetry/Trace/Configuration/OpenTelemetrySdk.cs
+++ b/src/OpenTelemetry/Trace/Configuration/OpenTelemetrySdk.cs
@@ -132,8 +132,6 @@ namespace OpenTelemetry.Trace.Configuration
public void Dispose()
{
- this.listener.Dispose();
-
foreach (var item in this.instrumentations)
{
if (item is IDisposable disposable)
@@ -148,6 +146,11 @@ namespace OpenTelemetry.Trace.Configuration
{
disposableProcessor.Dispose();
}
+
+ // Shutdown the listener last so that anything created while instrumentation cleans up will still be processed.
+ // Redis instrumentation, for example, flushes during dispose which creates Activity objects for any profiling
+ // sessions that were open.
+ this.listener.Dispose();
}
internal static ActivityDataRequest ComputeActivityDataRequest(
diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerSpanConverterTest.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerSpanConverterTest.cs
index 6aa9f0565..cdb142a86 100644
--- a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerSpanConverterTest.cs
+++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerSpanConverterTest.cs
@@ -290,7 +290,7 @@ namespace OpenTelemetry.Exporter.Jaeger.Tests.Implementation
// The last tag should be ot.status_code in this case
tag = tags[tags.Length - 1];
Assert.Equal(JaegerTagType.STRING, tag.VType);
- Assert.Equal("ot.status_code", tag.Key);
+ Assert.Equal(SpanAttributeConstants.StatusCodeKey, tag.Key);
Assert.Equal("Ok", tag.VStr);
var logs = jaegerSpan.Logs.ToArray();
diff --git a/test/OpenTelemetry.Instrumentation.Dependencies.Tests/OpenTelemetry.Instrumentation.Dependencies.Tests.csproj b/test/OpenTelemetry.Instrumentation.Dependencies.Tests/OpenTelemetry.Instrumentation.Dependencies.Tests.csproj
index 7bb291089..11611f801 100644
--- a/test/OpenTelemetry.Instrumentation.Dependencies.Tests/OpenTelemetry.Instrumentation.Dependencies.Tests.csproj
+++ b/test/OpenTelemetry.Instrumentation.Dependencies.Tests/OpenTelemetry.Instrumentation.Dependencies.Tests.csproj
@@ -16,6 +16,7 @@
+
diff --git a/test/OpenTelemetry.Instrumentation.Dependencies.Tests/SqlEventSourceTests.netfx.cs b/test/OpenTelemetry.Instrumentation.Dependencies.Tests/SqlEventSourceTests.netfx.cs
index 673759f70..58b7b5d74 100644
--- a/test/OpenTelemetry.Instrumentation.Dependencies.Tests/SqlEventSourceTests.netfx.cs
+++ b/test/OpenTelemetry.Instrumentation.Dependencies.Tests/SqlEventSourceTests.netfx.cs
@@ -23,6 +23,7 @@ using System.Linq;
using System.Threading.Tasks;
using Moq;
using OpenTelemetry.Instrumentation.Dependencies.Implementation;
+using OpenTelemetry.Internal.Test;
using OpenTelemetry.Trace;
using OpenTelemetry.Trace.Configuration;
using OpenTelemetry.Trace.Export;
@@ -33,15 +34,15 @@ namespace OpenTelemetry.Instrumentation.Dependencies.Tests
public class SqlEventSourceTests
{
/*
- To run the integration tests, set the ot.SqlConnectionString machine-level environment variable to a valid Sql Server connection string.
+ To run the integration tests, set the OT_SQLCONNECTIONSTRING machine-level environment variable to a valid Sql Server connection string.
To use Docker...
1) Run: docker run -d --name sql2019 -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@word" -p 5433:1433 mcr.microsoft.com/mssql/server:2019-latest
- 2) Set ot.SqlConnectionString as: Data Source=127.0.0.1,5433; User ID=sa; Password=Pass@word
+ 2) Set OT_SQLCONNECTIONSTRING as: Data Source=127.0.0.1,5433; User ID=sa; Password=Pass@word
*/
- private const string SqlConnectionStringEnvVarName = "ot.SqlConnectionString";
- private static readonly string SqlConnectionString = Environment.GetEnvironmentVariable(SqlConnectionStringEnvVarName, EnvironmentVariableTarget.Machine);
+ private const string SqlConnectionStringEnvVarName = "OT_SQLCONNECTIONSTRING";
+ private static readonly string SqlConnectionString = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(SqlConnectionStringEnvVarName);
[Trait("CategoryName", "SqlIntegrationTests")]
[SkipUnlessEnvVarFoundTheory(SqlConnectionStringEnvVarName)]
diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/AttributesExtensions.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/AttributesExtensions.cs
deleted file mode 100644
index 3a14387e5..000000000
--- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/AttributesExtensions.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-//
-// Copyright The OpenTelemetry Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-using System.Collections.Generic;
-using System.Linq;
-
-namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Tests
-{
- internal static class AttributesExtensions
- {
- public static object GetValue(this IEnumerable> attributes, string key)
- {
- return attributes.FirstOrDefault(kvp => kvp.Key == key).Value;
- }
- }
-}
diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs
new file mode 100644
index 000000000..d09d2c7e9
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs
@@ -0,0 +1,178 @@
+//
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using Moq;
+using OpenTelemetry.Trace;
+using OpenTelemetry.Trace.Configuration;
+using StackExchange.Redis;
+using StackExchange.Redis.Profiling;
+using Xunit;
+
+namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation
+{
+ [Collection("Redis")]
+ public class RedisProfilerEntryToActivityConverterTests : IDisposable
+ {
+ private readonly ConnectionMultiplexer connection;
+ private readonly IDisposable sdk;
+
+ public RedisProfilerEntryToActivityConverterTests()
+ {
+ var connectionOptions = new ConfigurationOptions
+ {
+ AbortOnConnectFail = false,
+ };
+ connectionOptions.EndPoints.Add("localhost:6379");
+
+ this.connection = ConnectionMultiplexer.Connect(connectionOptions);
+
+ this.sdk = OpenTelemetrySdk.EnableOpenTelemetry(
+ (builder) => builder.AddRedisInstrumentation(this.connection));
+ }
+
+ public void Dispose()
+ {
+ this.sdk.Dispose();
+ this.connection.Dispose();
+ }
+
+ [Fact]
+ public void ProfilerCommandToActivity_UsesCommandAsName()
+ {
+ var activity = new Activity("redis-profiler");
+ var profiledCommand = new Mock();
+ profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
+ profiledCommand.Setup(m => m.Command).Returns("SET");
+
+ var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
+
+ Assert.Equal("SET", result.DisplayName);
+ }
+
+ [Fact]
+ public void ProfilerCommandToActivity_UsesTimestampAsStartTime()
+ {
+ var now = DateTimeOffset.Now;
+ var activity = new Activity("redis-profiler");
+ var profiledCommand = new Mock();
+ profiledCommand.Setup(m => m.CommandCreated).Returns(now.DateTime);
+
+ var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
+
+ Assert.Equal(now, result.StartTimeUtc);
+ }
+
+ [Fact]
+ public void ProfilerCommandToActivity_SetsDbTypeAttributeAsRedis()
+ {
+ var activity = new Activity("redis-profiler");
+ var profiledCommand = new Mock();
+ profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
+
+ var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
+
+ Assert.Contains(result.Tags, kvp => kvp.Key == SpanAttributeConstants.DatabaseSystemKey);
+ Assert.Equal("redis", result.Tags.FirstOrDefault(kvp => kvp.Key == SpanAttributeConstants.DatabaseSystemKey).Value);
+ }
+
+ [Fact]
+ public void ProfilerCommandToActivity_UsesCommandAsDbStatementAttribute()
+ {
+ var activity = new Activity("redis-profiler");
+ var profiledCommand = new Mock();
+ profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
+ profiledCommand.Setup(m => m.Command).Returns("SET");
+
+ var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
+
+ Assert.Contains(result.Tags, kvp => kvp.Key == SpanAttributeConstants.DatabaseStatementKey);
+ Assert.Equal("SET", result.Tags.FirstOrDefault(kvp => kvp.Key == SpanAttributeConstants.DatabaseStatementKey).Value);
+ }
+
+ [Fact]
+ public void ProfilerCommandToActivity_UsesFlagsForFlagsAttribute()
+ {
+ var activity = new Activity("redis-profiler");
+ var profiledCommand = new Mock();
+ profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
+ var expectedFlags = CommandFlags.FireAndForget |
+ CommandFlags.NoRedirect;
+ profiledCommand.Setup(m => m.Flags).Returns(expectedFlags);
+
+ var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
+
+ Assert.Contains(result.Tags, kvp => kvp.Key == StackExchangeRedisCallsInstrumentation.RedisFlagsKeyName);
+ Assert.Equal("PreferMaster, FireAndForget, NoRedirect", result.Tags.FirstOrDefault(kvp => kvp.Key == StackExchangeRedisCallsInstrumentation.RedisFlagsKeyName).Value);
+ }
+
+ [Fact]
+ public void ProfilerCommandToActivity_UsesIpEndPointAsEndPoint()
+ {
+ long address = 1;
+ int port = 2;
+
+ var activity = new Activity("redis-profiler");
+ IPEndPoint ipLocalEndPoint = new IPEndPoint(address, port);
+ var profiledCommand = new Mock();
+ profiledCommand.Setup(m => m.EndPoint).Returns(ipLocalEndPoint);
+
+ var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
+
+ Assert.Contains(result.Tags, kvp => kvp.Key == SpanAttributeConstants.NetPeerIp);
+ Assert.Equal($"{address}.0.0.0", result.Tags.FirstOrDefault(kvp => kvp.Key == SpanAttributeConstants.NetPeerIp).Value);
+ Assert.Contains(result.Tags, kvp => kvp.Key == SpanAttributeConstants.NetPeerPort);
+ Assert.Equal($"{port}", result.Tags.FirstOrDefault(kvp => kvp.Key == SpanAttributeConstants.NetPeerPort).Value);
+ }
+
+ [Fact]
+ public void ProfilerCommandToActivity_UsesDnsEndPointAsEndPoint()
+ {
+ var dnsEndPoint = new DnsEndPoint("https://opentelemetry.io/", 443);
+
+ var activity = new Activity("redis-profiler");
+ var profiledCommand = new Mock();
+ profiledCommand.Setup(m => m.EndPoint).Returns(dnsEndPoint);
+
+ var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
+
+ Assert.Contains(result.Tags, kvp => kvp.Key == SpanAttributeConstants.NetPeerName);
+ Assert.Equal(dnsEndPoint.Host, result.Tags.FirstOrDefault(kvp => kvp.Key == SpanAttributeConstants.NetPeerName).Value);
+ Assert.Contains(result.Tags, kvp => kvp.Key == SpanAttributeConstants.NetPeerPort);
+ Assert.Equal(dnsEndPoint.Port.ToString(), result.Tags.FirstOrDefault(kvp => kvp.Key == SpanAttributeConstants.NetPeerPort).Value);
+ }
+
+#if !NET461
+ [Fact]
+ public void ProfilerCommandToActivity_UsesOtherEndPointAsEndPoint()
+ {
+ var unixEndPoint = new UnixDomainSocketEndPoint("https://opentelemetry.io/");
+ var activity = new Activity("redis-profiler");
+ var profiledCommand = new Mock();
+ profiledCommand.Setup(m => m.EndPoint).Returns(unixEndPoint);
+
+ var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand.Object);
+
+ Assert.Contains(result.Tags, kvp => kvp.Key == SpanAttributeConstants.PeerServiceKey);
+ Assert.Equal(unixEndPoint.ToString(), result.Tags.FirstOrDefault(kvp => kvp.Key == SpanAttributeConstants.PeerServiceKey).Value);
+ }
+#endif
+ }
+}
diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToSpanConverterTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToSpanConverterTests.cs
deleted file mode 100644
index 0ff3aec13..000000000
--- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToSpanConverterTests.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-//
-// Copyright The OpenTelemetry Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-using System;
-using Moq;
-using OpenTelemetry.Instrumentation.StackExchangeRedis.Tests;
-using OpenTelemetry.Trace;
-using OpenTelemetry.Trace.Configuration;
-using StackExchange.Redis.Profiling;
-using Xunit;
-
-namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation
-{
- public class RedisProfilerEntryToSpanConverterTests
- {
- private readonly Tracer tracer;
-
- public RedisProfilerEntryToSpanConverterTests()
- {
- this.tracer = TracerFactory.Create(b => { }).GetTracer(null);
- }
-
- [Fact]
- public void DrainSessionUsesCommandAsName()
- {
- var profiledCommand = new Mock();
-
- profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
- profiledCommand.Setup(m => m.Command).Returns("SET");
-
- var result = (SpanSdk)RedisProfilerEntryToSpanConverter.ProfilerCommandToSpan(this.tracer, null, profiledCommand.Object);
- Assert.Equal("SET", result.Name);
- }
-
- [Fact]
- public void ProfiledCommandToSpanUsesTimestampAsStartTime()
- {
- var profiledCommand = new Mock();
- var now = DateTimeOffset.Now;
- profiledCommand.Setup(m => m.CommandCreated).Returns(now.DateTime);
- var result = (SpanSdk)RedisProfilerEntryToSpanConverter.ProfilerCommandToSpan(this.tracer, null, profiledCommand.Object);
- Assert.Equal(now, result.StartTimestamp);
- }
-
- [Fact]
- public void ProfiledCommandToSpanSetsDbTypeAttributeAsRedis()
- {
- var profiledCommand = new Mock();
- profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
- var result = (SpanSdk)RedisProfilerEntryToSpanConverter.ProfilerCommandToSpan(this.tracer, null, profiledCommand.Object);
- Assert.Contains(result.Attributes, kvp => kvp.Key == "db.type");
- Assert.Equal("redis", result.Attributes.GetValue("db.type"));
- }
-
- [Fact]
- public void ProfiledCommandToSpanUsesCommandAsDbStatementAttribute()
- {
- var profiledCommand = new Mock();
- profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
- profiledCommand.Setup(m => m.Command).Returns("SET");
- var result = (SpanSdk)RedisProfilerEntryToSpanConverter.ProfilerCommandToSpan(this.tracer, null, profiledCommand.Object);
- Assert.Contains(result.Attributes, kvp => kvp.Key == "db.statement");
- Assert.Equal("SET", result.Attributes.GetValue("db.statement"));
- }
-
- [Fact]
- public void ProfiledCommandToSpanUsesFlagsForFlagsAttribute()
- {
- var profiledCommand = new Mock();
- profiledCommand.Setup(m => m.CommandCreated).Returns(DateTime.UtcNow);
- var expectedFlags = StackExchange.Redis.CommandFlags.FireAndForget |
- StackExchange.Redis.CommandFlags.NoRedirect;
- profiledCommand.Setup(m => m.Flags).Returns(expectedFlags);
- var result = (SpanSdk)RedisProfilerEntryToSpanConverter.ProfilerCommandToSpan(this.tracer, null, profiledCommand.Object);
- Assert.Contains(result.Attributes, kvp => kvp.Key == "redis.flags");
- Assert.Equal("None, FireAndForget, NoRedirect", result.Attributes.GetValue("redis.flags"));
- }
- }
-}
diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj
index 33c60cd51..d5e02d8f4 100644
--- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj
+++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj
@@ -5,6 +5,10 @@
$(TargetFrameworks);net461
+
+
+
+
diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs
index 69aba255a..318c1490b 100644
--- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs
+++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs
@@ -13,36 +13,128 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
-
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
using System.Threading.Tasks;
using Moq;
+using OpenTelemetry.Internal.Test;
+using OpenTelemetry.Trace;
using OpenTelemetry.Trace.Configuration;
using OpenTelemetry.Trace.Export;
+using StackExchange.Redis;
using StackExchange.Redis.Profiling;
using Xunit;
-namespace OpenTelemetry.Instrumentation.StackExchangeRedis
+namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Tests
{
+ [Collection("Redis")]
public class StackExchangeRedisCallsInstrumentationTests
{
+ /*
+ To run the integration tests, set the OT_REDISENDPOINT machine-level environment variable to a valid Redis endpoint.
+
+ To use Docker...
+ 1) Run: docker run -d --name redis -p 6379:6379 redis
+ 2) Set OT_REDISENDPOINT as: localhost:6379
+ */
+
+ private const string RedisEndPointEnvVarName = "OT_REDISENDPOINT";
+ private static readonly string RedisEndPoint = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(RedisEndPointEnvVarName);
+
+ [Trait("CategoryName", "RedisIntegrationTests")]
+ [SkipUnlessEnvVarFoundTheory(RedisEndPointEnvVarName)]
+ [InlineData("value1")]
+ public void SuccessfulCommandTest(string value)
+ {
+ var connectionOptions = new ConfigurationOptions
+ {
+ AbortOnConnectFail = true,
+ };
+ connectionOptions.EndPoints.Add(RedisEndPoint);
+
+ using var connection = ConnectionMultiplexer.Connect(connectionOptions);
+
+ var activityProcessor = new Mock();
+ using (OpenTelemetrySdk.EnableOpenTelemetry(b =>
+ {
+ b.AddProcessorPipeline(c => c.AddProcessor(ap => activityProcessor.Object));
+ b.AddRedisInstrumentation(connection);
+ }))
+ {
+ IDatabase db = connection.GetDatabase();
+
+ bool set = db.StringSet("key1", value, TimeSpan.FromSeconds(60));
+
+ Assert.True(set);
+
+ var redisValue = db.StringGet("key1");
+
+ Assert.True(redisValue.HasValue);
+ Assert.Equal(value, redisValue.ToString());
+ }
+
+ // Disposing SDK should flush the Redis profiling session immediately.
+
+ Assert.Equal(4, activityProcessor.Invocations.Count);
+
+ VerifyActivityData((Activity)activityProcessor.Invocations[1].Arguments[0], true, connection.GetEndPoints()[0]);
+ VerifyActivityData((Activity)activityProcessor.Invocations[3].Arguments[0], false, connection.GetEndPoints()[0]);
+ }
+
[Fact]
public async void ProfilerSessionUsesTheSameDefault()
{
- var spanProcessor = new Mock();
- var tracer = TracerFactory.Create(b => b
- .AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object)))
- .GetTracer(null);
+ var connectionOptions = new ConfigurationOptions
+ {
+ AbortOnConnectFail = false,
+ };
+ connectionOptions.EndPoints.Add("localhost:6379");
- using var instrumentation = new StackExchangeRedisCallsInstrumentation(tracer);
+ var connection = ConnectionMultiplexer.Connect(connectionOptions);
+
+ using var instrumentation = new StackExchangeRedisCallsInstrumentation(connection, new StackExchangeRedisCallsInstrumentationOptions());
var profilerFactory = instrumentation.GetProfilerSessionsFactory();
var first = profilerFactory();
var second = profilerFactory();
-
ProfilingSession third = null;
await Task.Delay(1).ContinueWith((t) => { third = profilerFactory(); });
-
Assert.Equal(first, second);
Assert.Equal(second, third);
}
+
+ private static void VerifyActivityData(Activity activity, bool isSet, EndPoint endPoint)
+ {
+ if (isSet)
+ {
+ Assert.Equal("SETEX", activity.DisplayName);
+ Assert.Equal("SETEX", activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.DatabaseStatementKey).Value);
+ }
+ else
+ {
+ Assert.Equal("GET", activity.DisplayName);
+ Assert.Equal("GET", activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.DatabaseStatementKey).Value);
+ }
+
+ Assert.Equal(SpanHelper.GetCachedCanonicalCodeString(StatusCanonicalCode.Ok), activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value);
+ Assert.Equal("redis", activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.DatabaseSystemKey).Value);
+ Assert.Equal("0", activity.Tags.FirstOrDefault(t => t.Key == StackExchangeRedisCallsInstrumentation.RedisDatabaseIndexKeyName).Value);
+
+ if (endPoint is IPEndPoint ipEndPoint)
+ {
+ Assert.Equal(ipEndPoint.Address.ToString(), activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.NetPeerIp).Value);
+ Assert.Equal(ipEndPoint.Port.ToString(), activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.NetPeerPort).Value);
+ }
+ else if (endPoint is DnsEndPoint dnsEndPoint)
+ {
+ Assert.Equal(dnsEndPoint.Host, activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.NetPeerName).Value);
+ Assert.Equal(dnsEndPoint.Port.ToString(), activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.NetPeerPort).Value);
+ }
+ else
+ {
+ Assert.Equal(endPoint.ToString(), activity.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.PeerServiceKey).Value);
+ }
+ }
}
}
diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.integration.yml b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.integration.yml
new file mode 100644
index 000000000..51ecb1036
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.integration.yml
@@ -0,0 +1,20 @@
+# Start a redis container and then run OpenTelemetry redis integration tests.
+# This should be run from the root of the repo:
+# opentelemetry>docker-compose --file=test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.integration.yml --project-directory=. up --exit-code-from=redis_integration_tests --build
+version: '3.1'
+
+services:
+ redis:
+ image: redis
+ ports:
+ - "6379:6379"
+
+ redis_integration_tests:
+ build:
+ context: .
+ dockerfile: ./test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile
+ command: --filter CategoryName=RedisIntegrationTests
+ environment:
+ - OT_REDISENDPOINT=redis:6379
+ depends_on:
+ - redis
\ No newline at end of file
diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile
new file mode 100644
index 000000000..8ef21274a
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile
@@ -0,0 +1,23 @@
+# Create a container for running the OpenTelemetry redis unit tests.
+# This should be run from the root of the repo:
+# opentelemetry>docker build -f test\OpenTelemetry.Instrumentation.StackExchangeRedis.Tests\dockerfile .
+FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS base
+
+FROM mcr.microsoft.com/dotnet/core/sdk:3.1 as build
+ARG PUBLISH_CONFIGURATION=Release
+ARG PUBLISH_FRAMEWORK=netcoreapp3.1
+WORKDIR /src
+COPY ["NuGet.config", ""] # Needed for the .NET 5 preview packages. Won't be needed in the future.
+COPY ["test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj", "test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/"]
+COPY ["src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj", "src/OpenTelemetry.Instrumentation.StackExchangeRedis/"]
+COPY ["src/OpenTelemetry/OpenTelemetry.csproj", "src/OpenTelemetry/"]
+COPY ["src/OpenTelemetry.Api/OpenTelemetry.Api.csproj", "src/OpenTelemetry.Api/"]
+RUN dotnet restore "test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj" --configfile "NuGet.config"
+COPY . .
+WORKDIR "/src/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests"
+RUN dotnet publish "OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj" -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /build -p:IntegrationBuild=true --no-restore
+
+FROM base AS final
+WORKDIR /test
+COPY --from=build /build .
+ENTRYPOINT ["dotnet", "test", "OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.dll"]
\ No newline at end of file
diff --git a/test/OpenTelemetry.Instrumentation.Dependencies.Tests/SkipUnlessEnvVarFoundTheoryAttribute.cs b/test/OpenTelemetry.Tests/Implementation/Internal/SkipUnlessEnvVarFoundTheoryAttribute.cs
similarity index 62%
rename from test/OpenTelemetry.Instrumentation.Dependencies.Tests/SkipUnlessEnvVarFoundTheoryAttribute.cs
rename to test/OpenTelemetry.Tests/Implementation/Internal/SkipUnlessEnvVarFoundTheoryAttribute.cs
index 5bb7169e5..7e96a2004 100644
--- a/test/OpenTelemetry.Instrumentation.Dependencies.Tests/SkipUnlessEnvVarFoundTheoryAttribute.cs
+++ b/test/OpenTelemetry.Tests/Implementation/Internal/SkipUnlessEnvVarFoundTheoryAttribute.cs
@@ -16,16 +16,28 @@
using System;
using Xunit;
-namespace OpenTelemetry.Instrumentation.Dependencies.Tests
+namespace OpenTelemetry.Internal.Test
{
public class SkipUnlessEnvVarFoundTheoryAttribute : TheoryAttribute
{
public SkipUnlessEnvVarFoundTheoryAttribute(string environmentVariable)
{
- if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(environmentVariable, EnvironmentVariableTarget.Machine)))
+ if (string.IsNullOrEmpty(GetEnvironmentVariable(environmentVariable)))
{
this.Skip = $"Skipped because {environmentVariable} environment variable was not configured.";
}
}
+
+ public static string GetEnvironmentVariable(string environmentVariableName)
+ {
+ string environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process);
+
+ if (string.IsNullOrEmpty(environmentVariableValue))
+ {
+ environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Machine);
+ }
+
+ return environmentVariableValue;
+ }
}
}