Redis instrumentation with ActivitySource by Eddy & Mike (#800)

* Attempting to get Redis instrumentation up and working using ActivitySource API.

* updating tests

* renaming and updating tests

* Test fixes.

* Fixed Redis net461 tests failing on connection.

* Redis integration tests and bug fixes.

* Adding tests

* Put back redis connection options.

* Updates for changes in master.

* adding comments to testRedis

* updating based on comments

* updating summary

* Switched a couple spots using string keys to constants.

* Redis integration tests

* Small tweak to the GitHub action to make it more consistent with the others.

* Made instrumentation internal. Updated README.

* rename MaxFlushInterval to FlushInterval, adding flushInterval to samples

* Removed running of non-integration tests from Redis dockerfile.

Co-authored-by: Eddy Nakamura <eddynaka@gmail.com>
Co-authored-by: Cijo Thomas <cithomas@microsoft.com>
This commit is contained in:
Mikel Blanchard 2020-07-14 08:47:59 -07:00 committed by GitHub
parent 3214f1cd96
commit 9298e0ef3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 675 additions and 333 deletions

21
.dockerignore Normal file
View File

@ -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

15
.github/workflows/integration-redis.yml vendored Normal file
View File

@ -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

View File

@ -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}"

View File

@ -2,7 +2,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="MinVer" Version="2.3.0">
<PackageReference Include="MinVer" Version="2.3.0" Condition="'$(IntegrationBuild)' != 'true'">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.3.0" />
<PackageReference Include="StackExchange.Redis" Version="2.0.519" />
<PackageReference Include="StackExchange.Redis" Version="2.1.58" />
</ItemGroup>
<ItemGroup>

View File

@ -38,7 +38,7 @@ namespace Samples
/// <param name="args">Arguments from command line.</param>
public static void Main(string[] args)
{
Parser.Default.ParseArguments<JaegerOptions, ZipkinOptions, PrometheusOptions, HttpClientOptions, ZPagesOptions, ConsoleOptions, OtlpOptions>(args)
Parser.Default.ParseArguments<JaegerOptions, ZipkinOptions, PrometheusOptions, HttpClientOptions, RedisOptions, ZPagesOptions, ConsoleOptions, OtlpOptions>(args)
.MapResult(
(JaegerOptions options) => TestJaegerExporter.Run(options.Host, options.Port),
(ZipkinOptions options) => TestZipkinExporter.Run(options.Uri),

View File

@ -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

View File

@ -2,7 +2,7 @@
<Import Project="..\Directory.Build.targets" Condition="Exists('..\Directory.Build.targets')" />
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-*">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-*" Condition="'$(IntegrationBuild)' != 'true'">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -0,0 +1,118 @@
// <copyright file="RedisProfilerEntryToActivityConverter.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<IProfiledCommand> sessionCommands)
{
foreach (var command in sessionCommands)
{
ProfilerCommandToActivity(parentActivity, command);
}
}
}
}

View File

@ -1,90 +0,0 @@
// <copyright file="RedisProfilerEntryToSpanConverter.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<IProfiledCommand> sessionCommands)
{
foreach (var command in sessionCommands)
{
ProfilerCommandToSpan(tracer, parentSpan, command);
}
}
}
}

View File

@ -7,7 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\OpenTelemetry\OpenTelemetry.csproj" />
<PackageReference Include="StackExchange.Redis" Version="2.0.519" />
<PackageReference Include="StackExchange.Redis" Version="2.1.58" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,53 @@
// <copyright file="OpenTelemetryBuilderExtensions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System;
using OpenTelemetry.Instrumentation.StackExchangeRedis;
using StackExchange.Redis;
namespace OpenTelemetry.Trace.Configuration
{
/// <summary>
/// Extension methods to simplify registering of dependency instrumentation.
/// </summary>
public static class OpenTelemetryBuilderExtensions
{
/// <summary>
/// Enables the outgoing requests automatic data collection for Redis.
/// </summary>
/// <param name="builder"><see cref="OpenTelemetryBuilder"/> being configured.</param>
/// <param name="connection"><see cref="ConnectionMultiplexer"/> to instrument.</param>
/// <param name="configureOptions">Redis configuration options.</param>
/// <returns>The instance of <see cref="OpenTelemetryBuilder"/> to chain the calls.</returns>
public static OpenTelemetryBuilder AddRedisInstrumentation(
this OpenTelemetryBuilder builder,
ConnectionMultiplexer connection,
Action<StackExchangeRedisCallsInstrumentationOptions> 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);
}
}
}

View File

@ -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;
})))
{
}
// 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).

View File

@ -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
/// <summary>
/// Redis calls instrumentation.
/// </summary>
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<TelemetrySpan, ProfilingSession> cache = new ConcurrentDictionary<TelemetrySpan, ProfilingSession>();
private readonly PropertyInfo spanEndTimestampInfo;
private readonly ConcurrentDictionary<ActivityTraceId, Tuple<Activity, ProfilingSession>> cache = new ConcurrentDictionary<ActivityTraceId, Tuple<Activity, ProfilingSession>>();
/// <summary>
/// Initializes a new instance of the <see cref="StackExchangeRedisCallsInstrumentation"/> class.
/// </summary>
/// <param name="tracer">Tracer to record traced with.</param>
public StackExchangeRedisCallsInstrumentation(Tracer tracer)
/// <param name="connection"><see cref="ConnectionMultiplexer"/> to instrument.</param>
/// <param name="options">Configuration options for redis instrumentation.</param>
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());
}
/// <summary>
@ -73,71 +72,70 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis
/// <returns>Session associated with the current span context to record Redis calls.</returns>
public Func<ProfilingSession> 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<Activity, ProfilingSession>(parent, new ProfilingSession());
this.cache.TryAdd(parent.TraceId, session);
}
return session.Item2;
};
}
/// <inheritdoc/>
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());
if (this.stopHandle.WaitOne(this.options.FlushInterval))
{
break;
}
this.Flush();
}
}
private void Flush()
{
RedisProfilerEntryToActivityConverter.DrainSession(null, this.defaultSession.FinishProfiling());
foreach (var entry in this.cache)
{
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)
var parent = entry.Value.Item1;
if (parent.Duration == TimeSpan.Zero)
{
this.cache.TryRemove(span, out session);
}
else
{
this.cache.TryGetValue(span, out session);
// Activity is still running, don't drain.
continue;
}
if (session != null)
{
RedisProfilerEntryToSpanConverter.DrainSession(this.tracer, span, session.FinishProfiling());
}
}
Thread.Sleep(TimeSpan.FromSeconds(1));
RedisProfilerEntryToActivityConverter.DrainSession(parent, entry.Value.Item2.FinishProfiling());
}
}
}

View File

@ -13,6 +13,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System;
using System.Diagnostics;
namespace OpenTelemetry.Instrumentation.StackExchangeRedis
{
@ -21,5 +23,9 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis
/// </summary>
public class StackExchangeRedisCallsInstrumentationOptions
{
/// <summary>
/// Gets or sets the maximum time that should elapse between flushing the internal buffer of Redis profiling sessions and creating <see cref="Activity"/> objects. Default value: 00:00:10.
/// </summary>
public TimeSpan FlushInterval { get; set; } = TimeSpan.FromSeconds(10);
}
}

View File

@ -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(

View File

@ -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();

View File

@ -16,6 +16,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="..\OpenTelemetry.Tests\Implementation\Internal\SkipUnlessEnvVarFoundTheoryAttribute.cs" Link="SkipUnlessEnvVarFoundTheoryAttribute.cs" />
<Compile Include="..\OpenTelemetry.Tests\Implementation\Internal\TestHttpServer.cs" Link="TestHttpServer.cs" />
</ItemGroup>

View File

@ -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)]

View File

@ -1,29 +0,0 @@
// <copyright file="AttributesExtensions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System.Collections.Generic;
using System.Linq;
namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Tests
{
internal static class AttributesExtensions
{
public static object GetValue(this IEnumerable<KeyValuePair<string, object>> attributes, string key)
{
return attributes.FirstOrDefault(kvp => kvp.Key == key).Value;
}
}
}

View File

@ -0,0 +1,178 @@
// <copyright file="RedisProfilerEntryToActivityConverterTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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
}
}

View File

@ -1,92 +0,0 @@
// <copyright file="RedisProfilerEntryToSpanConverterTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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<IProfiledCommand>();
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"));
}
}
}

View File

@ -5,6 +5,10 @@
<TargetFrameworks Condition="$(OS) == 'Windows_NT'">$(TargetFrameworks);net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\OpenTelemetry.Tests\Implementation\Internal\SkipUnlessEnvVarFoundTheoryAttribute.cs" Link="Implementation\SkipUnlessEnvVarFoundTheoryAttribute.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenTelemetry.Instrumentation.StackExchangeRedis\OpenTelemetry.Instrumentation.StackExchangeRedis.csproj" />
<ProjectReference Include="..\..\src\OpenTelemetry\OpenTelemetry.csproj" />

View File

@ -13,36 +13,128 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
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<ActivityProcessor>();
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<SpanProcessor>();
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);
}
}
}
}

View File

@ -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

View File

@ -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"]

View File

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