Add Sql Collector (Part 2) (#536)

* Picking up where alexvaluyskiy left off with the SqlClientCollector. Worked on PR feedback. Added unit tests.

* Updated README.

* Fixed inconsistent comments.

* Code review.

* Code review #2.

* More code review.

* Code review feedback.

Co-authored-by: Sergey Kanzhelev <S.Kanzhelev@live.com>
This commit is contained in:
Mikel Blanchard 2020-03-17 13:37:01 -07:00 committed by GitHub
parent 1c82e6aaa3
commit c1d72e6927
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 616 additions and 36 deletions

View File

@ -57,7 +57,7 @@ Myget feeds:
| Package | MyGet (CI) | NuGet (releases) |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| ASP.NET Core | [![MyGet Nightly][OpenTelemetry-collect-aspnetcore-myget-image]][OpenTelemetry-collect-aspnetcore-myget-url] | [![NuGet Release][OpenTelemetry-collect-aspnetcore-nuget-image]][OpenTelemetry-collect-aspnetcore-nuget-url] |
| .NET Core HttpClient & Azure SDKs | [![MyGet Nightly][OpenTelemetry-collect-deps-myget-image]][OpenTelemetry-collect-deps-myget-url] | [![NuGet Release][OpenTelemetry-collect-deps-nuget-image]][OpenTelemetry-collect-deps-nuget-url] |
| .NET Core HttpClient, Microsoft.Data.SqlClient, System.Data.SqlClient, & Azure SDKs | [![MyGet Nightly][OpenTelemetry-collect-deps-myget-image]][OpenTelemetry-collect-deps-myget-url] | [![NuGet Release][OpenTelemetry-collect-deps-nuget-image]][OpenTelemetry-collect-deps-nuget-url] |
| StackExchange.Redis | [![MyGet Nightly][OpenTelemetry-collect-stackexchange-redis-myget-image]][OpenTelemetry-collect-stackexchange-redis-myget-url] | [![NuGet Release][OpenTelemetry-collect-stackexchange-redis-nuget-image]][OpenTelemetry-collect-stackexchange-redis-nuget-url] |
### Exporters Packages

View File

@ -21,7 +21,7 @@ namespace LoggingTracer.Demo.AspNetCore
var tracerFactory = new LoggingTracerFactory();
var tracer = tracerFactory.GetTracer("ServerApp", "semver:1.0.0");
var dependenciesCollector = new DependenciesCollector(new HttpClientCollectorOptions(), tracerFactory);
var dependenciesCollector = new DependenciesCollector(tracerFactory);
var aspNetCoreCollector = new AspNetCoreCollector(tracer);
return tracerFactory;

View File

@ -16,17 +16,27 @@
namespace OpenTelemetry.Trace
{
internal static class SpanAttributeConstants
/// <summary>
/// Defines well-known span attribute keys.
/// </summary>
public static class SpanAttributeConstants
{
public static readonly string ComponentKey = "component";
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public const string ComponentKey = "component";
public const string PeerServiceKey = "peer.service";
public static readonly string HttpMethodKey = "http.method";
public static readonly string HttpStatusCodeKey = "http.status_code";
public static readonly string HttpUserAgentKey = "http.user_agent";
public static readonly string HttpPathKey = "http.path";
public static readonly string HttpHostKey = "http.host";
public static readonly string HttpUrlKey = "http.url";
public static readonly string HttpRouteKey = "http.route";
public static readonly string HttpFlavorKey = "http.flavor";
public const string HttpMethodKey = "http.method";
public const string HttpStatusCodeKey = "http.status_code";
public const string HttpUserAgentKey = "http.user_agent";
public const string HttpPathKey = "http.path";
public const string HttpHostKey = "http.host";
public const string HttpUrlKey = "http.url";
public const string HttpRouteKey = "http.route";
public const string HttpFlavorKey = "http.flavor";
public const string DatabaseTypeKey = "db.type";
public const string DatabaseInstanceKey = "db.instance";
public const string DatabaseStatementKey = "db.statement";
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}
}

View File

@ -34,6 +34,19 @@ namespace OpenTelemetry.Trace
return span;
}
/// <summary>
/// Helper method that populates span properties from component
/// to https://github.com/open-telemetry/opentelemetry-specification/blob/2316771e7e0ca3bfe9b2286d13e3a41ded6b8858/specification/data-span-general.md.
/// </summary>
/// <param name="span">Span to fill out.</param>
/// <param name="peerService">Peer service.</param>
/// <returns>Span with populated http method properties.</returns>
public static TelemetrySpan PutPeerServiceAttribute(this TelemetrySpan span, string peerService)
{
span.SetAttribute(SpanAttributeConstants.PeerServiceKey, peerService);
return span;
}
/// <summary>
/// Helper method that populates span properties from http method according
/// to https://github.com/open-telemetry/opentelemetry-specification/blob/2316771e7e0ca3bfe9b2286d13e3a41ded6b8858/specification/data-http.md.
@ -208,11 +221,50 @@ namespace OpenTelemetry.Trace
/// </summary>
/// <param name="span">Span to fill out.</param>
/// <param name="flavor">HTTP version.</param>
/// <returns>Span with populated request size properties.</returns>
/// <returns>Span with populated properties.</returns>
public static TelemetrySpan PutHttpFlavorAttribute(this TelemetrySpan span, string flavor)
{
span.SetAttribute(SpanAttributeConstants.HttpFlavorKey, flavor);
return span;
}
/// <summary>
/// Helper method that populates database type
/// to https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-database.md.
/// </summary>
/// <param name="span">Span to fill out.</param>
/// <param name="type">Database type.</param>
/// <returns>Span with populated properties.</returns>
public static TelemetrySpan PutDatabaseTypeAttribute(this TelemetrySpan span, string type)
{
span.SetAttribute(SpanAttributeConstants.DatabaseTypeKey, type);
return span;
}
/// <summary>
/// Helper method that populates database instance
/// to https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-database.md.
/// </summary>
/// <param name="span">Span to fill out.</param>
/// <param name="instance">Database instance.</param>
/// <returns>Span with populated properties.</returns>
public static TelemetrySpan PutDatabaseInstanceAttribute(this TelemetrySpan span, string instance)
{
span.SetAttribute(SpanAttributeConstants.DatabaseInstanceKey, instance);
return span;
}
/// <summary>
/// Helper method that populates database statement
/// to https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-database.md.
/// </summary>
/// <param name="span">Span to fill out.</param>
/// <param name="statement">Database statement.</param>
/// <returns>Span with populated properties.</returns>
public static TelemetrySpan PutDatabaseStatementAttribute(this TelemetrySpan span, string statement)
{
span.SetAttribute(SpanAttributeConstants.DatabaseStatementKey, statement);
return span;
}
}
}

View File

@ -20,7 +20,7 @@ using OpenTelemetry.Trace;
namespace OpenTelemetry.Collector.Dependencies
{
/// <summary>
/// Instrumentation adaptor that automatically collect calls to http and Azure SDK.
/// Instrumentation adaptor that automatically collect calls to Http, SQL, and Azure SDK.
/// </summary>
public class DependenciesCollector : IDisposable
{
@ -29,18 +29,27 @@ namespace OpenTelemetry.Collector.Dependencies
/// <summary>
/// Initializes a new instance of the <see cref="DependenciesCollector"/> class.
/// </summary>
/// <param name="options">Configuration options.</param>
/// <param name="tracerFactory">Tracer factory to get a tracer from.</param>
public DependenciesCollector(HttpClientCollectorOptions options, TracerFactoryBase tracerFactory)
/// <param name="httpOptions">Http configuration options.</param>
/// <param name="sqlOptions">Sql configuration options.</param>
public DependenciesCollector(TracerFactoryBase tracerFactory, HttpClientCollectorOptions httpOptions = null, SqlClientCollectorOptions sqlOptions = null)
{
if (tracerFactory == null)
{
throw new ArgumentNullException(nameof(tracerFactory));
}
var assemblyVersion = typeof(DependenciesCollector).Assembly.GetName().Version;
var httpClientListener = new HttpClientCollector(tracerFactory.GetTracer(nameof(HttpClientCollector), "semver:" + assemblyVersion), options);
var httpClientListener = new HttpClientCollector(tracerFactory.GetTracer(nameof(HttpClientCollector), "semver:" + assemblyVersion), httpOptions ?? new HttpClientCollectorOptions());
var azureClientsListener = new AzureClientsCollector(tracerFactory.GetTracer(nameof(AzureClientsCollector), "semver:" + assemblyVersion));
var azurePipelineListener = new AzurePipelineCollector(tracerFactory.GetTracer(nameof(AzurePipelineCollector), "semver:" + assemblyVersion));
var sqlClientListener = new SqlClientCollector(tracerFactory.GetTracer(nameof(AzurePipelineCollector), "semver:" + assemblyVersion), sqlOptions ?? new SqlClientCollectorOptions());
this.collectors.Add(httpClientListener);
this.collectors.Add(azureClientsListener);
this.collectors.Add(azurePipelineListener);
this.collectors.Add(sqlClientListener);
}
/// <inheritdoc />

View File

@ -0,0 +1,156 @@
// <copyright file="SqlClientDiagnosticListener.cs" company="OpenTelemetry Authors">
// Copyright 2018, 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.Data;
using System.Diagnostics;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Collector.Dependencies.Implementation
{
internal class SqlClientDiagnosticListener : ListenerHandler
{
internal const string SqlDataBeforeExecuteCommand = "System.Data.SqlClient.WriteCommandBefore";
internal const string SqlMicrosoftBeforeExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandBefore";
internal const string SqlDataAfterExecuteCommand = "System.Data.SqlClient.WriteCommandAfter";
internal const string SqlMicrosoftAfterExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandAfter";
internal const string SqlDataWriteCommandError = "System.Data.SqlClient.WriteCommandError";
internal const string SqlMicrosoftWriteCommandError = "Microsoft.Data.SqlClient.WriteCommandError";
private const string DatabaseStatementTypeSpanAttributeKey = "db.statementType";
private readonly PropertyFetcher commandFetcher = new PropertyFetcher("Command");
private readonly PropertyFetcher connectionFetcher = new PropertyFetcher("Connection");
private readonly PropertyFetcher dataSourceFetcher = new PropertyFetcher("DataSource");
private readonly PropertyFetcher databaseFetcher = new PropertyFetcher("Database");
private readonly PropertyFetcher commandTypeFetcher = new PropertyFetcher("CommandType");
private readonly PropertyFetcher commandTextFetcher = new PropertyFetcher("CommandText");
private readonly PropertyFetcher exceptionFetcher = new PropertyFetcher("Exception");
private readonly SqlClientCollectorOptions options;
public SqlClientDiagnosticListener(string sourceName, Tracer tracer, SqlClientCollectorOptions options)
: base(sourceName, tracer)
{
this.options = options ?? throw new ArgumentNullException(nameof(options));
}
public override void OnStartActivity(Activity activity, object payload)
{
}
public override void OnCustom(string name, Activity activity, object payload)
{
switch (name)
{
case SqlDataBeforeExecuteCommand:
case SqlMicrosoftBeforeExecuteCommand:
{
var command = this.commandFetcher.Fetch(payload);
if (command == null)
{
CollectorEventSource.Log.NullPayload($"{nameof(SqlClientDiagnosticListener)}-{name}");
return;
}
var connection = this.connectionFetcher.Fetch(command);
var database = this.databaseFetcher.Fetch(connection);
this.Tracer.StartActiveSpan((string)database, SpanKind.Client, out var span);
if (span.IsRecording)
{
var dataSource = this.dataSourceFetcher.Fetch(connection);
var commandText = this.commandTextFetcher.Fetch(command);
span.PutComponentAttribute("sql");
span.PutDatabaseTypeAttribute("sql");
span.PutPeerServiceAttribute((string)dataSource);
span.PutDatabaseInstanceAttribute((string)database);
if (this.commandTypeFetcher.Fetch(command) is CommandType commandType)
{
span.SetAttribute(DatabaseStatementTypeSpanAttributeKey, commandType.ToString());
switch (commandType)
{
case CommandType.StoredProcedure:
if (this.options.CaptureStoredProcedureCommandName)
{
span.PutDatabaseStatementAttribute((string)commandText);
}
break;
case CommandType.Text:
if (this.options.CaptureTextCommandContent)
{
span.PutDatabaseStatementAttribute((string)commandText);
}
break;
}
}
}
}
break;
case SqlDataAfterExecuteCommand:
case SqlMicrosoftAfterExecuteCommand:
{
var span = this.Tracer.CurrentSpan;
if (span == null || !span.Context.IsValid)
{
CollectorEventSource.Log.NullOrBlankSpan($"{nameof(SqlClientDiagnosticListener)}-{name}");
return;
}
span.End();
}
break;
case SqlDataWriteCommandError:
case SqlMicrosoftWriteCommandError:
{
var span = this.Tracer.CurrentSpan;
if (span == null || !span.Context.IsValid)
{
CollectorEventSource.Log.NullOrBlankSpan($"{nameof(SqlClientDiagnosticListener)}-{name}");
return;
}
if (span.IsRecording)
{
if (this.exceptionFetcher.Fetch(payload) is Exception exception)
{
span.Status = Status.Unknown.WithDescription(exception.Message);
}
else
{
CollectorEventSource.Log.NullPayload($"{nameof(SqlClientDiagnosticListener)}-{name}");
}
}
}
break;
}
}
}
}

View File

@ -0,0 +1,60 @@
// <copyright file="SqlClientCollector.cs" company="OpenTelemetry Authors">
// Copyright 2018, 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.Collector.Dependencies.Implementation;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Collector.Dependencies
{
/// <summary>
/// SqlClient collector.
/// </summary>
public class SqlClientCollector : IDisposable
{
internal const string SqlClientDiagnosticListenerName = "SqlClientDiagnosticListener";
private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber;
/// <summary>
/// Initializes a new instance of the <see cref="SqlClientCollector"/> class.
/// </summary>
/// <param name="tracer">Tracer to record traced with.</param>
public SqlClientCollector(Tracer tracer)
: this(tracer, new SqlClientCollectorOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SqlClientCollector"/> class.
/// </summary>
/// <param name="tracer">Tracer to record traced with.</param>
/// <param name="options">Configuration options for sql collector.</param>
public SqlClientCollector(Tracer tracer, SqlClientCollectorOptions options)
{
this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(
name => new SqlClientDiagnosticListener(name, tracer, options),
listener => listener.Name == SqlClientDiagnosticListenerName,
null);
this.diagnosticSourceSubscriber.Subscribe();
}
/// <inheritdoc/>
public void Dispose()
{
this.diagnosticSourceSubscriber?.Dispose();
}
}
}

View File

@ -0,0 +1,42 @@
// <copyright file="SqlClientCollectorOptions.cs" company="OpenTelemetry Authors">
// Copyright 2018, 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.Data;
namespace OpenTelemetry.Collector.Dependencies
{
/// <summary>
/// Options for <see cref="SqlClientCollector"/>.
/// </summary>
public class SqlClientCollectorOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="SqlClientCollectorOptions"/> class.
/// </summary>
public SqlClientCollectorOptions()
{
}
/// <summary>
/// Gets or sets a value indicating whether or not the <see cref="SqlClientCollector"/> should capture the names of <see cref="CommandType.StoredProcedure"/> commands. Default value: True.
/// </summary>
public bool CaptureStoredProcedureCommandName { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the <see cref="SqlClientCollector"/> should capture the text of <see cref="CommandType.Text"/> commands. Default value: False.
/// </summary>
public bool CaptureTextCommandContent { get; set; }
}
}

View File

@ -39,34 +39,38 @@ namespace OpenTelemetry.Trace.Configuration
return builder
.AddCollector((t) => new AzureClientsCollector(t))
.AddCollector((t) => new AzurePipelineCollector(t))
.AddCollector((t) => new HttpClientCollector(t));
.AddCollector((t) => new HttpClientCollector(t))
.AddCollector((t) => new SqlClientCollector(t));
}
/// <summary>
/// Enables the outgoing requests automatic data collection.
/// </summary>
/// <param name="builder">Trace builder to use.</param>
/// <param name="configure">Configuration options.</param>
/// <param name="configureHttpCollectorOptions">Http configuration options.</param>
/// <param name="configureSqlCollectorOptions">Sql configuration options.</param>
/// <returns>The instance of <see cref="TracerBuilder"/> to chain the calls.</returns>
public static TracerBuilder AddDependencyCollector(this TracerBuilder builder, Action<HttpClientCollectorOptions> configure)
public static TracerBuilder AddDependencyCollector(
this TracerBuilder builder,
Action<HttpClientCollectorOptions> configureHttpCollectorOptions = null,
Action<SqlClientCollectorOptions> configureSqlCollectorOptions = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
var httpOptions = new HttpClientCollectorOptions();
configureHttpCollectorOptions?.Invoke(httpOptions);
var options = new HttpClientCollectorOptions();
configure(options);
var sqlOptions = new SqlClientCollectorOptions();
configureSqlCollectorOptions?.Invoke(sqlOptions);
return builder
.AddCollector((t) => new AzureClientsCollector(t))
.AddCollector((t) => new AzurePipelineCollector(t))
.AddCollector((t) => new HttpClientCollector(t, options));
.AddCollector((t) => new HttpClientCollector(t, httpOptions))
.AddCollector((t) => new SqlClientCollector(t, sqlOptions));
}
}
}

View File

@ -47,7 +47,7 @@ namespace OpenTelemetry.Exporter.Jaeger.Implementation
private static readonly Dictionary<string, int> PeerServiceKeyResolutionDictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["peer.service"] = 0, // peer.service primary.
[SpanAttributeConstants.PeerServiceKey] = 0, // peer.service primary.
["net.peer.name"] = 1, // peer.service first alternative.
["peer.hostname"] = 2, // peer.service second alternative.
["peer.address"] = 2, // peer.service second alternative.

View File

@ -30,12 +30,12 @@ namespace OpenTelemetry.Exporter.Zipkin.Implementation
private static readonly Dictionary<string, int> RemoteEndpointServiceNameKeyResolutionDictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["net.peer.name"] = 0, // RemoteEndpoint.ServiceName primary.
["peer.service"] = 0, // RemoteEndpoint.ServiceName primary.
["peer.hostname"] = 1, // RemoteEndpoint.ServiceName alternative.
["peer.address"] = 1, // RemoteEndpoint.ServiceName alternative.
["http.host"] = 2, // RemoteEndpoint.ServiceName for Http.
["db.instance"] = 2, // RemoteEndpoint.ServiceName for Redis.
[SpanAttributeConstants.PeerServiceKey] = 0, // RemoteEndpoint.ServiceName primary.
["net.peer.name"] = 1, // RemoteEndpoint.ServiceName first alternative.
["peer.hostname"] = 2, // RemoteEndpoint.ServiceName second alternative.
["peer.address"] = 2, // RemoteEndpoint.ServiceName second alternative.
["http.host"] = 3, // RemoteEndpoint.ServiceName for Http.
["db.instance"] = 4, // RemoteEndpoint.ServiceName for Redis.
};
private static readonly ConcurrentDictionary<string, ZipkinEndpoint> LocalEndpointCache = new ConcurrentDictionary<string, ZipkinEndpoint>();

View File

@ -54,7 +54,7 @@ namespace OpenTelemetry.Collector.Dependencies.Tests
{
TracerBuilder builder = null;
Assert.Throws<ArgumentNullException>(() => builder.AddDependencyCollector());
Assert.Throws<ArgumentNullException>(() => TracerFactory.Create(b => b.AddDependencyCollector(null)));
Assert.Throws<ArgumentNullException>(() => builder.AddDependencyCollector(null, null));
}
[Fact]
@ -209,7 +209,7 @@ namespace OpenTelemetry.Collector.Dependencies.Tests
await c.SendAsync(request);
}
Assert.Equal(0, spanProcessor.Invocations.Count);
Assert.Equal(0, spanProcessor.Invocations.Count);
}
[Fact]

View File

@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="1.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Moq" Version="4.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />

View File

@ -0,0 +1,246 @@
// <copyright file="SqlClientTests.cs" company="OpenTelemetry Authors">
// Copyright 2018, 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.Data;
using System.Diagnostics;
using System.Linq;
using Microsoft.Data.SqlClient;
using Moq;
using OpenTelemetry.Collector.Dependencies.Implementation;
using OpenTelemetry.Trace;
using OpenTelemetry.Trace.Configuration;
using OpenTelemetry.Trace.Export;
using Xunit;
namespace OpenTelemetry.Collector.Dependencies.Tests
{
public class SqlClientTests : IDisposable
{
private const string TestConnectionString = "Data Source=(localdb)\\MSSQLLocalDB;Database=master";
private readonly FakeSqlClientDiagnosticSource fakeSqlClientDiagnosticSource;
public SqlClientTests()
{
this.fakeSqlClientDiagnosticSource = new FakeSqlClientDiagnosticSource();
}
public void Dispose()
{
this.fakeSqlClientDiagnosticSource.Dispose();
}
[Theory]
[InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", true, false)]
[InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.Text, "select * from sys.databases", true, false)]
[InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", false, true)]
[InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.Text, "select * from sys.databases", false, true)]
public void SqlClientCallsAreCollectedSuccessfully(
string beforeCommand,
string afterCommand,
CommandType commandType,
string commandText,
bool captureStoredProcedureCommandName,
bool captureTextCommandContent)
{
var activity = new Activity("Current").AddBaggage("Stuff", "123");
activity.Start();
var spanProcessor = new Mock<SpanProcessor>();
var tracer = TracerFactory.Create(b => b
.AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object)))
.GetTracer(null);
using (new SqlClientCollector(
tracer,
new SqlClientCollectorOptions
{
CaptureStoredProcedureCommandName = captureStoredProcedureCommandName,
CaptureTextCommandContent = captureTextCommandContent,
}))
{
var operationId = Guid.NewGuid();
var sqlConnection = new SqlConnection(TestConnectionString);
var sqlCommand = sqlConnection.CreateCommand();
sqlCommand.CommandType = commandType;
sqlCommand.CommandText = commandText;
var beforeExecuteEventData = new
{
OperationId = operationId,
Command = sqlCommand,
Timestamp = (long?)1000000L,
};
this.fakeSqlClientDiagnosticSource.Write(
beforeCommand,
beforeExecuteEventData);
var afterExecuteEventData = new
{
OperationId = operationId,
Command = sqlCommand,
Timestamp = 2000000L,
};
this.fakeSqlClientDiagnosticSource.Write(
afterCommand,
afterExecuteEventData);
}
Assert.Equal(2, spanProcessor.Invocations.Count); // begin was called
var span = (SpanData)spanProcessor.Invocations[1].Arguments[0];
Assert.Equal("master", span.Name);
Assert.Equal(SpanKind.Client, span.Kind);
Assert.Equal(CanonicalCode.Ok, span.Status.CanonicalCode);
Assert.Null(span.Status.Description);
Assert.Equal("sql", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.ComponentKey).Value as string);
Assert.Equal("sql", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseTypeKey).Value as string);
Assert.Equal("master", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseInstanceKey).Value as string);
switch (commandType)
{
case CommandType.StoredProcedure:
if (captureStoredProcedureCommandName)
{
Assert.Equal(commandText, span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseStatementKey).Value as string);
}
else
{
Assert.Null(span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseStatementKey).Value as string);
}
break;
case CommandType.Text:
if (captureTextCommandContent)
{
Assert.Equal(commandText, span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseStatementKey).Value as string);
}
else
{
Assert.Null(span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseStatementKey).Value as string);
}
break;
}
Assert.Equal("(localdb)\\MSSQLLocalDB", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.PeerServiceKey).Value as string);
activity.Stop();
}
[Theory]
[InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError)]
[InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError)]
public void SqlClientErrorsAreCollectedSuccessfully(string beforeCommand, string errorCommand)
{
var activity = new Activity("Current").AddBaggage("Stuff", "123");
activity.Start();
var spanProcessor = new Mock<SpanProcessor>();
var tracer = TracerFactory.Create(b => b
.AddProcessorPipeline(p => p.AddProcessor(_ => spanProcessor.Object)))
.GetTracer(null);
using (new SqlClientCollector(tracer))
{
var operationId = Guid.NewGuid();
var sqlConnection = new SqlConnection(TestConnectionString);
var sqlCommand = sqlConnection.CreateCommand();
sqlCommand.CommandText = "SP_GetOrders";
sqlCommand.CommandType = CommandType.StoredProcedure;
var beforeExecuteEventData = new
{
OperationId = operationId,
Command = sqlCommand,
Timestamp = (long?)1000000L,
};
this.fakeSqlClientDiagnosticSource.Write(
beforeCommand,
beforeExecuteEventData);
var commandErrorEventData = new
{
OperationId = operationId,
Command = sqlCommand,
Exception = new Exception("Boom!"),
Timestamp = 2000000L,
};
this.fakeSqlClientDiagnosticSource.Write(
errorCommand,
commandErrorEventData);
}
Assert.Equal(1, spanProcessor.Invocations.Count); // begin and end was called
var span = (SpanData)spanProcessor.Invocations[0].Arguments[0];
Assert.Equal("master", span.Name);
Assert.Equal(SpanKind.Client, span.Kind);
Assert.Equal(CanonicalCode.Unknown, span.Status.CanonicalCode);
Assert.Equal("Boom!", span.Status.Description);
Assert.Equal("sql", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.ComponentKey).Value as string);
Assert.Equal("sql", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseTypeKey).Value as string);
Assert.Equal("master", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseInstanceKey).Value as string);
Assert.Equal("SP_GetOrders", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.DatabaseStatementKey).Value as string);
Assert.Equal("(localdb)\\MSSQLLocalDB", span.Attributes.FirstOrDefault(i =>
i.Key == SpanAttributeConstants.PeerServiceKey).Value as string);
activity.Stop();
}
private class FakeSqlClientDiagnosticSource : IDisposable
{
private readonly DiagnosticListener listener;
public FakeSqlClientDiagnosticSource()
{
this.listener = new DiagnosticListener(SqlClientCollector.SqlClientDiagnosticListenerName);
}
public void Write(string name, object value)
{
this.listener.Write(name, value);
}
public void Dispose()
{
this.listener.Dispose();
}
}
}
}