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:
parent
3214f1cd96
commit
9298e0ef3f
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
@ -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());
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue