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:
parent
1c82e6aaa3
commit
c1d72e6927
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue