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();
+ }
+ }
+ }
+}