From e9a1ff4c86f3b946d1889be0b9e020229cb4091b Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 2 Jul 2019 11:53:34 -0700 Subject: [PATCH] Add configuration to record sanitized sql query for .NET Core (#49) * Add command text to SQL command * Add configuration to allow collection of query text Converts XRayOptions fields to auto-properties Adds InternalsVisibleTo so tests can access internal members * PR feedback * added CollectSqlQueries to configuration * Removed doc references to TraceableSqlCommand to avoid circular referencing * Added tests to testXRayOptions to test for the CollectSqlQueries configuration parameter. * added missing json files for unit tests --- .../Internal/Utils/AppSettings.netcore.cs | 67 +++-- .../Internal/Utils/ConfigurationExtensions.cs | 2 + sdk/src/Core/Properties/AssemblyInfo.cs | 10 +- .../SqlServer/DbCommandInterceptor.cs | 155 ++++++++++++ .../TraceableSqlCommand.netstandard.cs | 111 ++++---- .../AWSXRayRecorder.UnitTests.csproj | 7 + .../UnitTests/DbCommandInterceptorTests.cs | 239 ++++++++++++++++++ .../Appsetting/CollectSqlQueriesFalse.json | 5 + .../Appsetting/CollectSqlQueriesTrue.json | 5 + sdk/test/UnitTests/TestXRayOptions.cs | 30 +++ sdk/test/UnitTests/Tools/DbCommandStub.cs | 62 +++++ 11 files changed, 599 insertions(+), 94 deletions(-) create mode 100644 sdk/src/Handlers/SqlServer/DbCommandInterceptor.cs create mode 100644 sdk/test/UnitTests/DbCommandInterceptorTests.cs create mode 100644 sdk/test/UnitTests/JSONs/Appsetting/CollectSqlQueriesFalse.json create mode 100644 sdk/test/UnitTests/JSONs/Appsetting/CollectSqlQueriesTrue.json create mode 100644 sdk/test/UnitTests/Tools/DbCommandStub.cs diff --git a/sdk/src/Core/Internal/Utils/AppSettings.netcore.cs b/sdk/src/Core/Internal/Utils/AppSettings.netcore.cs index 252c39e1..74d1440f 100644 --- a/sdk/src/Core/Internal/Utils/AppSettings.netcore.cs +++ b/sdk/src/Core/Internal/Utils/AppSettings.netcore.cs @@ -21,12 +21,6 @@ namespace Amazon.XRay.Recorder.Core.Internal.Utils /// public class XRayOptions { - private string _pluginSetting; - private string _samplingRuleManifest; - private string _awsServiceHandlerManifest; - private bool _isXRayTracingDisabled; - private bool _useRuntimeErrors = true; - /// /// Default constructor. /// @@ -43,49 +37,66 @@ public XRayOptions() /// Tracing disabled value, either true or false. public XRayOptions(string pluginSetting, string samplingRuleManifest, string awsServiceHandlerManifest, bool isXRayTracingDisabled) : this(pluginSetting, samplingRuleManifest, awsServiceHandlerManifest, isXRayTracingDisabled, true) - { - } - - /// - /// Creates instance of - /// - /// Plugin setting. - /// Sampling rule file path - /// AWS Service manifest file path. + { + } + + /// + /// Creates instance of + /// + /// Plugin setting. + /// Sampling rule file path + /// AWS Service manifest file path. /// Tracing disabled value, either true or false. /// Should errors be thrown at runtime if segment not started, either true or false. - public XRayOptions(string pluginSetting, string samplingRuleManifest, string awsServiceHandlerManifest, bool isXRayTracingDisabled, bool useRuntimeErrors) + /// + /// Include the TraceableSqlCommand.CommandText in the sanitized_query section of + /// the SQL subsegment. Parameterized values will appear in their tokenized form and will not be expanded. + /// You should not enable this flag if you are including sensitive information as clear text. + /// This flag can also be overridden for each TraceableSqlCommand instance individually. + /// See the official documentation on SqlCommand.Parameters + /// + public XRayOptions(string pluginSetting, string samplingRuleManifest, string awsServiceHandlerManifest, bool isXRayTracingDisabled, bool useRuntimeErrors, bool collectSqlQueries = false) { PluginSetting = pluginSetting; SamplingRuleManifest = samplingRuleManifest; AwsServiceHandlerManifest = awsServiceHandlerManifest; IsXRayTracingDisabled = isXRayTracingDisabled; UseRuntimeErrors = useRuntimeErrors; + CollectSqlQueries = collectSqlQueries; } - /// - /// Plugin setting. + /// + /// Plugin setting. /// - public string PluginSetting { get => _pluginSetting; set => _pluginSetting = value; } + public string PluginSetting { get; set; } - /// - /// Sampling rule file path. + /// + /// Sampling rule file path. /// - public string SamplingRuleManifest { get => _samplingRuleManifest; set => _samplingRuleManifest = value; } + public string SamplingRuleManifest { get; set; } - /// - /// AWS Service manifest file path. + /// + /// AWS Service manifest file path. /// - public string AwsServiceHandlerManifest { get => _awsServiceHandlerManifest; set => _awsServiceHandlerManifest = value; } - + public string AwsServiceHandlerManifest { get; set; } + /// /// Tracing disabled value, either true or false. /// - public bool IsXRayTracingDisabled { get => _isXRayTracingDisabled; set => _isXRayTracingDisabled = value; } + public bool IsXRayTracingDisabled { get; set; } /// /// For missing Segments/Subsegments, if set to true, runtime exception is thrown, if set to false, runtime exceptions are avoided and logged. /// - public bool UseRuntimeErrors { get => _useRuntimeErrors; set => _useRuntimeErrors = value; } + public bool UseRuntimeErrors { get; set; } = true; + + /// + /// Include the TraceableSqlCommand.CommandText in the sanitized_query section of + /// the SQL subsegment. Parameterized values will appear in their tokenized form and will not be expanded. + /// You should not enable this flag if you are not including sensitive information as clear text. + /// When set to true, the sanitized sql query will be recorded for all the instances of TraceableSqlCommand + /// in the application, unless it is overridden on the individual TraceableSqlCommand instances. + /// + public bool CollectSqlQueries { get; set; } = false; } } diff --git a/sdk/src/Core/Internal/Utils/ConfigurationExtensions.cs b/sdk/src/Core/Internal/Utils/ConfigurationExtensions.cs index 34b62c9d..cd48cfaa 100644 --- a/sdk/src/Core/Internal/Utils/ConfigurationExtensions.cs +++ b/sdk/src/Core/Internal/Utils/ConfigurationExtensions.cs @@ -31,6 +31,7 @@ public static class XRayConfiguration private const string AWSServiceHandlerManifestKey = "AWSServiceHandlerManifest"; private const string DisableXRayTracingKey = "DisableXRayTracing"; private const string UseRuntimeErrorsKey = "UseRuntimeErrors"; + private const string CollectSqlQueries = "CollectSqlQueries"; /// /// Reads configuration from object for X-Ray. @@ -63,6 +64,7 @@ private static XRayOptions GetXRayOptions(IConfiguration config, string configSe options.AwsServiceHandlerManifest =GetSetting(AWSServiceHandlerManifestKey, section); options.IsXRayTracingDisabled = GetSettingBool(DisableXRayTracingKey,section); options.UseRuntimeErrors = GetSettingBool(UseRuntimeErrorsKey, section, defaultValue: true); + options.CollectSqlQueries = GetSettingBool(CollectSqlQueries, section, defaultValue: false); return options; } diff --git a/sdk/src/Core/Properties/AssemblyInfo.cs b/sdk/src/Core/Properties/AssemblyInfo.cs index 81b536c9..92fef342 100644 --- a/sdk/src/Core/Properties/AssemblyInfo.cs +++ b/sdk/src/Core/Properties/AssemblyInfo.cs @@ -1,5 +1,13 @@ -using System; +using System; using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; [assembly: ComVisible(false)] [assembly: CLSCompliant(true)] +[assembly: InternalsVisibleTo("AWSXRayRecorder.UnitTests,PublicKey="+ +"0024000004800000940000000602000000240000525341310004000001000100712913451f6deb" ++ "158da1d2129b21119cca7d4eebeef5b310e8acd7f2d9506346071207652f1210a3bfa1545d6897" ++ "a607fc3a515954e660ec6fc5797730022867514e58411e8ecd61c767a319d2c29facee20f5d4f4" ++ "2b5425f27518616a8f4c1e5ac0e3e2b407bd8786d1b360af6b49c2b987478fe76b124c72f48864" ++ "55199df6" +)] \ No newline at end of file diff --git a/sdk/src/Handlers/SqlServer/DbCommandInterceptor.cs b/sdk/src/Handlers/SqlServer/DbCommandInterceptor.cs new file mode 100644 index 00000000..7de13a7a --- /dev/null +++ b/sdk/src/Handlers/SqlServer/DbCommandInterceptor.cs @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------------- +// +// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file 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.Data.Common; +using System.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Amazon.XRay.Recorder.Core; + +namespace Amazon.XRay.Recorder.Handlers.SqlServer +{ + /// + /// Intercepts DbCommands and records them in new Subsegments. + /// + public interface IDbCommandInterceptor + { + /// + /// Begins a new Subsegment, executes the provided async operation, + /// and records the request in the "sql" member of the subsegment. + /// + /// + /// + /// await InterceptAsync(() => dbCommand.ExecuteNonQueryAsync(cancellationToken), dbCommand); + /// + /// + Task InterceptAsync(Func> method, DbCommand command); + + /// + /// Begins a new Subsegment, executes the provided operation, + /// and records the request in the "sql" member of the subsegment. + /// + /// + /// + /// await Intercept(() => dbCommand.ExecuteNonQuery(), dbCommand); + /// + /// + TResult Intercept(Func method, DbCommand command); + } + + /// + public class DbCommandInterceptor : IDbCommandInterceptor + { + private const string DataBaseTypeString = "sqlserver"; + private readonly AWSXRayRecorder _recorder; + private readonly bool? _collectSqlQueriesOverride; + + public DbCommandInterceptor(AWSXRayRecorder recorder, bool? collectSqlQueries = null) + { + _recorder = recorder; + _collectSqlQueriesOverride = collectSqlQueries; + } + + /// + public async Task InterceptAsync(Func> method, DbCommand command) + { + _recorder.BeginSubsegment(BuildSubsegmentName(command)); + try + { + _recorder.SetNamespace("remote"); + var ret = await method(); + CollectSqlInformation(command); + + return ret; + } + catch (Exception e) + { + _recorder.AddException(e); + throw; + } + finally + { + _recorder.EndSubsegment(); + } + } + + /// + public TResult Intercept(Func method, DbCommand command) + { + _recorder.BeginSubsegment(BuildSubsegmentName(command)); + try + { + _recorder.SetNamespace("remote"); + var ret = method(); + CollectSqlInformation(command); + + return ret; + } + catch (Exception e) + { + _recorder.AddException(e); + throw; + } + finally + { + _recorder.EndSubsegment(); + } + } + + /// + /// Records the SQL information on the current subsegment, + /// + protected virtual void CollectSqlInformation(DbCommand command) + { + _recorder.AddSqlInformation("database_type", DataBaseTypeString); + + _recorder.AddSqlInformation("database_version", command.Connection.ServerVersion); + + SqlConnectionStringBuilder connectionStringBuilder = new SqlConnectionStringBuilder(command.Connection.ConnectionString); + + // Remove sensitive information from connection string + connectionStringBuilder.Remove("Password"); + + _recorder.AddSqlInformation("user", connectionStringBuilder.UserID); + _recorder.AddSqlInformation("connection_string", connectionStringBuilder.ToString()); + + if(ShouldCollectSqlText()) + { + _recorder.AddSqlInformation("sanitized_query", command.CommandText); + } + } + + /// + /// Builds the name of the subsegment in the format database@datasource + /// + /// + /// Returns the formed subsegment name as a string. + private string BuildSubsegmentName(DbCommand command) + => command.Connection.Database + "@" + SqlUtil.RemovePortNumberFromDataSource(command.Connection.DataSource); + +#if !NET45 + private bool ShouldCollectSqlText() + => _collectSqlQueriesOverride ?? _recorder.XRayOptions.CollectSqlQueries; +#else + private bool ShouldCollectSqlText() + => _collectSqlQueriesOverride ?? false; +#endif + } +} \ No newline at end of file diff --git a/sdk/src/Handlers/SqlServer/TraceableSqlCommand.netstandard.cs b/sdk/src/Handlers/SqlServer/TraceableSqlCommand.netstandard.cs index d1dc6485..02bc4ab3 100644 --- a/sdk/src/Handlers/SqlServer/TraceableSqlCommand.netstandard.cs +++ b/sdk/src/Handlers/SqlServer/TraceableSqlCommand.netstandard.cs @@ -23,6 +23,7 @@ using System.Threading.Tasks; using System.Xml; using Amazon.XRay.Recorder.Core; +using Amazon.XRay.Recorder.Core.Internal.Utils; namespace Amazon.XRay.Recorder.Handlers.SqlServer { @@ -33,23 +34,41 @@ namespace Amazon.XRay.Recorder.Handlers.SqlServer /// public class TraceableSqlCommand : DbCommand, ICloneable { - private const string DataBaseTypeString = "sqlserver"; + private IDbCommandInterceptor _interceptor { get; set; } /// /// Initializes a new instance of the class. /// - public TraceableSqlCommand() + /// + /// Include the in the sanitized_query section of + /// the SQL subsegment. Parameterized values will appear in their tokenized form and will not be expanded. + /// You should not enable this flag if you are not including sensitive information as clear text. + /// This flag will overridde any behavior configured by . + /// If a value is not provided, then the globally configured value will be used, which is false by default. + /// See the official documentation on SqlCommand.Parameters + /// + public TraceableSqlCommand(bool? collectSqlQueries = null) { InnerSqlCommand = new SqlCommand(); + _interceptor = new DbCommandInterceptor(AWSXRayRecorder.Instance, collectSqlQueries); } /// /// Initializes a new instance of the class. /// /// The text of the query. - public TraceableSqlCommand(string cmdText) + /// + /// Include the in the sanitized_query section of + /// the SQL subsegment. Parameterized values will appear in their tokenized form and will not be expanded. + /// You should not enable this flag if you are not including sensitive information as clear text. + /// This flag will overridde any behavior configured by . + /// If a value is not provided, then the globally configured value will be used, which is false by default. + /// See the official documentation on SqlCommand.Parameters + /// + public TraceableSqlCommand(string cmdText, bool? collectSqlQueries = null) { InnerSqlCommand = new SqlCommand(cmdText); + _interceptor = new DbCommandInterceptor(AWSXRayRecorder.Instance, collectSqlQueries); } /// @@ -57,9 +76,18 @@ public TraceableSqlCommand(string cmdText) /// /// The text of the query. /// The connection to an instance of SQL Server. - public TraceableSqlCommand(string cmdText, SqlConnection connection) + /// + /// Include the in the sanitized_query section of + /// the SQL subsegment. Parameterized values will appear in their tokenized form and will not be expanded. + /// You should not enable this flag if you are not including sensitive information as clear text. + /// This flag will overridde any behavior configured by . + /// If a value is not provided, then the globally configured value will be used, which is false by default. + /// See the official documentation on SqlCommand.Parameters + /// + public TraceableSqlCommand(string cmdText, SqlConnection connection, bool? collectSqlQueries = null) { InnerSqlCommand = new SqlCommand(cmdText, connection); + _interceptor = new DbCommandInterceptor(AWSXRayRecorder.Instance, collectSqlQueries); } /// @@ -68,9 +96,18 @@ public TraceableSqlCommand(string cmdText, SqlConnection connection) /// The text of the query. /// The connection to an instance of SQL Server. /// The in which the executes. - public TraceableSqlCommand(string cmdText, SqlConnection connection, SqlTransaction transaction) + /// + /// Include the in the sanitized_query section of + /// the SQL subsegment. Parameterized values will appear in their tokenized form and will not be expanded. + /// You should not enable this flag if you are not including sensitive information as clear text. + /// This flag will overridde any behavior configured by . + /// If a value is not provided, then the globally configured value will be used, which is false by default. + /// See the official documentation on SqlCommand.Parameters + /// + public TraceableSqlCommand(string cmdText, SqlConnection connection, SqlTransaction transaction, bool? collectSqlQueries = null) { InnerSqlCommand = new SqlCommand(cmdText, connection, transaction); + _interceptor = new DbCommandInterceptor(AWSXRayRecorder.Instance, collectSqlQueries); } private TraceableSqlCommand(TraceableSqlCommand from) @@ -394,66 +431,10 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) return Intercept(() => InnerSqlCommand.ExecuteReader(behavior)); } - private async Task InterceptAsync(Func> method) - { - AWSXRayRecorder recorder = AWSXRayRecorder.Instance; - recorder.BeginSubsegment(Connection.Database + "@" + SqlUtil.RemovePortNumberFromDataSource(Connection.DataSource)); - try - { - recorder.SetNamespace("remote"); - var ret = await method(); - CollectSqlInformation(); - - return ret; - } - catch (Exception e) - { - recorder.AddException(e); - throw; - } - finally - { - recorder.EndSubsegment(); - } - } - - private TResult Intercept(Func method) - { - AWSXRayRecorder recorder = AWSXRayRecorder.Instance; - recorder.BeginSubsegment(Connection.Database + "@" + SqlUtil.RemovePortNumberFromDataSource(Connection.DataSource)); - try - { - recorder.SetNamespace("remote"); - var ret = method(); - CollectSqlInformation(); - - return ret; - } - catch (Exception e) - { - recorder.AddException(e); - throw; - } - finally - { - recorder.EndSubsegment(); - } - } + protected virtual async Task InterceptAsync(Func> method) + => await _interceptor.InterceptAsync(method, this); - private void CollectSqlInformation() - { - AWSXRayRecorder recorder = AWSXRayRecorder.Instance; - recorder.AddSqlInformation("database_type", DataBaseTypeString); - - recorder.AddSqlInformation("database_version", Connection.ServerVersion); - - SqlConnectionStringBuilder connectionStringBuilder = new SqlConnectionStringBuilder(Connection.ConnectionString); - - // Remove sensitive information from connection string - connectionStringBuilder.Remove("Password"); - - recorder.AddSqlInformation("user", connectionStringBuilder.UserID); - recorder.AddSqlInformation("connection_string", connectionStringBuilder.ToString()); - } + protected virtual TResult Intercept(Func method) + => _interceptor.Intercept(method, this); } } diff --git a/sdk/test/UnitTests/AWSXRayRecorder.UnitTests.csproj b/sdk/test/UnitTests/AWSXRayRecorder.UnitTests.csproj index 1967d7e2..c974bca6 100644 --- a/sdk/test/UnitTests/AWSXRayRecorder.UnitTests.csproj +++ b/sdk/test/UnitTests/AWSXRayRecorder.UnitTests.csproj @@ -79,6 +79,7 @@ + @@ -86,6 +87,12 @@ + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/sdk/test/UnitTests/DbCommandInterceptorTests.cs b/sdk/test/UnitTests/DbCommandInterceptorTests.cs new file mode 100644 index 00000000..6d266d58 --- /dev/null +++ b/sdk/test/UnitTests/DbCommandInterceptorTests.cs @@ -0,0 +1,239 @@ +//----------------------------------------------------------------------------- +// +// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file 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.Threading.Tasks; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using Amazon.XRay.Recorder.Handlers.SqlServer; +using Amazon.XRay.Recorder.Core.Internal.Utils; +using Amazon.XRay.Recorder.Core.Internal.Entities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Amazon.XRay.Recorder.Core; +using Amazon.XRay.Recorder.UnitTests.Tools; + +namespace Amazon.XRay.Recorder.UnitTests +{ + [TestClass] + public class DbCommandInterceptorTests : TestBase + { + private DbCommandStub _command = new DbCommandStub(); + private const string _userId = "admin"; + private const string _connectionString = "Data Source=xyz.com,3306;User ID=" + _userId + ";Password=Secret.123;"; + private const string _sanitizedConnectionString = "Data Source=xyz.com,3306;User ID=" + _userId; + + [TestInitialize] + public void TestInitialize() + { + var connectionMock = new Mock(); + connectionMock.Setup(c => c.DataSource).Returns("xyz.com,3306"); + connectionMock.Setup(c => c.Database).Returns("master"); + connectionMock.Setup(c => c.ServerVersion).Returns("13.0.5026.0"); + connectionMock.Setup(c => c.ConnectionString).Returns(_connectionString); + + _command.Connection = connectionMock.Object; + _command.CommandText = "SELECT a.* FROM dbo.Accounts a ..."; + } + + [TestCleanup] + public new void TestCleanup() + { + base.TestCleanup(); + AWSXRayRecorder.Instance.Dispose(); + } + + [TestMethod] + public void Intercept_DoesNot_CollectQueries_When_NotEnabled() + { + // arrange + var recorder = new AWSXRayRecorder { + XRayOptions = new XRayOptions() + }; + recorder.BeginSegment("test"); + var interceptor = new DbCommandInterceptor(recorder); + + // act + interceptor.Intercept(() => 0, _command); + + // assert + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + + AssertNotCollected(recorder); + recorder.EndSegment(); + } + + [TestMethod] + public async Task InterceptAsync_DoesNot_CollectQueries_When_NotEnabled() + { + // arrange + var recorder = new AWSXRayRecorder { + XRayOptions = new XRayOptions() + }; + recorder.BeginSegment("test"); + var interceptor = new DbCommandInterceptor(recorder); + + // act + await interceptor.InterceptAsync(() => Task.FromResult(0), _command); + + // assert + var segment = AWSXRayRecorder.Instance.TraceContext.GetEntity(); + + AssertNotCollected(recorder); + recorder.EndSegment(); + } + + [TestMethod] + public void Intercept_CollectsQueries_When_DisabledGlobally_And_EnabledLocally() + { + // arrange + var recorder = new AWSXRayRecorder { + XRayOptions = new XRayOptions { CollectSqlQueries = false } + }; + recorder.BeginSegment("test"); + var interceptor = new DbCommandInterceptor(recorder, collectSqlQueries: true); + + // act + interceptor.Intercept(() => 0, _command); + + // assert + AssertCollected(recorder); + recorder.EndSegment(); + } + + [TestMethod] + public async Task InterceptAsync_CollectsQueries_When_DisabledGlobally_And_EnabledLocally() + { + // arrange + var recorder = new AWSXRayRecorder { + XRayOptions = new XRayOptions { CollectSqlQueries = false } + }; + recorder.BeginSegment("test"); + var interceptor = new DbCommandInterceptor(recorder, collectSqlQueries: true); + + // act + await interceptor.InterceptAsync(() => Task.FromResult(0), _command); + + // assert + AssertCollected(recorder); + recorder.EndSegment(); + } + + [TestMethod] + public void Intercept_CollectsQueries_When_EnabledGlobally() + { + // arrange + var recorder = new AWSXRayRecorder { + XRayOptions = new XRayOptions { CollectSqlQueries = true } + }; + var interceptor = new DbCommandInterceptor(recorder); + recorder.BeginSegment("test"); + + // act + interceptor.Intercept(() => 0, _command); + + // assert + AssertCollected(recorder); + recorder.EndSegment(); + } + + [TestMethod] + public async Task InterceptAsync_CollectsQueries_When_EnabledGlobally() + { + // arrange + var recorder = new AWSXRayRecorder { + XRayOptions = new XRayOptions { CollectSqlQueries = true } + }; + var interceptor = new DbCommandInterceptor(recorder); + recorder.BeginSegment("test"); + + // act + await interceptor.InterceptAsync(() => Task.FromResult(0), _command); + + // assert + AssertCollected(recorder); + recorder.EndSegment(); + } + + [TestMethod] + public void Intercept_DoesNot_CollectQueries_When_EnabledGlobally_And_DisabledLocally() + { + // arrange + var recorder = new AWSXRayRecorder { + XRayOptions = new XRayOptions { CollectSqlQueries = true } + }; + var interceptor = new DbCommandInterceptor(recorder, collectSqlQueries: false); + recorder.BeginSegment("test"); + + // act + interceptor.Intercept(() => 0, _command); + + // assert + AssertNotCollected(recorder); + recorder.EndSegment(); + } + + [TestMethod] + public async Task InterceptAsync_DoesNot_CollectQueries_When_EnabledGlobally_And_DisabledLocally() + { + // arrange + var recorder = new AWSXRayRecorder { + XRayOptions = new XRayOptions { CollectSqlQueries = true } + }; + var interceptor = new DbCommandInterceptor(recorder, collectSqlQueries: false); + recorder.BeginSegment("test"); + + // act + await interceptor.InterceptAsync(() => Task.FromResult(0), _command); + + // assert + AssertNotCollected(recorder); + recorder.EndSegment(); + } + + private void AssertNotCollected(AWSXRayRecorder recorder) + { + var segment = recorder.TraceContext.GetEntity().Subsegments[0]; + + AssertExpectedSqlInformation(segment); + + Assert.AreEqual(4, segment.Sql.Count); + Assert.IsFalse(segment.Sql.ContainsKey("sanitized_query")); + } + + private void AssertCollected(AWSXRayRecorder recorder) + { + var segment = recorder.TraceContext.GetEntity().Subsegments[0]; + + AssertExpectedSqlInformation(segment); + + Assert.AreEqual(5, segment.Sql.Count); + Assert.AreEqual(_command.CommandText, segment.Sql["sanitized_query"]); + } + + private void AssertExpectedSqlInformation(Subsegment segment) + { + Assert.IsNotNull(segment); + Assert.IsNotNull(segment.Sql); + Assert.AreEqual("sqlserver", segment.Sql["database_type"]); + Assert.AreEqual(_command.Connection.ServerVersion, segment.Sql["database_version"]); + Assert.AreEqual(_userId, segment.Sql["user"]); + Assert.AreEqual(_sanitizedConnectionString, segment.Sql["connection_string"]); + } + } +} \ No newline at end of file diff --git a/sdk/test/UnitTests/JSONs/Appsetting/CollectSqlQueriesFalse.json b/sdk/test/UnitTests/JSONs/Appsetting/CollectSqlQueriesFalse.json new file mode 100644 index 00000000..98b6fac2 --- /dev/null +++ b/sdk/test/UnitTests/JSONs/Appsetting/CollectSqlQueriesFalse.json @@ -0,0 +1,5 @@ +{ + "XRay": { + "CollectSqlQueries": "false" + } +} \ No newline at end of file diff --git a/sdk/test/UnitTests/JSONs/Appsetting/CollectSqlQueriesTrue.json b/sdk/test/UnitTests/JSONs/Appsetting/CollectSqlQueriesTrue.json new file mode 100644 index 00000000..850cf8d6 --- /dev/null +++ b/sdk/test/UnitTests/JSONs/Appsetting/CollectSqlQueriesTrue.json @@ -0,0 +1,5 @@ +{ + "XRay": { + "CollectSqlQueries": "true" + } +} \ No newline at end of file diff --git a/sdk/test/UnitTests/TestXRayOptions.cs b/sdk/test/UnitTests/TestXRayOptions.cs index 84bb7a28..aa3db53a 100644 --- a/sdk/test/UnitTests/TestXRayOptions.cs +++ b/sdk/test/UnitTests/TestXRayOptions.cs @@ -213,6 +213,36 @@ public void TestUseRuntimeErrorsDefaultsTrue_WhenNotSpecifiedInJson() Assert.AreEqual(AWSXRayRecorder.Instance.ContextMissingStrategy, Core.Strategies.ContextMissingStrategy.RUNTIME_ERROR); } + [TestMethod] + public void TestCollectSqlQueriesFalse_WhenNotSpecifiedInJson() + { + IConfiguration configuration = BuildConfiguration("DisabledXRayMissing.json"); + _xRayOptions = XRayConfiguration.GetXRayOptions(configuration); + AWSXRayRecorder.InitializeInstance(configuration); + + Assert.IsFalse(_xRayOptions.CollectSqlQueries); + } + + [TestMethod] + public void TestCollecSqlQueriesFalse() + { + IConfiguration configuration = BuildConfiguration("CollectSqlQueriesFalse.json"); + _xRayOptions = XRayConfiguration.GetXRayOptions(configuration); + AWSXRayRecorder.InitializeInstance(configuration); + + Assert.IsFalse(_xRayOptions.CollectSqlQueries); + } + + [TestMethod] + public void TestCollecSqlQueriesTrue() + { + IConfiguration configuration = BuildConfiguration("CollectSqlQueriesTrue.json"); + _xRayOptions = XRayConfiguration.GetXRayOptions(configuration); + AWSXRayRecorder.InitializeInstance(configuration); + + Assert.IsTrue(_xRayOptions.CollectSqlQueries); + } + // Creating custom configuration private IConfiguration BuildConfiguration(string path) { diff --git a/sdk/test/UnitTests/Tools/DbCommandStub.cs b/sdk/test/UnitTests/Tools/DbCommandStub.cs new file mode 100644 index 00000000..eb0ea8d1 --- /dev/null +++ b/sdk/test/UnitTests/Tools/DbCommandStub.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------------- +// +// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file 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.Data.Common; +using System.Data.SqlClient; + +// This class exists because DbCommand has non-abstract members +// that cannot be mocked, but need to be used. A specific example is +// DbCommand.Connection. This property needs to be used, but the getters +// and setters cannot be mocked. Instead, this class inherits from DbCommand +// allowing the base implementations to be invoked. +// REF: https://github.com/dotnet/corefx/blob/84bedcf58cfe951d37c5b3eba2957ddb2410f34d/src/System.Data.Common/src/System/Data/Common/DbCommand.cs#L30 +namespace Amazon.XRay.Recorder.UnitTests.Tools +{ + public class DbCommandStub : DbCommand + { + private object _scalarResult = null; + private int _nonQueryResult = 0; + private DbDataReader _dbDataReader = null; + private DbParameter _createDbParameterResult = null; + + // setup methods + public void WithScalarResult(object _) => _scalarResult = _; + public void WithNonQueryResult(int _) => _nonQueryResult = _; + public void WithDbReader(DbDataReader _) => _dbDataReader = _; + public void WithCreateDbParameterResult(DbParameter _) => _createDbParameterResult = _; + + // stubbed methods + public override string CommandText { get; set; } + public override int CommandTimeout { get; set; } + public override CommandType CommandType { get; set; } + protected override DbConnection DbConnection { get; set; } + protected override DbParameterCollection DbParameterCollection { get; } + protected override DbTransaction DbTransaction { get; set; } + public override bool DesignTimeVisible { get; set; } + public override UpdateRowSource UpdatedRowSource { get; set; } + public override void Cancel() { } + protected override DbParameter CreateDbParameter() => _createDbParameterResult; + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + => _dbDataReader; + + public override int ExecuteNonQuery() => _nonQueryResult; + public override object ExecuteScalar() => _scalarResult; + public override void Prepare() { } + } +} \ No newline at end of file