diff --git a/Datadog.Trace.sln b/Datadog.Trace.sln index d194eec941c9..0a57f35d256b 100644 --- a/Datadog.Trace.sln +++ b/Datadog.Trace.sln @@ -236,6 +236,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".azure-pipelines", ".azure- EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SynchronizeVersions", "tools\SynchronizeVersions\SynchronizeVersions.csproj", "{7F0BD409-FA74-4FB1-853F-D10D888E1542}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.MySql", "samples\Samples.MySql\Samples.MySql.csproj", "{42FA33DD-AEA3-4FF3-8319-F30244A666A4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -744,6 +746,16 @@ Global {7F0BD409-FA74-4FB1-853F-D10D888E1542}.Release|x64.Build.0 = Release|Any CPU {7F0BD409-FA74-4FB1-853F-D10D888E1542}.Release|x86.ActiveCfg = Release|Any CPU {7F0BD409-FA74-4FB1-853F-D10D888E1542}.Release|x86.Build.0 = Release|Any CPU + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Debug|Any CPU.ActiveCfg = Debug|x86 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Debug|x64.ActiveCfg = Debug|x64 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Debug|x64.Build.0 = Debug|x64 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Debug|x86.ActiveCfg = Debug|x86 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Debug|x86.Build.0 = Debug|x86 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Release|Any CPU.ActiveCfg = Release|x86 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Release|x64.ActiveCfg = Release|x64 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Release|x64.Build.0 = Release|x64 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Release|x86.ActiveCfg = Release|x86 + {42FA33DD-AEA3-4FF3-8319-F30244A666A4}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -797,6 +809,7 @@ Global {6ABAD006-E206-488E-ACA1-1AA73F9B5146} = {FEBCE7DC-9FD1-48A6-B911-71ABB240A030} {C52D6695-4E05-4930-88F8-0EFF8056A967} = {FEBCE7DC-9FD1-48A6-B911-71ABB240A030} {7F0BD409-FA74-4FB1-853F-D10D888E1542} = {5D8E1F81-B820-4736-B797-271B0FE787EE} + {42FA33DD-AEA3-4FF3-8319-F30244A666A4} = {AA6F5582-3B71-49AC-AA39-8F7815AC46BE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {160A1D00-1F5B-40F8-A155-621B4459D78F} diff --git a/docker-compose.yml b/docker-compose.yml index 11206a22d3db..8705b033bf1e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,6 +43,16 @@ services: ports: - "127.0.0.1:5432:5432" + mysql: + image: mysql/mysql-server:5.7 + environment: + - MYSQL_DATABASE=world + - MYSQL_ROOT_PASSWORD=mysqldb + - MYSQL_USER=mysqldb + - MYSQL_PASSWORD=mysqldb + ports: + - "127.0.0.1:3306:3306" + sqlserver: image: microsoft/mssql-server-linux:latest ports: @@ -163,6 +173,19 @@ services: - POSTGRES_HOST=postgres depends_on: - postgres + + Samples.MySql: + build: + context: ./ + dockerfile: ./docker/dotnet.dockerfile + image: datadog-dotnet + command: bash -c "/project/docker/with-profiler-logs.bash wait-for-it mysql:3306 -- /project/docker/with-profiler.bash dotnet /project/samples/Samples.MySql/bin/Release/netcoreapp2.1/publish/Samples.MySql.dll" + volumes: + - ./:/project + environment: + - MYSQL_HOST=mysql + depends_on: + - mysql Samples.MongoDB: build: @@ -191,6 +214,7 @@ services: - ELASTICSEARCH5_HOST=elasticsearch5:9200 - SQLSERVER_CONNECTION_STRING=Server=sqlserver;Database=BlogDatabase;User=sa;Password=Strong!Passw0rd - POSTGRES_HOST=postgres + - MYSQL_HOST=mysql depends_on: - servicestackredis - stackexchangeredis diff --git a/samples/Samples.MySql/Program.cs b/samples/Samples.MySql/Program.cs new file mode 100644 index 000000000000..995f86e20408 --- /dev/null +++ b/samples/Samples.MySql/Program.cs @@ -0,0 +1,83 @@ +using System; +using Datadog.Trace.ClrProfiler; +using MySql.Data.MySqlClient; + +namespace Samples.MySql +{ + class Program + { + private static string Host() + { + return Environment.GetEnvironmentVariable("MYSQL_HOST") ?? "localhost"; + } + + private static string ConnectionString(string database) + { + return $"server={Host()};user=mysqldb;password=mysqldb;port=3306;database={database}"; + } + + private static void Main(string[] args) + { + Console.WriteLine($"Profiler attached: {Instrumentation.ProfilerAttached}"); + Console.WriteLine($"Platform: {(Environment.Is64BitProcess ? "x64" : "x32")}"); + Console.WriteLine(); + + Console.WriteLine("Opening the connection."); + + string connStr = ConnectionString("world"); + var conn = new MySqlConnection(connStr); + conn.Open(); + + Console.WriteLine("Creating the table for the continents."); + + // Create table + var tableCommand = + new MySqlCommand( + "DROP TABLE IF EXISTS `continent`; CREATE TABLE continent (continent_id INT AUTO_INCREMENT, name VARCHAR(255) NOT NULL, PRIMARY KEY(continent_id));", + conn); + tableCommand.ExecuteNonQuery(); + + Console.WriteLine("Creating the continents."); + + // Create continents + MySqlCommand createContinent; + createContinent = new MySqlCommand("INSERT INTO continent (name) VALUES ('Africa');", conn); + createContinent.ExecuteNonQuery(); + createContinent = new MySqlCommand("INSERT INTO continent (name) VALUES ('Antarctica');", conn); + createContinent.ExecuteNonQuery(); + createContinent = new MySqlCommand("INSERT INTO continent (name) VALUES ('Asia');", conn); + createContinent.ExecuteNonQuery(); + createContinent = new MySqlCommand("INSERT INTO continent (name) VALUES ('Australia');", conn); + createContinent.ExecuteNonQuery(); + createContinent = new MySqlCommand("INSERT INTO continent (name) VALUES ('Europe');", conn); + createContinent.ExecuteNonQuery(); + createContinent = new MySqlCommand("INSERT INTO continent (name) VALUES ('North America');", conn); + createContinent.ExecuteNonQuery(); + createContinent = new MySqlCommand("INSERT INTO continent (name) VALUES ('South America');", conn); + createContinent.ExecuteNonQuery(); + + try + { + Console.WriteLine("Beginning to read the continents."); + string sql = "SELECT continent_id, name FROM continent"; + var readContinents = new MySqlCommand(sql, conn); + using (var rdr = readContinents.ExecuteReader()) + { + while (rdr.Read()) + { + Console.WriteLine(rdr[0] + " -- " + rdr[1]); + } + rdr.Close(); + } + Console.WriteLine("Done reading the continents."); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + + conn.Close(); + Console.WriteLine("Done setting up, inserting, and reading from MySQL."); + } + } +} diff --git a/samples/Samples.MySql/Properties/launchSettings.json b/samples/Samples.MySql/Properties/launchSettings.json new file mode 100644 index 000000000000..a6fbbc0dd4a6 --- /dev/null +++ b/samples/Samples.MySql/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Samples.MySql": { + "commandName": "Project", + "environmentVariables": { + "COR_ENABLE_PROFILING": "1", + "COR_PROFILER": "{846F5F1C-F9AE-4B07-969E-05C26BC060D8}", + "COR_PROFILER_PATH": "$(ProjectDir)$(OutputPath)profiler-lib\\Datadog.Trace.ClrProfiler.Native.dll", + + "CORECLR_ENABLE_PROFILING": "1", + "CORECLR_PROFILER": "{846F5F1C-F9AE-4B07-969E-05C26BC060D8}", + "CORECLR_PROFILER_PATH": "$(ProjectDir)$(OutputPath)profiler-lib\\Datadog.Trace.ClrProfiler.Native.dll", + + "DD_INTEGRATIONS": "$(ProjectDir)$(OutputPath)profiler-lib\\integrations.json" + }, + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/samples/Samples.MySql/Samples.MySql.csproj b/samples/Samples.MySql/Samples.MySql.csproj new file mode 100644 index 000000000000..3a0fef0e4257 --- /dev/null +++ b/samples/Samples.MySql/Samples.MySql.csproj @@ -0,0 +1,13 @@ + + + + 8.0.17 + + false + + + + + + + \ No newline at end of file diff --git a/samples/Samples.Npgsql/Program.cs b/samples/Samples.Npgsql/Program.cs index b3f71a5299f7..eb36fefc50c3 100644 --- a/samples/Samples.Npgsql/Program.cs +++ b/samples/Samples.Npgsql/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Data.Common; using System.Linq; using Datadog.Trace.ClrProfiler; using Npgsql; diff --git a/samples/Samples.SqlServer/Program.cs b/samples/Samples.SqlServer/Program.cs index 007997c0d02a..e47e0b6c05a8 100644 --- a/samples/Samples.SqlServer/Program.cs +++ b/samples/Samples.SqlServer/Program.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Microsoft.EntityFrameworkCore; namespace Samples.SqlServer { @@ -22,16 +23,37 @@ static void Main(string[] args) db.SaveChanges(); } - // Display all Blogs from the database + // Display all Blogs from the database synchronously var query = from b in db.Blogs orderby b.Name select b; - Console.WriteLine("All blogs in the database:"); + Console.WriteLine("All blogs in the database from the synchronous call:"); foreach (var item in query) { Console.WriteLine(item.Name); } + + var asyncName = "test-async"; + + var asyncBlog = (from b in db.Blogs where b.Name == asyncName select b).FirstOrDefaultAsync(); + if (asyncBlog.Result == null) + { + blog = new Blog { Name = asyncName }; + db.Blogs.Add(blog); + db.SaveChangesAsync().Wait(); + } + + // Display all Blogs from the database asynchronously + var asyncQueryTask = db.Blogs.Where(b => b.Name == asyncName).ToListAsync(); + + asyncQueryTask.Wait(); + + Console.WriteLine("All blogs in the database from the async call:"); + foreach (var item in asyncQueryTask.Result) + { + Console.WriteLine(item.Name); + } } } } diff --git a/src/Datadog.Trace.ClrProfiler.Managed/Integrations/AdoNetIntegration.cs b/src/Datadog.Trace.ClrProfiler.Managed/Integrations/AdoNetIntegration.cs index 46f2f255e4e9..1123640936ad 100644 --- a/src/Datadog.Trace.ClrProfiler.Managed/Integrations/AdoNetIntegration.cs +++ b/src/Datadog.Trace.ClrProfiler.Managed/Integrations/AdoNetIntegration.cs @@ -1,9 +1,11 @@ using System; using System.Data; using System.Data.Common; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Datadog.Trace.ClrProfiler.Emit; +using Datadog.Trace.ClrProfiler.Helpers; using Datadog.Trace.ExtensionMethods; using Datadog.Trace.Logging; @@ -16,6 +18,11 @@ public static class AdoNetIntegration { private const string IntegrationName = "AdoNet"; private const string Major4 = "4"; + private const string FrameworkAssembly = "System.Data"; + private const string CoreAssembly = "System.Data.Common"; + private const string SystemDataCommonDbCommand = "System.Data.Common.DbCommand"; + private const string SystemDataCommonDbDataReader = "System.Data.Common.DbDataReader"; + private const string SystemDataCommonCommandBehavior = "System.Data.CommandBehavior"; private static readonly ILog Log = LogProvider.GetLogger(typeof(AdoNetIntegration)); @@ -29,15 +36,15 @@ public static class AdoNetIntegration /// A pointer to the module version GUID. /// The value returned by the instrumented method. [InterceptMethod( - TargetAssembly = "System.Data", // .NET Framework - TargetType = "System.Data.Common.DbCommand", - TargetSignatureTypes = new[] { "System.Data.Common.DbDataReader", "System.Data.CommandBehavior" }, + TargetAssembly = FrameworkAssembly, // .NET Framework + TargetType = SystemDataCommonDbCommand, + TargetSignatureTypes = new[] { SystemDataCommonDbDataReader, SystemDataCommonCommandBehavior }, TargetMinimumVersion = Major4, TargetMaximumVersion = Major4)] [InterceptMethod( - TargetAssembly = "System.Data.Common", // .NET Core - TargetType = "System.Data.Common.DbCommand", - TargetSignatureTypes = new[] { "System.Data.Common.DbDataReader", "System.Data.CommandBehavior" }, + TargetAssembly = CoreAssembly, // .NET Core + TargetType = SystemDataCommonDbCommand, + TargetSignatureTypes = new[] { SystemDataCommonDbDataReader, SystemDataCommonCommandBehavior }, TargetMinimumVersion = Major4, TargetMaximumVersion = Major4)] public static object ExecuteDbDataReader( @@ -47,24 +54,35 @@ public static object ExecuteDbDataReader( int mdToken, long moduleVersionPtr) { - var command = (DbCommand)@this; + Func instrumentedMethod = null; var commandBehavior = (CommandBehavior)behavior; - var executeReader = Emit.DynamicMethodBuilder> - .GetOrCreateMethodCallDelegate( - typeof(DbCommand), - "ExecuteDbDataReader", - (OpCodeValue)opCode); + try + { + var instrumentedType = typeof(DbCommand); + instrumentedMethod = + MethodBuilder> + .Start(moduleVersionPtr, mdToken, opCode, nameof(ExecuteDbDataReader)) + .WithConcreteType(instrumentedType) + .WithParameters(commandBehavior) + .WithNamespaceAndNameFilters(SystemDataCommonDbDataReader, SystemDataCommonCommandBehavior) + .Build(); + } + catch (Exception ex) + { + Log.ErrorException($"Error resolving {SystemDataCommonDbCommand}.{nameof(ExecuteDbDataReader)}(...)", ex); + throw; + } - using (var scope = CreateScope(command)) + using (var scope = CreateScope((DbCommand)@this)) { try { - return executeReader(command, commandBehavior); + return instrumentedMethod(@this, commandBehavior); } - catch (Exception ex) when (scope?.Span.SetExceptionForFilter(ex) ?? false) + catch (Exception ex) { - // unreachable code + scope?.Span.SetException(ex); throw; } } @@ -81,15 +99,15 @@ public static object ExecuteDbDataReader( /// A pointer to the module version GUID. /// The value returned by the instrumented method. [InterceptMethod( - TargetAssembly = "System.Data", // .NET Framework - TargetType = "System.Data.Common.DbCommand", - TargetSignatureTypes = new[] { "System.Threading.Tasks.Task`1", "System.Data.CommandBehavior", "System.Threading.CancellationToken" }, + TargetAssembly = FrameworkAssembly, // .NET Framework + TargetType = SystemDataCommonDbCommand, + TargetSignatureTypes = new[] { "System.Threading.Tasks.Task`1", SystemDataCommonCommandBehavior, ClrNames.CancellationToken }, TargetMinimumVersion = Major4, TargetMaximumVersion = Major4)] [InterceptMethod( - TargetAssembly = "System.Data.Common", // .NET Core - TargetType = "System.Data.Common.DbCommand", - TargetSignatureTypes = new[] { "System.Threading.Tasks.Task`1", "System.Data.CommandBehavior", "System.Threading.CancellationToken" }, + TargetAssembly = CoreAssembly, // .NET Core + TargetType = SystemDataCommonDbCommand, + TargetSignatureTypes = new[] { "System.Threading.Tasks.Task`1", SystemDataCommonCommandBehavior, ClrNames.CancellationToken }, TargetMinimumVersion = Major4, TargetMaximumVersion = Major4)] public static object ExecuteDbDataReaderAsync( @@ -102,35 +120,51 @@ public static object ExecuteDbDataReaderAsync( { var tokenSource = cancellationTokenSource as CancellationTokenSource; var cancellationToken = tokenSource?.Token ?? CancellationToken.None; - - var command = (DbCommand)@this; + var instrumentedType = typeof(DbCommand); + var dataReaderType = typeof(DbDataReader); var commandBehavior = (CommandBehavior)behavior; - var callOpCode = (OpCodeValue)opCode; + Func instrumentedMethod = null; - return ExecuteDbDataReaderAsyncInternal(command, commandBehavior, cancellationToken, callOpCode); + try + { + instrumentedMethod = + MethodBuilder> + .Start(moduleVersionPtr, mdToken, opCode, nameof(ExecuteDbDataReaderAsync)) + .WithConcreteType(instrumentedType) + .WithParameters(commandBehavior, cancellationToken) + .WithNamespaceAndNameFilters(ClrNames.GenericTask, SystemDataCommonCommandBehavior, ClrNames.CancellationToken) + .Build(); + } + catch (Exception ex) + { + Log.ErrorException($"Error resolving {SystemDataCommonDbCommand}.{nameof(ExecuteDbDataReaderAsync)}(...)", ex); + throw; + } + + return AsyncHelper.InvokeGenericTaskDelegate( + owningType: instrumentedType, + taskResultType: dataReaderType, + nameOfIntegrationMethod: nameof(ExecuteDbDataReaderAsyncInternal), + integrationType: typeof(AdoNetIntegration), + parametersToPass: new object[] { @this, behavior, cancellationToken, instrumentedMethod }); } - private static async Task ExecuteDbDataReaderAsyncInternal( + private static async Task ExecuteDbDataReaderAsyncInternal( DbCommand command, CommandBehavior behavior, CancellationToken cancellationToken, - OpCodeValue callOpCode) + Func instrumentedMethod) { - var executeReader = Emit.DynamicMethodBuilder>> - .GetOrCreateMethodCallDelegate( - typeof(DbCommand), - "ExecuteDbDataReaderAsync", - callOpCode); - using (var scope = CreateScope(command)) { try { - return await executeReader(command, behavior, cancellationToken).ConfigureAwait(false); + var task = (Task)instrumentedMethod(command, behavior, cancellationToken); + return await task.ConfigureAwait(false); } - catch (Exception ex) when (scope?.Span.SetExceptionForFilter(ex) ?? false) + catch (Exception ex) { - // unreachable code + scope?.Span.SetException(ex); throw; } } diff --git a/test/Datadog.Trace.ClrProfiler.IntegrationTests/NpgSqlTests.cs b/test/Datadog.Trace.ClrProfiler.IntegrationTests/NpgSqlTests.cs index 1050618ea8e2..d4a98e810791 100644 --- a/test/Datadog.Trace.ClrProfiler.IntegrationTests/NpgSqlTests.cs +++ b/test/Datadog.Trace.ClrProfiler.IntegrationTests/NpgSqlTests.cs @@ -23,7 +23,7 @@ public void SubmitsTraces(string packageVersion) { Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode}"); - var spans = agent.WaitForSpans(1); + var spans = agent.WaitForSpans(3); Assert.True(spans.Count > 0, "expected at least one span"); foreach (var span in spans) { diff --git a/test/Datadog.Trace.ClrProfiler.IntegrationTests/SqlServerTests.cs b/test/Datadog.Trace.ClrProfiler.IntegrationTests/SqlServerTests.cs index 867105f666b3..8faa95c35288 100644 --- a/test/Datadog.Trace.ClrProfiler.IntegrationTests/SqlServerTests.cs +++ b/test/Datadog.Trace.ClrProfiler.IntegrationTests/SqlServerTests.cs @@ -26,7 +26,7 @@ public void SubmitsTraces(string packageVersion) { Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode}"); - var spans = agent.WaitForSpans(1); + var spans = agent.WaitForSpans(4); Assert.True(spans.Count > 0, "expected at least one span"); foreach (var span in spans) {