Skip to content

Commit

Permalink
Instrument SqlCommand ExecuteXmlReader and ExecuteXmlReaderAsyn (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
pjanotti authored Jun 28, 2021
1 parent 215c5a4 commit 24fd7b6
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 2 deletions.
3 changes: 2 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## [Unreleased]

-
- Support for .NET 5.0
- New instrumentations for SqlCommand: ExecuteXmlReader and ExecuteXmlReaderAsync methods

[Commits](https://github.com/signalfx/signalfx-dotnet-tracing/v0.1.11...HEAD)

Expand Down
98 changes: 98 additions & 0 deletions integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,104 @@
{
"name": "SqlCommand",
"method_replacements": [
{
"caller": {},
"target": {
"assembly": "System.Data",
"type": "System.Data.SqlClient.SqlCommand",
"method": "ExecuteXmlReader",
"signature_types": [
"System.Xml.XmlReader"
],
"minimum_major": 4,
"minimum_minor": 0,
"minimum_patch": 0,
"maximum_major": 5,
"maximum_minor": 65535,
"maximum_patch": 65535
},
"wrapper": {
"assembly": "SignalFx.Tracing.ClrProfiler.Managed, Version=0.1.11.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
"type": "Datadog.Trace.ClrProfiler.Integrations.AdoNet.SqlCommandIntegration",
"method": "ExecuteXmlReader",
"signature": "00 04 1C 1C 08 08 0A",
"action": "ReplaceTargetMethod"
}
},
{
"caller": {},
"target": {
"assembly": "System.Data.SqlClient",
"type": "System.Data.SqlClient.SqlCommand",
"method": "ExecuteXmlReader",
"signature_types": [
"System.Xml.XmlReader"
],
"minimum_major": 4,
"minimum_minor": 0,
"minimum_patch": 0,
"maximum_major": 5,
"maximum_minor": 65535,
"maximum_patch": 65535
},
"wrapper": {
"assembly": "SignalFx.Tracing.ClrProfiler.Managed, Version=0.1.11.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
"type": "Datadog.Trace.ClrProfiler.Integrations.AdoNet.SqlCommandIntegration",
"method": "ExecuteXmlReader",
"signature": "00 04 1C 1C 08 08 0A",
"action": "ReplaceTargetMethod"
}
},
{
"caller": {},
"target": {
"assembly": "System.Data",
"type": "System.Data.SqlClient.SqlCommand",
"method": "ExecuteXmlReaderAsync",
"signature_types": [
"System.Threading.Tasks.Task`1<System.Xml.XmlReader>",
"System.Threading.CancellationToken"
],
"minimum_major": 4,
"minimum_minor": 0,
"minimum_patch": 0,
"maximum_major": 5,
"maximum_minor": 65535,
"maximum_patch": 65535
},
"wrapper": {
"assembly": "SignalFx.Tracing.ClrProfiler.Managed, Version=0.1.11.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
"type": "Datadog.Trace.ClrProfiler.Integrations.AdoNet.SqlCommandIntegration",
"method": "ExecuteXmlReaderAsync",
"signature": "00 05 1C 1C 1C 08 08 0A",
"action": "ReplaceTargetMethod"
}
},
{
"caller": {},
"target": {
"assembly": "System.Data.SqlClient",
"type": "System.Data.SqlClient.SqlCommand",
"method": "ExecuteXmlReaderAsync",
"signature_types": [
"System.Threading.Tasks.Task`1<System.Xml.XmlReader>",
"System.Threading.CancellationToken"
],
"minimum_major": 4,
"minimum_minor": 0,
"minimum_patch": 0,
"maximum_major": 5,
"maximum_minor": 65535,
"maximum_patch": 65535
},
"wrapper": {
"assembly": "SignalFx.Tracing.ClrProfiler.Managed, Version=0.1.11.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
"type": "Datadog.Trace.ClrProfiler.Integrations.AdoNet.SqlCommandIntegration",
"method": "ExecuteXmlReaderAsync",
"signature": "00 05 1C 1C 1C 08 08 0A",
"action": "ReplaceTargetMethod"
}
},
{
"caller": {},
"target": {
Expand Down
6 changes: 6 additions & 0 deletions samples/Samples.SqlServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ private static async Task Main()
{
using (var root = Tracer.Instance.StartActive("root"))
{
using (var connection = CreateConnection())
{
var xmlQueries = new SqlClientXmlQueries(connection);
await xmlQueries.RunAsync().ConfigureAwait(false);
}

using (var connection = CreateConnection())
{
var testQueries = new RelationalDatabaseTestHarness<SqlConnection, SqlCommand, SqlDataReader>(
Expand Down
87 changes: 87 additions & 0 deletions samples/Samples.SqlServer/SqlClientXmlQueries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using SignalFx.Tracing;

namespace Samples.SqlServer
{
public class SqlClientXmlQueries
{
private const string DropAndCreateCommandText =
"DROP TABLE IF EXISTS Cities; CREATE TABLE Cities (Id int PRIMARY KEY, Name varchar(100));";
private const string InsertCommandText =
"INSERT INTO Cities (Id, Name) VALUES (0, 'Seattle');INSERT INTO Cities (Id, Name) VALUES (1, 'Renton');";
private const string SelectXmlCommandText =
"SELECT * FROM Cities FOR XML AUTO, XMLDATA;";

private readonly IDbConnection _connection;

public SqlClientXmlQueries(IDbConnection connection)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
}

public async Task RunAsync()
{
using (var scopeAll = Tracer.Instance.StartActive("sql.client.xml"))
{
scopeAll.Span.SetTag("command-type", typeof(SqlCommand).FullName);

_connection.Open();
SetupTableForXmlQueries(_connection);
SelectXml(_connection);
await SelectXmlAsync(_connection).ConfigureAwait(false);
_connection.Close();
}
}

private static void SetupTableForXmlQueries(IDbConnection connection)
{
using (var command = (SqlCommand)connection.CreateCommand())
{
command.CommandText = DropAndCreateCommandText;

int records = command.ExecuteNonQuery();
Console.WriteLine($"Dropped and recreated table for XML queries. {records} record(s) affected.");

command.CommandText = InsertCommandText;
records = command.ExecuteNonQuery();
Console.WriteLine($"Inserted {records} record(s).");
}
}

private static void SelectXml(IDbConnection connection)
{
using (var command = (SqlCommand)connection.CreateCommand())
{
command.CommandText = SelectXmlCommandText;
var xmlReader = command.ExecuteXmlReader();
Console.WriteLine("Selected XML data synchronously: " + XmlReaderToString(xmlReader));
}
}

private static async Task SelectXmlAsync(IDbConnection connection)
{
using (var command = (SqlCommand)connection.CreateCommand())
{
command.CommandText = SelectXmlCommandText;
var xmlReader = await command.ExecuteXmlReaderAsync().ConfigureAwait(false);
Console.WriteLine("Selected XML data asynchronously: " + XmlReaderToString(xmlReader));
}
}

private static string XmlReaderToString(XmlReader xmlReader)
{
var sb = new StringBuilder();
while (xmlReader.Read())
{
sb.Append(xmlReader.ReadOuterXml());
}

return sb.ToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
using System;
using System.Data;
using System.Data.Common;
#if NETFRAMEWORK
using System.Data.SqlClient;
#endif
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Datadog.Trace.ClrProfiler.Emit;
using SignalFx.Tracing;
using SignalFx.Tracing.Logging;
Expand All @@ -22,8 +26,140 @@ public static class SqlCommandIntegration
private const string SqlCommandTypeName = "System.Data.SqlClient.SqlCommand";
private const string SqlDataReaderTypeName = "System.Data.SqlClient.SqlDataReader";

// ExceuteXmlReader and ExceuteXmlReaderAsync are only available on SqlCommand.
private const string ExecuteXmlReaderMethodName = "ExecuteXmlReader";
private const string ExecuteXmlReaderAsyncMethodName = "ExecuteXmlReaderAsync";
private const string XmlReaderTypeName = "System.Xml.XmlReader";

private static readonly ILog Log = LogProvider.GetCurrentClassLogger();

/// <summary>
/// Instrumentation wrapper for SqlCommand.ExecuteXmlReader().
/// </summary>
/// <param name="command">The object referenced by this in the instrumented method.</param>
/// <param name="opCode">The OpCode used in the original method call.</param>
/// <param name="mdToken">The mdToken of the original method call.</param>
/// <param name="moduleVersionPtr">A pointer to the module version GUID.</param>
/// <returns>The value returned by the instrumented method.</returns>
[InterceptMethod(
TargetAssemblies = new[] { AdoNetConstants.AssemblyNames.SystemData, AdoNetConstants.AssemblyNames.SystemDataSqlClient },
TargetType = SqlCommandTypeName,
TargetMethod = ExecuteXmlReaderMethodName,
TargetSignatureTypes = new[] { XmlReaderTypeName },
TargetMinimumVersion = Major4,
TargetMaximumVersion = Major5)]
public static object ExecuteXmlReader(
object command,
int opCode,
int mdToken,
long moduleVersionPtr)
{
Func<object, object> instrumentedMethod;

try
{
var targetType = command.GetInstrumentedType(SqlCommandTypeName);

instrumentedMethod =
MethodBuilder<Func<object, object>>
.Start(moduleVersionPtr, mdToken, opCode, ExecuteXmlReaderMethodName)
.WithConcreteType(targetType)
.WithNamespaceAndNameFilters(XmlReaderTypeName)
.Build();
}
catch (Exception ex)
{
Log.ErrorException($"Error resolving {SqlCommandTypeName}.{ExecuteXmlReaderMethodName}(...)", ex);
throw;
}

using (var scope = ScopeFactory.CreateDbCommandScope(Tracer.Instance, command as DbCommand, IntegrationName))
{
try
{
return instrumentedMethod(command);
}
catch (Exception ex)
{
scope?.Span.SetException(ex);
throw;
}
}
}

/// <summary>
/// Instrumentation wrapper for SqlCommand.ExecuteXmlReaderAsync().
/// </summary>
/// <param name="command">The object referenced by this in the instrumented method.</param>
/// <param name="boxedCancellationToken">The <see cref="CancellationToken"/> value used in the original method call.</param>
/// <param name="opCode">The OpCode used in the original method call.</param>
/// <param name="mdToken">The mdToken of the original method call.</param>
/// <param name="moduleVersionPtr">A pointer to the module version GUID.</param>
/// <returns>The value returned by the instrumented method.</returns>
[InterceptMethod(
TargetAssemblies = new[] { AdoNetConstants.AssemblyNames.SystemData, AdoNetConstants.AssemblyNames.SystemDataSqlClient },
TargetType = SqlCommandTypeName,
TargetSignatureTypes = new[] { "System.Threading.Tasks.Task`1<System.Xml.XmlReader>", ClrNames.CancellationToken },
TargetMinimumVersion = Major4,
TargetMaximumVersion = Major5)]
public static object ExecuteXmlReaderAsync(
object command,
object boxedCancellationToken,
int opCode,
int mdToken,
long moduleVersionPtr)
{
var cancellationToken = (CancellationToken)boxedCancellationToken;

return ExecuteXmlReaderAsyncInternal(
(DbCommand)command,
cancellationToken,
opCode,
mdToken,
moduleVersionPtr);
}

private static async Task<XmlReader> ExecuteXmlReaderAsyncInternal(
DbCommand command,
CancellationToken cancellationToken,
int opCode,
int mdToken,
long moduleVersionPtr)
{
Func<DbCommand, CancellationToken, Task<XmlReader>> instrumentedMethod;

try
{
var targetType = command.GetInstrumentedType(SqlCommandTypeName);

instrumentedMethod =
MethodBuilder<Func<DbCommand, CancellationToken, Task<XmlReader>>>
.Start(moduleVersionPtr, mdToken, opCode, nameof(ExecuteXmlReaderAsync))
.WithConcreteType(targetType)
.WithParameters(cancellationToken)
.WithNamespaceAndNameFilters(ClrNames.GenericTask, ClrNames.CancellationToken)
.Build();
}
catch (Exception ex)
{
Log.ErrorException($"Error resolving {SqlCommandTypeName}.{ExecuteXmlReaderAsyncMethodName}(...)", ex);
throw;
}

using (var scope = ScopeFactory.CreateDbCommandScope(Tracer.Instance, command, IntegrationName))
{
try
{
return await instrumentedMethod(command, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
scope?.Span.SetException(ex);
throw;
}
}
}

/// <summary>
/// Instrumentation wrapper for SqlCommand.ExecuteReader().
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public SqlCommandTests(ITestOutputHelper output)
[Trait("RunOnWindows", "True")]
public void SubmitsTraces(string packageVersion)
{
const int expectedSpanCount = 35;
const int expectedSpanCount = 39;
const string dbType = "sql-server";
const string expectedOperationName = dbType + ".query";
const string expectedServiceName = "Samples.SqlServer";
Expand Down

0 comments on commit 24fd7b6

Please sign in to comment.