diff --git a/README.md b/README.md index 1e63dfd899..7d05aa8687 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/samples/LoggingTracer/LoggingTracer.Demo.AspNetCore/Startup.cs b/samples/LoggingTracer/LoggingTracer.Demo.AspNetCore/Startup.cs index 836bb6d2c5..cc8e4aeed4 100644 --- a/samples/LoggingTracer/LoggingTracer.Demo.AspNetCore/Startup.cs +++ b/samples/LoggingTracer/LoggingTracer.Demo.AspNetCore/Startup.cs @@ -21,7 +21,7 @@ public void ConfigureServices(IServiceCollection services) 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; diff --git a/src/OpenTelemetry.Api/Trace/SpanAttributeConstants.cs b/src/OpenTelemetry.Api/Trace/SpanAttributeConstants.cs index 754e7d1919..0a95e05ff6 100644 --- a/src/OpenTelemetry.Api/Trace/SpanAttributeConstants.cs +++ b/src/OpenTelemetry.Api/Trace/SpanAttributeConstants.cs @@ -16,17 +16,27 @@ namespace OpenTelemetry.Trace { - internal static class SpanAttributeConstants + /// + /// Defines well-known span attribute keys. + /// + 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 } } diff --git a/src/OpenTelemetry.Api/Trace/SpanExtensions.cs b/src/OpenTelemetry.Api/Trace/SpanExtensions.cs index f2cde1ee57..50aa813d0d 100644 --- a/src/OpenTelemetry.Api/Trace/SpanExtensions.cs +++ b/src/OpenTelemetry.Api/Trace/SpanExtensions.cs @@ -34,6 +34,19 @@ public static TelemetrySpan PutComponentAttribute(this TelemetrySpan span, strin return span; } + /// + /// Helper method that populates span properties from component + /// to https://github.com/open-telemetry/opentelemetry-specification/blob/2316771e7e0ca3bfe9b2286d13e3a41ded6b8858/specification/data-span-general.md. + /// + /// Span to fill out. + /// Peer service. + /// Span with populated http method properties. + public static TelemetrySpan PutPeerServiceAttribute(this TelemetrySpan span, string peerService) + { + span.SetAttribute(SpanAttributeConstants.PeerServiceKey, peerService); + return span; + } + /// /// 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 @@ public static TelemetrySpan PutHttpStatusCode(this TelemetrySpan span, int statu /// /// Span to fill out. /// HTTP version. - /// Span with populated request size properties. + /// Span with populated properties. public static TelemetrySpan PutHttpFlavorAttribute(this TelemetrySpan span, string flavor) { span.SetAttribute(SpanAttributeConstants.HttpFlavorKey, flavor); return span; } + + /// + /// Helper method that populates database type + /// to https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-database.md. + /// + /// Span to fill out. + /// Database type. + /// Span with populated properties. + public static TelemetrySpan PutDatabaseTypeAttribute(this TelemetrySpan span, string type) + { + span.SetAttribute(SpanAttributeConstants.DatabaseTypeKey, type); + return span; + } + + /// + /// Helper method that populates database instance + /// to https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-database.md. + /// + /// Span to fill out. + /// Database instance. + /// Span with populated properties. + public static TelemetrySpan PutDatabaseInstanceAttribute(this TelemetrySpan span, string instance) + { + span.SetAttribute(SpanAttributeConstants.DatabaseInstanceKey, instance); + return span; + } + + /// + /// Helper method that populates database statement + /// to https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-database.md. + /// + /// Span to fill out. + /// Database statement. + /// Span with populated properties. + public static TelemetrySpan PutDatabaseStatementAttribute(this TelemetrySpan span, string statement) + { + span.SetAttribute(SpanAttributeConstants.DatabaseStatementKey, statement); + return span; + } } } diff --git a/src/OpenTelemetry.Collector.Dependencies/DependenciesCollector.cs b/src/OpenTelemetry.Collector.Dependencies/DependenciesCollector.cs index 48bdbb283b..0ec20f415b 100644 --- a/src/OpenTelemetry.Collector.Dependencies/DependenciesCollector.cs +++ b/src/OpenTelemetry.Collector.Dependencies/DependenciesCollector.cs @@ -20,7 +20,7 @@ namespace OpenTelemetry.Collector.Dependencies { /// - /// Instrumentation adaptor that automatically collect calls to http and Azure SDK. + /// Instrumentation adaptor that automatically collect calls to Http, SQL, and Azure SDK. /// public class DependenciesCollector : IDisposable { @@ -29,18 +29,27 @@ public class DependenciesCollector : IDisposable /// /// Initializes a new instance of the class. /// - /// Configuration options. /// Tracer factory to get a tracer from. - public DependenciesCollector(HttpClientCollectorOptions options, TracerFactoryBase tracerFactory) + /// Http configuration options. + /// Sql configuration options. + 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); } /// diff --git a/src/OpenTelemetry.Collector.Dependencies/Implementation/SqlClientDiagnosticListener.cs b/src/OpenTelemetry.Collector.Dependencies/Implementation/SqlClientDiagnosticListener.cs new file mode 100644 index 0000000000..d965b883cf --- /dev/null +++ b/src/OpenTelemetry.Collector.Dependencies/Implementation/SqlClientDiagnosticListener.cs @@ -0,0 +1,156 @@ +// +// 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. +// +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; + } + } + } +} diff --git a/src/OpenTelemetry.Collector.Dependencies/SqlClientCollector.cs b/src/OpenTelemetry.Collector.Dependencies/SqlClientCollector.cs new file mode 100644 index 0000000000..143c6785d5 --- /dev/null +++ b/src/OpenTelemetry.Collector.Dependencies/SqlClientCollector.cs @@ -0,0 +1,60 @@ +// +// 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. +// +using System; +using OpenTelemetry.Collector.Dependencies.Implementation; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Collector.Dependencies +{ + /// + /// SqlClient collector. + /// + public class SqlClientCollector : IDisposable + { + internal const string SqlClientDiagnosticListenerName = "SqlClientDiagnosticListener"; + + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + + /// + /// Initializes a new instance of the class. + /// + /// Tracer to record traced with. + public SqlClientCollector(Tracer tracer) + : this(tracer, new SqlClientCollectorOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Tracer to record traced with. + /// Configuration options for sql collector. + public SqlClientCollector(Tracer tracer, SqlClientCollectorOptions options) + { + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( + name => new SqlClientDiagnosticListener(name, tracer, options), + listener => listener.Name == SqlClientDiagnosticListenerName, + null); + this.diagnosticSourceSubscriber.Subscribe(); + } + + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); + } + } +} diff --git a/src/OpenTelemetry.Collector.Dependencies/SqlClientCollectorOptions.cs b/src/OpenTelemetry.Collector.Dependencies/SqlClientCollectorOptions.cs new file mode 100644 index 0000000000..33f17606b9 --- /dev/null +++ b/src/OpenTelemetry.Collector.Dependencies/SqlClientCollectorOptions.cs @@ -0,0 +1,42 @@ +// +// 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. +// +using System.Data; + +namespace OpenTelemetry.Collector.Dependencies +{ + /// + /// Options for . + /// + public class SqlClientCollectorOptions + { + /// + /// Initializes a new instance of the class. + /// + public SqlClientCollectorOptions() + { + } + + /// + /// Gets or sets a value indicating whether or not the should capture the names of commands. Default value: True. + /// + public bool CaptureStoredProcedureCommandName { get; set; } = true; + + /// + /// Gets or sets a value indicating whether or not the should capture the text of commands. Default value: False. + /// + public bool CaptureTextCommandContent { get; set; } + } +} diff --git a/src/OpenTelemetry.Collector.Dependencies/TracerBuilderExtensions.cs b/src/OpenTelemetry.Collector.Dependencies/TracerBuilderExtensions.cs index 22b498d99d..27e2252529 100644 --- a/src/OpenTelemetry.Collector.Dependencies/TracerBuilderExtensions.cs +++ b/src/OpenTelemetry.Collector.Dependencies/TracerBuilderExtensions.cs @@ -39,34 +39,38 @@ public static TracerBuilder AddDependencyCollector(this TracerBuilder builder) 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)); } /// /// Enables the outgoing requests automatic data collection. /// /// Trace builder to use. - /// Configuration options. + /// Http configuration options. + /// Sql configuration options. /// The instance of to chain the calls. - public static TracerBuilder AddDependencyCollector(this TracerBuilder builder, Action configure) + public static TracerBuilder AddDependencyCollector( + this TracerBuilder builder, + Action configureHttpCollectorOptions = null, + Action 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)); } } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs index a66f58bb09..e2992d66b8 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs @@ -47,7 +47,7 @@ internal static class JaegerConversionExtensions private static readonly Dictionary PeerServiceKeyResolutionDictionary = new Dictionary(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. diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs index 6009f83024..8f69cc9427 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs @@ -30,12 +30,12 @@ internal static class ZipkinConversionExtensions private static readonly Dictionary RemoteEndpointServiceNameKeyResolutionDictionary = new Dictionary(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 LocalEndpointCache = new ConcurrentDictionary(); diff --git a/test/OpenTelemetry.Collector.Dependencies.Tests/BasicTests.cs b/test/OpenTelemetry.Collector.Dependencies.Tests/BasicTests.cs index 90a81b9461..75c5cf279d 100644 --- a/test/OpenTelemetry.Collector.Dependencies.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Collector.Dependencies.Tests/BasicTests.cs @@ -54,7 +54,7 @@ public void AddDependencyCollector_BadArgs() { TracerBuilder builder = null; Assert.Throws(() => builder.AddDependencyCollector()); - Assert.Throws(() => TracerFactory.Create(b => b.AddDependencyCollector(null))); + Assert.Throws(() => builder.AddDependencyCollector(null, null)); } [Fact] @@ -209,7 +209,7 @@ public async Task HttpDependenciesCollectorBacksOffIfAlreadyInstrumented() await c.SendAsync(request); } - Assert.Equal(0, spanProcessor.Invocations.Count); + Assert.Equal(0, spanProcessor.Invocations.Count); } [Fact] diff --git a/test/OpenTelemetry.Collector.Dependencies.Tests/OpenTelemetry.Collector.Dependencies.Tests.csproj b/test/OpenTelemetry.Collector.Dependencies.Tests/OpenTelemetry.Collector.Dependencies.Tests.csproj index 89b13f899f..3bca4a9cb3 100644 --- a/test/OpenTelemetry.Collector.Dependencies.Tests/OpenTelemetry.Collector.Dependencies.Tests.csproj +++ b/test/OpenTelemetry.Collector.Dependencies.Tests/OpenTelemetry.Collector.Dependencies.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/test/OpenTelemetry.Collector.Dependencies.Tests/SqlClientTests.cs b/test/OpenTelemetry.Collector.Dependencies.Tests/SqlClientTests.cs new file mode 100644 index 0000000000..e207b6cb01 --- /dev/null +++ b/test/OpenTelemetry.Collector.Dependencies.Tests/SqlClientTests.cs @@ -0,0 +1,246 @@ +// +// 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. +// + +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(); + 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(); + 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(); + } + } + } +}