// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // using System; using System.Data; using System.Diagnostics; using System.Linq; #if NET452 using System.Data.SqlClient; #else using Microsoft.Data.SqlClient; #endif using Moq; using OpenTelemetry.Instrumentation.SqlClient.Implementation; using OpenTelemetry.Internal.Test; using OpenTelemetry.Trace; using Xunit; namespace OpenTelemetry.Instrumentation.SqlClient.Tests { public class SqlClientTests : IDisposable { /* To run the integration tests, set the OTEL_SQLCONNECTIONSTRING machine-level environment variable to a valid Sql Server connection string. To use Docker... 1) Run: docker run -d --name sql2019 -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@word" -p 5433:1433 mcr.microsoft.com/mssql/server:2019-latest 2) Set OTEL_SQLCONNECTIONSTRING as: Data Source=127.0.0.1,5433; User ID=sa; Password=Pass@word */ private const string SqlConnectionStringEnvVarName = "OTEL_SQLCONNECTIONSTRING"; private const string TestConnectionString = "Data Source=(localdb)\\MSSQLLocalDB;Database=master"; private static readonly string SqlConnectionString = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(SqlConnectionStringEnvVarName); private readonly FakeSqlClientDiagnosticSource fakeSqlClientDiagnosticSource; public SqlClientTests() { this.fakeSqlClientDiagnosticSource = new FakeSqlClientDiagnosticSource(); } public void Dispose() { this.fakeSqlClientDiagnosticSource.Dispose(); } [Fact] public void SqlClient_BadArgs() { TracerProviderBuilder builder = null; Assert.Throws(() => builder.AddSqlClientInstrumentation()); } [Trait("CategoryName", "SqlIntegrationTests")] [SkipUnlessEnvVarFoundTheory(SqlConnectionStringEnvVarName)] [InlineData(CommandType.Text, "select 1/1", false)] #if !NETFRAMEWORK [InlineData(CommandType.Text, "select 1/1", false, true)] #endif [InlineData(CommandType.Text, "select 1/0", false, false, true)] [InlineData(CommandType.StoredProcedure, "sp_who", false)] [InlineData(CommandType.StoredProcedure, "sp_who", true)] public void SuccessfulCommandTest( CommandType commandType, string commandText, bool captureStoredProcedureCommandName, bool captureTextCommandContent = false, bool isFailure = false) { var activityProcessor = new Mock(); using var shutdownSignal = Sdk.CreateTracerProvider(b => { b.AddProcessorPipeline(c => c.AddProcessor(ap => activityProcessor.Object)); b.AddSqlClientInstrumentation(options => { options.SetStoredProcedureCommandName = captureStoredProcedureCommandName; options.SetTextCommandContent = captureTextCommandContent; }); }); using SqlConnection sqlConnection = new SqlConnection(SqlConnectionString); sqlConnection.Open(); string dataSource = sqlConnection.DataSource; sqlConnection.ChangeDatabase("master"); using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) { CommandType = commandType, }; try { sqlCommand.ExecuteNonQuery(); } catch { } Assert.Equal(2, activityProcessor.Invocations.Count); var activity = (Activity)activityProcessor.Invocations[1].Arguments[0]; VerifyActivityData(commandType, commandText, captureStoredProcedureCommandName, captureTextCommandContent, isFailure, dataSource, activity); } [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) { using var sqlConnection = new SqlConnection(TestConnectionString); using var sqlCommand = sqlConnection.CreateCommand(); var spanProcessor = new Mock(); using (Sdk.CreateTracerProvider( (builder) => builder.AddSqlClientInstrumentation( (opt) => { opt.SetTextCommandContent = captureTextCommandContent; opt.SetStoredProcedureCommandName = captureStoredProcedureCommandName; }) .AddProcessorPipeline(p => p.AddProcessor(n => spanProcessor.Object)))) { var operationId = Guid.NewGuid(); 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 and end was called VerifyActivityData(sqlCommand.CommandType, sqlCommand.CommandText, captureStoredProcedureCommandName, captureTextCommandContent, false, sqlConnection.DataSource, (Activity)spanProcessor.Invocations[1].Arguments[0]); } [Theory] [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError)] [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError)] public void SqlClientErrorsAreCollectedSuccessfully(string beforeCommand, string errorCommand) { using var sqlConnection = new SqlConnection(TestConnectionString); using var sqlCommand = sqlConnection.CreateCommand(); var spanProcessor = new Mock(); using (Sdk.CreateTracerProvider( (builder) => builder.AddSqlClientInstrumentation() .AddProcessorPipeline(p => p.AddProcessor(n => spanProcessor.Object)))) { var operationId = Guid.NewGuid(); 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(2, spanProcessor.Invocations.Count); // begin and end was called VerifyActivityData(sqlCommand.CommandType, sqlCommand.CommandText, true, false, true, sqlConnection.DataSource, (Activity)spanProcessor.Invocations[1].Arguments[0]); } private static void VerifyActivityData( CommandType commandType, string commandText, bool captureStoredProcedureCommandName, bool captureTextCommandContent, bool isFailure, string dataSource, Activity activity) { Assert.Equal("master", activity.DisplayName); Assert.Equal(ActivityKind.Client, activity.Kind); if (!isFailure) { Assert.Equal("Ok", activity.Tags.FirstOrDefault(i => i.Key == SpanAttributeConstants.StatusCodeKey).Value); Assert.Null(activity.Tags.FirstOrDefault(i => i.Key == SpanAttributeConstants.StatusDescriptionKey).Value); } else { Assert.Equal("Unknown", activity.Tags.FirstOrDefault(i => i.Key == SpanAttributeConstants.StatusCodeKey).Value); Assert.Contains(activity.Tags, i => i.Key == SpanAttributeConstants.StatusDescriptionKey); } Assert.Equal(SqlClientDiagnosticListener.MicrosoftSqlServerDatabaseSystemName, activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeDbSystem).Value); Assert.Equal("master", activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeDbName).Value); switch (commandType) { case CommandType.StoredProcedure: if (captureStoredProcedureCommandName) { Assert.Equal(commandText, activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeDbStatement).Value); } else { Assert.Null(activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeDbStatement).Value); } break; case CommandType.Text: if (captureTextCommandContent) { Assert.Equal(commandText, activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeDbStatement).Value); } else { Assert.Null(activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeDbStatement).Value); } break; } Assert.Equal(dataSource, activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributePeerService).Value); } private class FakeSqlClientDiagnosticSource : IDisposable { private readonly DiagnosticListener listener; public FakeSqlClientDiagnosticSource() { this.listener = new DiagnosticListener(SqlClientInstrumentation.SqlClientDiagnosticListenerName); } public void Write(string name, object value) { this.listener.Write(name, value); } public void Dispose() { this.listener.Dispose(); } } } }