diff --git a/README.md b/README.md index 66c037bc..01c7d389 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Following API reference documentation provides guidance for using the SDK and mo 4. [Trace AWS SDK request](https://github.com/aws/aws-xray-sdk-dotnet/tree/master#trace-aws-sdk-request-net-and-net-core--nuget) 5. [Trace out-going HTTP requests](https://github.com/aws/aws-xray-sdk-dotnet/tree/master#trace-out-going-http-requests-net-and-net-core--nuget) 6. [Trace Query to SQL Server](https://github.com/aws/aws-xray-sdk-dotnet/tree/master#trace-query-to-sql-server-net-and-net-core--nuget) -7. [Trace SQL Query through Entity Framework Core 3.0 and above](https://github.com/aws/aws-xray-sdk-dotnet/tree/master#trace-sql-query-through-entity-framework-core-30-and-above-net-core--nuget) +7. [Trace SQL Query through Entity Framework](https://github.com/aws/aws-xray-sdk-dotnet/tree/master#trace-sql-query-through-entity-framework-net-and-net-core--nuget) 8. [Multithreaded Execution](https://github.com/aws/aws-xray-sdk-dotnet/tree/master#multithreaded-execution-net-and-net-core--nuget) 9. [Trace custom methods ](https://github.com/aws/aws-xray-sdk-dotnet/tree/master#trace-custom-methods-net-and-net-core) 10. [Creating custom Segment/Subsegment](https://github.com/aws/aws-xray-sdk-dotnet/tree/master#creating-custom-segmentsubsegment-net-and-net-core) @@ -384,7 +384,11 @@ using (var command = new TraceableSqlCommand("SELECT * FROM products", connectio 2. Parameterized values will appear in their tokenized form and will not be expanded. 3. The value of `collectSqlQueries` in the `TraceableSqlCommand` instance overrides the value set in the global configuration using the `CollectSqlQueries` property. -### Trace SQL Query through Entity Framework Core 3.0 and above (.NET Core) : [Nuget](https://www.nuget.org/packages/AWSXRayRecorder.Handlers.EntityFramework/) +### Trace SQL Query through Entity Framework (.NET and .NET Core) : [Nuget](https://www.nuget.org/packages/AWSXRayRecorder.Handlers.EntityFramework/) + +#### Setup + +##### .NET Core AWS XRay SDK for .NET Core provides interceptor for tracing SQL query through Entity Framework Core (>=3.0). @@ -397,8 +401,6 @@ For how to start with Entity Framework Core in an ASP.NET Core web app, please t *Known Limitation (as of 12-03-2020):* If you're using another `DbCommandInterceptor` implementation along with the `AddXRayInterceptor` in the `DbContext`, it may not work as expected and you may see a "EntityNotAvailableException" from the XRay EFCore interceptor. This is due to [`AsyncLocal`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1?view=netcore-2.0) not being able to maintain context across the `ReaderExecutingAsync` and `ReaderExecutedAsync` methods. Ref [here](https://github.com/dotnet/efcore/issues/22766) for more details on the issue. -#### Setup - In order to trace SQL query, you can register your `DbContext` with `AddXRayInterceptor()` accordingly in the `ConfigureServices` method in `startup.cs` file. For instance, when dealing with MySql server using Nuget: [Pomelo.EntityFrameworkCore.MySql](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql) (V 3.1.1). @@ -426,10 +428,45 @@ public class your_DbContext : DbContext The connection string can be either hard coded or configured from `appsettings.json` file. +##### .NET + +AWS XRay SDK for .NET provides interceptor for tracing SQL query through Entity Framework 6 (>= 6.2.0). + +For how to start with Entity Framework 6 in an ASP.NET web app, please take reference to [link](https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application). + +For instrumentation, you will need to install `AWSXRayRecorder.Handlers.EntityFramework` nuget package and call `AWSXRayEntityFramework6.AddXRayInterceptor()` in your code. Make sure to call it **only once** to avoid duplicate tracing. + +For instance, you can call `AddXRayInterceptor()` in the `Application_Start` method of **Global.asax** file. + +``` +using Amazon.XRay.Recorder.Handlers.EntityFramework; + +protected void Application_Start() +{ + AWSXRayEntityFramework6.AddXRayInterceptor(); +} +``` + +Or you can call it in the `DbConfiguration` class if there is one in your application to configure execution policy. + +``` +using Amazon.XRay.Recorder.Handlers.EntityFramework; + +public class YourDbConfiguration : DbConfiguration +{ + public YourDbConfiguration() + { + AWSXRayEntityFramework6.AddXRayInterceptor(); + } +} +``` + #### Capture SQL Query text in the traced SQL calls to SQL Server You can also opt in to capture the `DbCommand.CommandText` as part of the subsegment created for your SQL query. The collected `DbCommand.CommandText` will appear as `sanitized_query` in the subsegment JSON. By default, this feature is disabled due to security reasons. +##### .NET Core + If you want to enable this feature, it can be done in two ways. First, by setting the `CollectSqlQueries` to **true** in the `appsettings.json` file as follows: ```json @@ -440,7 +477,7 @@ If you want to enable this feature, it can be done in two ways. First, by settin } ``` -Secondly, you can set the `collectSqlQueries` parameter in the `AddXRayInterceptor()` as **true** to collect the SQL query text. If you set this parameter as **false**, it will disable the `collectSqlQueries` feature for this `AddXRayInterceptor()`. +Secondly, you can set the `collectSqlQueries` parameter in the `AddXRayInterceptor()` as **true** to collect the SQL query text. If you set this parameter as **false**, it will disable the `collectSqlQueries` feature for this `AddXRayInterceptor()`. Opting in `AddXRayInterceptor()` has the highest execution priority, which will override the configuration item in `appsettings.json` mentioned above. ```csharp using Microsoft.EntityFrameworkCore; @@ -463,6 +500,26 @@ public class your_DbContext : DbContext } ``` +##### .NET + +You can enable tracing SQL query text for EF 6 interceptor in the `Web.config` file. + +```xml + + + + + +``` + +You can also pass **true** to `AddXRayInterceptor()` to collect SQL query text, otherwise pass **false** to disable. Opting in `AddXRayInterceptor()` has the highest execution priority, which will override the configuration item in `Web.config` mentioned above. + +``` +using Amazon.XRay.Recorder.Handlers.EntityFramework; + +AWSXRayEntityFramework6.AddXRayInterceptor(true); +``` + ### Multithreaded Execution (.NET and .NET Core) : [Nuget](https://www.nuget.org/packages/AWSXRayRecorder.Core/) In multithreaded execution, X-Ray context from current to its child thread is automatically set. diff --git a/sdk/src/Handlers/EntityFramework/AWSXRayEntityFramework6.cs b/sdk/src/Handlers/EntityFramework/AWSXRayEntityFramework6.cs new file mode 100644 index 00000000..c17fb06c --- /dev/null +++ b/sdk/src/Handlers/EntityFramework/AWSXRayEntityFramework6.cs @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +// +// Copyright 2020 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.Data.Entity.Infrastructure.Interception; + +namespace Amazon.XRay.Recorder.Handlers.EntityFramework +{ + /// + /// Class for to add . + /// User can pass collectSqlQueries to AddXRayInterceptor() to decide if sanitized_query should be included in the trace + /// context or not. + /// + public static class AWSXRayEntityFramework6 + { + /// + /// Enable tracing SQL queries through EntityFramework 6 for .NET framework by calling AWSXRayEntityFramework6.AddXRayInterceptor() to add into to register X-Ray tracing interceptor. + /// + /// Set this parameter to true to capture sql query text. The value set here overrides the value of CollectSqlQueries in Web.config if present. The default value of this parameter is null. + public static void AddXRayInterceptor(bool? collectSqlQueries = null) + { + DbInterception.Add(new EFInterceptor(collectSqlQueries)); + } + } +} diff --git a/sdk/src/Handlers/EntityFramework/AWSXRayInterceptorExtensions.cs b/sdk/src/Handlers/EntityFramework/AWSXRayInterceptorExtensions.cs index 83084c45..c9919ea9 100644 --- a/sdk/src/Handlers/EntityFramework/AWSXRayInterceptorExtensions.cs +++ b/sdk/src/Handlers/EntityFramework/AWSXRayInterceptorExtensions.cs @@ -30,7 +30,7 @@ public static class AWSXRayInterceptorExtensions /// Add to . /// /// Instance of . - /// + /// Set this parameter to true to capture sql query text. The value set here overrides the value of CollectSqlQueries in appsettings.json if present. The default value of this parameter is null. /// Instance of . public static DbContextOptionsBuilder AddXRayInterceptor(this DbContextOptionsBuilder dbContextOptionsBuilder, bool? collectSqlQueries = null) { diff --git a/sdk/src/Handlers/EntityFramework/AWSXRayRecorder.Handlers.EntityFramework.csproj b/sdk/src/Handlers/EntityFramework/AWSXRayRecorder.Handlers.EntityFramework.csproj index baaa9804..14865753 100644 --- a/sdk/src/Handlers/EntityFramework/AWSXRayRecorder.Handlers.EntityFramework.csproj +++ b/sdk/src/Handlers/EntityFramework/AWSXRayRecorder.Handlers.EntityFramework.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + net45;netstandard2.0 Amazon.com, Inc Amazon Web Service X-Ray Recorder Copyright 2017-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -11,7 +11,7 @@ AWSXRayRecorder.Handlers.EntityFramework Amazon.XRay.Recorder.Handlers.EntityFramework Amazon Web Services - This package contains libraries to trace SQL queries through Entity Framework Core. + This package contains libraries to trace SQL queries through Entity Framework. http://aws.amazon.com/apache2.0/ https://aws.amazon.com/documentation/xray/ https://github.com/aws/aws-xray-sdk-dotnet @@ -30,8 +30,16 @@ 1701;1702;1591; - + + + + + + + + + diff --git a/sdk/src/Handlers/EntityFramework/EFInterceptor.cs b/sdk/src/Handlers/EntityFramework/EFInterceptor.cs index 8b632ca4..071d7e8d 100644 --- a/sdk/src/Handlers/EntityFramework/EFInterceptor.cs +++ b/sdk/src/Handlers/EntityFramework/EFInterceptor.cs @@ -17,9 +17,6 @@ using System.Data.Common; using Microsoft.EntityFrameworkCore.Diagnostics; -using Amazon.XRay.Recorder.Core; -using Amazon.XRay.Recorder.Core.Internal.Entities; -using Amazon.XRay.Recorder.Core.Exceptions; using System.Threading; using System.Threading.Tasks; @@ -27,22 +24,10 @@ namespace Amazon.XRay.Recorder.Handlers.EntityFramework { public class EFInterceptor : DbCommandInterceptor { - private readonly AWSXRayRecorder _recorder; private readonly bool? _collectSqlQueriesOverride; - public EFInterceptor() : this(AWSXRayRecorder.Instance) + public EFInterceptor(bool? collectSqlQueries = null) : base() { - - } - - public EFInterceptor(bool? collectSqlQueries = null) : this(AWSXRayRecorder.Instance, collectSqlQueries) - { - - } - - public EFInterceptor(AWSXRayRecorder recorder, bool? collectSqlQueries = null) : base() - { - _recorder = recorder; _collectSqlQueriesOverride = collectSqlQueries; } @@ -55,7 +40,7 @@ public EFInterceptor(AWSXRayRecorder recorder, bool? collectSqlQueries = null) : /// Result from . public override InterceptionResult ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) { - ProcessBeginCommand(eventData); + EFUtil.ProcessBeginCommand(command, _collectSqlQueriesOverride); return base.ReaderExecuting(command, eventData, result); } @@ -68,7 +53,7 @@ public override InterceptionResult ReaderExecuting(DbCommand comma /// Instance of . public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result) { - ProcessEndCommand(); + EFUtil.ProcessEndCommand(); return base.ReaderExecuted(command, eventData, result); } @@ -82,7 +67,7 @@ public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEv /// Task representing the async operation. public override Task> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { - ProcessBeginCommand(eventData); + EFUtil.ProcessBeginCommand(command, _collectSqlQueriesOverride); return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); } @@ -96,7 +81,7 @@ public override Task> ReaderExecutingAsync(DbCo /// Task representing the async operation. public override Task ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default) { - ProcessEndCommand(); + EFUtil.ProcessEndCommand(); return base.ReaderExecutedAsync(command, eventData, result, cancellationToken); } @@ -107,7 +92,7 @@ public override Task ReaderExecutedAsync(DbCommand command, Comman /// Instance of . public override void CommandFailed(DbCommand command, CommandErrorEventData eventData) { - ProcessCommandError(eventData); + EFUtil.ProcessCommandError(eventData.Exception); base.CommandFailed(command, eventData); } @@ -120,7 +105,7 @@ public override void CommandFailed(DbCommand command, CommandErrorEventData even /// Task representing the async operation. public override Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = default) { - ProcessCommandError(eventData); + EFUtil.ProcessCommandError(eventData.Exception); return base.CommandFailedAsync(command, eventData, cancellationToken); } @@ -133,7 +118,7 @@ public override Task CommandFailedAsync(DbCommand command, CommandErrorEventData /// Task representing the operation. public override InterceptionResult NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) { - ProcessBeginCommand(eventData); + EFUtil.ProcessBeginCommand(command, _collectSqlQueriesOverride); return base.NonQueryExecuting(command, eventData, result); } @@ -147,7 +132,7 @@ public override InterceptionResult NonQueryExecuting(DbCommand command, Com /// Task representing the async operation. public override Task> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { - ProcessBeginCommand(eventData); + EFUtil.ProcessBeginCommand(command, _collectSqlQueriesOverride); return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken); } @@ -160,7 +145,7 @@ public override Task> NonQueryExecutingAsync(DbCommand c /// Result as integer. public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result) { - ProcessEndCommand(); + EFUtil.ProcessEndCommand(); return base.NonQueryExecuted(command, eventData, result); } @@ -174,7 +159,7 @@ public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData /// Task representing the async operation. public override Task NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default) { - ProcessEndCommand(); + EFUtil.ProcessEndCommand(); return base.NonQueryExecutedAsync(command, eventData, result, cancellationToken); } @@ -187,7 +172,7 @@ public override Task NonQueryExecutedAsync(DbCommand command, CommandExecut /// Result from . public override InterceptionResult ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) { - ProcessBeginCommand(eventData); + EFUtil.ProcessBeginCommand(command, _collectSqlQueriesOverride); return base.ScalarExecuting(command, eventData, result); } @@ -201,7 +186,7 @@ public override InterceptionResult ScalarExecuting(DbCommand command, Co /// Task representing the async operation. public override Task> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { - ProcessBeginCommand(eventData); + EFUtil.ProcessBeginCommand(command, _collectSqlQueriesOverride); return base.ScalarExecutingAsync(command, eventData, result, cancellationToken); } @@ -214,7 +199,7 @@ public override Task> ScalarExecutingAsync(DbCommand /// Result object. public override object ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object result) { - ProcessEndCommand(); + EFUtil.ProcessEndCommand(); return base.ScalarExecuted(command, eventData, result); } @@ -228,104 +213,8 @@ public override object ScalarExecuted(DbCommand command, CommandExecutedEventDat /// Task representing the async operation. public override Task ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object result, CancellationToken cancellationToken = default) { - ProcessEndCommand(); + EFUtil.ProcessEndCommand(); return base.ScalarExecutedAsync(command, eventData, result, cancellationToken); } - - private void ProcessBeginCommand(CommandEventData eventData) - { - Entity entity = null; - try - { - entity = _recorder.GetEntity(); - } - catch (EntityNotAvailableException e) - { - _recorder.TraceContext.HandleEntityMissing(_recorder, e, "Cannot get entity while processing start of Entity Framework command."); - } - - _recorder.BeginSubsegment(BuildSubsegmentName(eventData.Command)); - _recorder.SetNamespace("remote"); - CollectSqlInformation(eventData); - } - - private void ProcessEndCommand() - { - Entity entity = null; - try - { - entity = _recorder.GetEntity(); - } - catch (EntityNotAvailableException e) - { - _recorder.TraceContext.HandleEntityMissing(_recorder, e, "Cannot get entity while processing end of Entity Framework command."); - return; - } - - _recorder.EndSubsegment(); - } - - private void ProcessCommandError(CommandErrorEventData eventData) - { - Entity subsegment; - try - { - subsegment = _recorder.GetEntity(); - } - catch (EntityNotAvailableException e) - { - _recorder.TraceContext.HandleEntityMissing(_recorder, e, "Cannot get entity while processing failure of Entity Framework command."); - return; - } - - subsegment.AddException(eventData.Exception); - - _recorder.EndSubsegment(); - } - - /// - /// Records the SQL information on the current subsegment, - /// - protected virtual void CollectSqlInformation(CommandEventData eventData) - { - // Get database type from DbContext - string databaseType = EFUtil.GetDataBaseType(eventData.Context); - _recorder.AddSqlInformation("database_type", databaseType); - - _recorder.AddSqlInformation("database_version", eventData.Command.Connection.ServerVersion); - - DbConnectionStringBuilder connectionStringBuilder = new DbConnectionStringBuilder - { - ConnectionString = eventData.Command.Connection.ConnectionString - }; - - // Remove sensitive information from connection string - connectionStringBuilder.Remove("Password"); - - // Do a pre-check for UserID since in the case of TrustedConnection, a UserID may not be available. - var user_id = EFUtil.GetUserId(connectionStringBuilder); - if (user_id != null) - { - _recorder.AddSqlInformation("user", user_id.ToString()); - } - - _recorder.AddSqlInformation("connection_string", connectionStringBuilder.ToString()); - - if (ShouldCollectSqlText()) - { - _recorder.AddSqlInformation("sanitized_query", eventData.Command.CommandText); - } - } - - /// - /// Builds the name of the subsegment in the format database@datasource - /// - /// Instance of . - /// Returns the formed subsegment name as a string. - private string BuildSubsegmentName(DbCommand command) - => command.Connection.Database + "@" + EFUtil.RemovePortNumberFromDataSource(command.Connection.DataSource); - - private bool ShouldCollectSqlText() - => _collectSqlQueriesOverride ?? _recorder.XRayOptions.CollectSqlQueries; } } \ No newline at end of file diff --git a/sdk/src/Handlers/EntityFramework/EFInterceptor.net45.cs b/sdk/src/Handlers/EntityFramework/EFInterceptor.net45.cs new file mode 100644 index 00000000..70825467 --- /dev/null +++ b/sdk/src/Handlers/EntityFramework/EFInterceptor.net45.cs @@ -0,0 +1,113 @@ +//----------------------------------------------------------------------------- +// +// Copyright 2020 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.Common; +using System.Data.Entity.Infrastructure.Interception; + +namespace Amazon.XRay.Recorder.Handlers.EntityFramework +{ + /// + /// Class to intercept SQL query through EF 6 for .NET framework. + /// + public class EFInterceptor : IDbCommandInterceptor + { + private readonly bool? _collectSqlQueriesOverride; + + public EFInterceptor(bool? collectSqlQueries = null) : base() + { + _collectSqlQueriesOverride = collectSqlQueries; + } + + /// + /// Trace before executing non query. + /// + /// Instance of . + /// Instance of . + public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) + { + OnCommandStart(command); + } + + /// + /// Trace after executing non query. + /// + /// Instance of . + /// Instance of . + public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) + { + OnCommandStop(interceptionContext.Exception); + } + + /// + /// Trace before executing reader. + /// + /// Instance of . + /// Instance of . + public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) + { + OnCommandStart(command); + } + + /// + /// Trace after executing reader. + /// + /// Instance of . + /// Instance of . + public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) + { + OnCommandStop(interceptionContext.Exception); + } + + /// + /// Trace before executing scalar. + /// + /// Instance of . + /// Instance of . + public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) + { + OnCommandStart(command); + } + + /// + /// Trace after executing scalar. + /// + /// Instance of . + /// Instance of . + public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) + { + OnCommandStop(interceptionContext.Exception); + } + + private void OnCommandStart(DbCommand command) + { + EFUtil.ProcessBeginCommand(command, _collectSqlQueriesOverride); + } + + private void OnCommandStop(Exception exception) + { + if (exception != null) + { + EFUtil.ProcessCommandError(exception); + } + else + { + EFUtil.ProcessEndCommand(); + } + } + } +} diff --git a/sdk/src/Handlers/EntityFramework/EFUtil.cs b/sdk/src/Handlers/EntityFramework/EFUtil.cs index 2f40034a..74d49182 100644 --- a/sdk/src/Handlers/EntityFramework/EFUtil.cs +++ b/sdk/src/Handlers/EntityFramework/EFUtil.cs @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // -// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2020 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. @@ -15,78 +15,152 @@ // //----------------------------------------------------------------------------- -using System.Collections.Generic; +using System; using System.Data.Common; using System.Text.RegularExpressions; -using Microsoft.EntityFrameworkCore; +using Amazon.Runtime.Internal.Util; +using Amazon.XRay.Recorder.Core; +#if NET45 +using Amazon.XRay.Recorder.Core.Internal.Utils; +#endif namespace Amazon.XRay.Recorder.Handlers.EntityFramework { /// /// Utilities for EFInterceptor /// - public static class EFUtil + internal static class EFUtil { - private static readonly string DefaultDatabaseType = "EntityFrameworkCore"; + private static readonly string DefaultDbTypeEntityFramework = "entityframework"; + private static readonly string SqlServerCompact35 = "sqlservercompact35"; + private static readonly string SqlServerCompact40 = "sqlservercompact40"; + private static readonly string MicrosoftSqlClient = "microsoft.data.sqlclient"; + private static readonly string SystemSqlClient = "system.data.sqlclient"; + private static readonly string SqlServer = "sqlserver"; private static readonly string[] UserIdFormatOptions = { "user id", "username", "user", "userid" }; // case insensitive - // Some database providers may not support Entity Framework Core 3.0 and above now - // https://docs.microsoft.com/en-us/ef/core/providers/?tabs=dotnet-core-cli - private static readonly Dictionary DatabaseTypes = new Dictionary() - { - { "Microsoft.EntityFrameworkCore.SqlServer" , "sqlserver" }, - { "Microsoft.EntityFrameworkCore.Sqlite" , "sqlite" }, - { "Npgsql.EntityFrameworkCore.PostgreSQL" , "postgresql" }, - { "Pomelo.EntityFrameworkCore.MySql" , "mysql" }, - { "FirebirdSql.EntityFrameworkCore.Firebird" , "firebirdsql" }, - - { "Microsoft.EntityFrameworkCore.InMemory" , "inmemory" }, - { "Microsoft.EntityFrameworkCore.Cosmos" , "cosmosdb" }, - { "Devart.Data.MySql.EFCore" , "mysql" }, - { "Devart.Data.Oracle.EFCore" , "oracle" }, - { "Devart.Data.PostgreSql.EFCore" , "postgresql" }, - { "Devart.Data.SQLite.EFCore" , "sqlite" }, - { "FileContextCore" , "filecontextcore" }, - { "EntityFrameworkCore.Jet" , "jet" }, - { "EntityFrameworkCore.SqlServerCompact35" , "sqlservercompact35" }, - { "EntityFrameworkCore.SqlServerCompact40" , "sqlservercompact40" }, - { "Teradata.EntityFrameworkCore" , "teradata" }, - { "EntityFrameworkCore.FirebirdSql" , "firebirdsql" }, - { "EntityFrameworkCore.OpenEdge" , "openedge" }, - { "MySql.Data.EntityFrameworkCore" , "mysql" }, - { "Oracle.EntityFrameworkCore" , "oracle" }, - { "IBM.EntityFrameworkCore" , "ibm" }, - { "IBM.EntityFrameworkCore-lnx" , "ibm" }, - { "IBM.EntityFrameworkCore-osx" , "ibm" }, - { "Pomelo.EntityFrameworkCore.MyCat" , "mycat" } - }; + private static readonly string[] DatabaseTypes = { "sqlserver", "sqlite", "postgresql", "mysql", "firebirdsql", + "inmemory" , "cosmosdb" , "oracle" , "filecontextcore" , + "jet" , "teradata" , "openedge" , "ibm" , "mycat" , "vfp"}; private static readonly Regex _portNumberRegex = new Regex(@"[,|:]\d+$"); + private static readonly AWSXRayRecorder _recorder = AWSXRayRecorder.Instance; + + private static readonly Logger _logger = Logger.GetLogger(typeof(EFUtil)); + + /// + /// Process command to begin subsegment. + /// + /// Instance of . + /// Nullable to indicate whether to collect sql query text or not. + internal static void ProcessBeginCommand(DbCommand command, bool? collectSqlQueriesOverride) + { + _recorder.BeginSubsegment(BuildSubsegmentName(command)); + _recorder.SetNamespace("remote"); + CollectSqlInformation(command, collectSqlQueriesOverride); + } + + /// + /// Process to end subsegment + /// + internal static void ProcessEndCommand() + { + _recorder.EndSubsegment(); + } + /// - /// Extract database_type from . + /// Process exception. /// - /// Instance of . + /// Instance of . + internal static void ProcessCommandError(Exception exception) + { + _recorder.AddException(exception); + _recorder.EndSubsegment(); + } + + /// + /// Builds the name of the subsegment in the format database@datasource + /// + /// Instance of . + /// Returns the formed subsegment name as a string. + private static string BuildSubsegmentName(DbCommand command) + => command.Connection.Database + "@" + RemovePortNumberFromDataSource(command.Connection.DataSource); + + /// + /// Records the SQL information on the current subsegment, + /// + private static void CollectSqlInformation(DbCommand command, bool? collectSqlQueriesOverride) + { + // Get database type from DbCommand + string databaseType = GetDataBaseType(command); + _recorder.AddSqlInformation("database_type", databaseType); + + DbConnectionStringBuilder connectionStringBuilder = new DbConnectionStringBuilder + { + ConnectionString = command.Connection.ConnectionString + }; + + // Remove sensitive information from connection string + connectionStringBuilder.Remove("Password"); + + _recorder.AddSqlInformation("connection_string", connectionStringBuilder.ToString()); + + // Do a pre-check for UserID since in the case of TrustedConnection, a UserID may not be available. + var user_id = GetUserId(connectionStringBuilder); + if (user_id != null) + { + _recorder.AddSqlInformation("user", user_id.ToString()); + } + + if (ShouldCollectSqlText(collectSqlQueriesOverride)) + { + _recorder.AddSqlInformation("sanitized_query", command.CommandText); + } + + _recorder.AddSqlInformation("database_version", command.Connection.ServerVersion); + } + + /// + /// Extract database_type from . + /// + /// Instance of . /// Type of database. - public static string GetDataBaseType(DbContext context) + internal static string GetDataBaseType(DbCommand command) { - string databaseProvider = context?.Database?.ProviderName; + var typeString = command?.Connection?.GetType()?.FullName?.ToLower(); - // Need to check if the context and its following parameter is null or not to avoid exception - if (string.IsNullOrEmpty(databaseProvider)) + if (string.IsNullOrEmpty(typeString)) { - return DefaultDatabaseType; + _logger.DebugFormat("Can't extract database type from connection, setting it as default: ({0})", DefaultDbTypeEntityFramework); + return DefaultDbTypeEntityFramework; } - string value = null; + if (typeString.Contains(MicrosoftSqlClient) || typeString.Contains(SystemSqlClient)) + { + return SqlServer; + } + + if (typeString.Contains(SqlServerCompact35)) + { + return SqlServerCompact35; + } + + if (typeString.Contains(SqlServerCompact40)) + { + return SqlServerCompact40; + } - if (DatabaseTypes.TryGetValue(databaseProvider, out value)) + foreach (var databaseType in DatabaseTypes) { - return value; + if (typeString.Contains(databaseType)) + { + return databaseType; + } } - return databaseProvider; + return typeString; } /// @@ -94,17 +168,17 @@ public static string GetDataBaseType(DbContext context) /// /// Instance of . /// - public static object GetUserId(DbConnectionStringBuilder builder) + internal static string GetUserId(DbConnectionStringBuilder builder) { - object value = null; foreach (string key in UserIdFormatOptions) { - if (builder.TryGetValue(key, out value)) + if (builder.TryGetValue(key, out object value)) { - break; + return (string)value; } } - return value; + + return null; } /// @@ -112,9 +186,17 @@ public static object GetUserId(DbConnectionStringBuilder builder) /// /// The data source. /// The data source string with port number removed. - public static string RemovePortNumberFromDataSource(string dataSource) + private static string RemovePortNumberFromDataSource(string dataSource) { return _portNumberRegex.Replace(dataSource, string.Empty); } + +#if !NET45 + private static bool ShouldCollectSqlText(bool? collectSqlQueriesOverride) + => collectSqlQueriesOverride ?? _recorder.XRayOptions.CollectSqlQueries; +#else + private static bool ShouldCollectSqlText(bool? collectSqlQueriesOverride) + => collectSqlQueriesOverride ?? AppSettings.CollectSqlQueries; +#endif } } diff --git a/sdk/src/Handlers/EntityFramework/Properties/AssemblyInfo.cs b/sdk/src/Handlers/EntityFramework/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..bbe308c3 --- /dev/null +++ b/sdk/src/Handlers/EntityFramework/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AWSXRayRecorder.UnitTests,PublicKey=" + +"0024000004800000940000000602000000240000525341310004000001000100712913451f6deb" ++ "158da1d2129b21119cca7d4eebeef5b310e8acd7f2d9506346071207652f1210a3bfa1545d6897" ++ "a607fc3a515954e660ec6fc5797730022867514e58411e8ecd61c767a319d2c29facee20f5d4f4" ++ "2b5425f27518616a8f4c1e5ac0e3e2b407bd8786d1b360af6b49c2b987478fe76b124c72f48864" ++ "55199df6" +)] diff --git a/sdk/test/UnitTests/AWSXRayRecorder.UnitTests.csproj b/sdk/test/UnitTests/AWSXRayRecorder.UnitTests.csproj index a46ce2e6..3c0a5672 100644 --- a/sdk/test/UnitTests/AWSXRayRecorder.UnitTests.csproj +++ b/sdk/test/UnitTests/AWSXRayRecorder.UnitTests.csproj @@ -1,4 +1,4 @@ - + @@ -54,9 +54,9 @@ - + @@ -64,6 +64,7 @@ + @@ -74,8 +75,11 @@ + + + @@ -83,8 +87,8 @@ + - diff --git a/sdk/test/UnitTests/EF6Tests.cs b/sdk/test/UnitTests/EF6Tests.cs new file mode 100644 index 00000000..bf86f378 --- /dev/null +++ b/sdk/test/UnitTests/EF6Tests.cs @@ -0,0 +1,186 @@ +//----------------------------------------------------------------------------- +// +// Copyright 2020 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 Amazon.XRay.Recorder.Core; +using Amazon.XRay.Recorder.Core.Internal.Entities; +using Amazon.XRay.Recorder.Handlers.EntityFramework; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Data.Entity.Infrastructure.Interception; +using System.Data.SQLite; + +namespace Amazon.XRay.Recorder.UnitTests +{ + [TestClass] + public class EF6Tests : TestBase + { + private SQLiteConnection connection = null; + private AWSXRayRecorder recorder = null; + private const string connectionString = "data source=:memory:"; + private const string database = "sqlite"; + private const string nameSpace = "remote"; + private const string commandText = "Test command text"; + + [TestInitialize] + public void Initialize() + { + recorder = new AWSXRayRecorder(); + recorder.BeginSegment("Test EF6"); + connection = new SQLiteConnection(connectionString); + connection.Open(); + } + + [TestCleanup] + public new void TestCleanup() + { + connection.Close(); + connection.Dispose(); + recorder.EndSegment(); + recorder.Dispose(); + base.TestCleanup(); + } + + [TestMethod] + public void TestEFInterceptorNonQuery() + { + var efInterceptor = RegisterInterceptor(true); + + try + { + using (var command = new SQLiteCommand(commandText, connection)) + { + DbInterception.Dispatch.Command.NonQuery(command, new DbCommandInterceptionContext()); // calling API from IDbCommandInterceptor + } + } + catch + { + // Will throw exception as command text is invalid + } + + var segment = recorder.GetEntity() as Segment; + Assert.IsTrue(segment.Subsegments.Count != 0); + + var subsegment = segment.Subsegments[0]; + AssertTraceCollected(subsegment); + Assert.AreEqual(commandText, subsegment.Sql["sanitized_query"]); + + RemoveInterceptor(efInterceptor); + } + + [TestMethod] + public void TestEFInterceptorReader() + { + var efInterceptor = RegisterInterceptor(true); + + try + { + using (var command = new SQLiteCommand(commandText, connection)) + { + DbInterception.Dispatch.Command.Reader(command, new DbCommandInterceptionContext()); // calling API from IDbCommandInterceptor + } + } + catch + { + // Will throw exception as command text is invalid + } + + var segment = recorder.GetEntity() as Segment; + Assert.IsTrue(segment.Subsegments.Count != 0); + + var subsegment = segment.Subsegments[0]; + AssertTraceCollected(subsegment); + Assert.AreEqual(commandText, subsegment.Sql["sanitized_query"]); + + RemoveInterceptor(efInterceptor); + } + + [TestMethod] + public void TestEFInterceptorScalar() + { + var efInterceptor = RegisterInterceptor(true); + + try + { + using (var command = new SQLiteCommand(commandText, connection)) + { + DbInterception.Dispatch.Command.Scalar(command, new DbCommandInterceptionContext()); // calling API from IDbCommandInterceptor + } + } + catch + { + // Will throw exception as command text is invalid + } + + var segment = recorder.GetEntity() as Segment; + Assert.IsTrue(segment.Subsegments.Count != 0); + + var subsegment = segment.Subsegments[0]; + AssertTraceCollected(subsegment); + Assert.AreEqual(commandText, subsegment.Sql["sanitized_query"]); + + RemoveInterceptor(efInterceptor); + } + + [TestMethod] + public void TestEFInterceptorNonQueryWithoutQueryText() + { + var efInterceptor = RegisterInterceptor(false); + + try + { + using (var command = new SQLiteCommand(commandText, connection)) + { + DbInterception.Dispatch.Command.NonQuery(command, new DbCommandInterceptionContext()); // calling API from IDbCommandInterceptor + } + } + catch + { + // Will throw exception as command text is invalid + } + + var segment = recorder.GetEntity() as Segment; + Assert.IsTrue(segment.Subsegments.Count != 0); + + var subsegment = segment.Subsegments[0]; + AssertTraceCollected(subsegment); + Assert.IsFalse(subsegment.Sql.ContainsKey("sanitized_query")); + + RemoveInterceptor(efInterceptor); + } + + private EFInterceptor RegisterInterceptor(bool collectSqlQueries) + { + var efInterceptor = new EFInterceptor(collectSqlQueries); + DbInterception.Add(efInterceptor); + return efInterceptor; + } + + // Remove EFInterceptor from DbInterceptor to avoid duplicate tracing + private void RemoveInterceptor(EFInterceptor interceptor) + { + DbInterception.Remove(interceptor); + } + + private void AssertTraceCollected(Subsegment subsegment) + { + Assert.AreEqual(connection.ConnectionString, subsegment.Sql["connection_string"]); + Assert.AreEqual(database, subsegment.Sql["database_type"]); + Assert.AreEqual(connection.ServerVersion, subsegment.Sql["database_version"]); + Assert.AreEqual(nameSpace, subsegment.Namespace); + Assert.IsTrue(subsegment.HasFault); + } + } +} diff --git a/sdk/test/UnitTests/EFUtilTests.cs b/sdk/test/UnitTests/EFUtilTests.cs index b429ef29..31941fab 100644 --- a/sdk/test/UnitTests/EFUtilTests.cs +++ b/sdk/test/UnitTests/EFUtilTests.cs @@ -18,6 +18,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Data.Common; using Amazon.XRay.Recorder.Handlers.EntityFramework; +using System.Data.SQLite; +using System.Data.SqlClient; namespace Amazon.XRay.Recorder.UnitTests { @@ -90,5 +92,31 @@ public void Test_Get_UserId_FirebirdSql() object result = EFUtil.GetUserId(builder); Assert.AreEqual("SYSDBA", result.ToString()); } + + [TestMethod] + public void Test_Get_Database_Type_Sqlite() + { + var connection = new SQLiteConnection(); + var command = new SQLiteCommand(null, connection); + var databaseType = EFUtil.GetDataBaseType(command); + Assert.AreEqual("sqlite", databaseType); + } + + [TestMethod] + public void Test_Get_Database_Type_SqlServer() + { + var connection = new SqlConnection(); + var command = new SqlCommand(null, connection); + var databaseType = EFUtil.GetDataBaseType(command); + Assert.AreEqual("sqlserver", databaseType); + } + + [TestMethod] + public void Test_Get_Database_Type_EmptyConnection() + { + var command = new SqlCommand(); + var databaseType = EFUtil.GetDataBaseType(command); + Assert.AreEqual("entityframework", databaseType); // Empty connection will return entityframework by default. + } } } \ No newline at end of file diff --git a/sdk/test/UnitTests/Tools/TestEFContext.cs b/sdk/test/UnitTests/Tools/TestEFContext.cs index b786f046..607531ed 100644 --- a/sdk/test/UnitTests/Tools/TestEFContext.cs +++ b/sdk/test/UnitTests/Tools/TestEFContext.cs @@ -1,6 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Text; +//----------------------------------------------------------------------------- +// +// Copyright 2020 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 Microsoft.EntityFrameworkCore;