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; + } } }