diff --git a/src/OpenTracing.Contrib.NetCore/Internal/PropertyFetcher.cs b/src/OpenTracing.Contrib.NetCore/Internal/PropertyFetcher.cs index a1dd855..1c7281a 100644 --- a/src/OpenTracing.Contrib.NetCore/Internal/PropertyFetcher.cs +++ b/src/OpenTracing.Contrib.NetCore/Internal/PropertyFetcher.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace OpenTracing.Contrib.NetCore.Internal @@ -33,6 +34,7 @@ public object Fetch(object obj) _fetchForExpectedType = PropertyFetch.FetcherForProperty(propertyInfo); _expectedType = objType; } + return _fetchForExpectedType.Fetch(obj); } @@ -51,13 +53,18 @@ private class PropertyFetch public static PropertyFetch FetcherForProperty(PropertyInfo propertyInfo) { if (propertyInfo == null) - return new PropertyFetch(); // returns null on any fetch. + return new PropertyFetch(); // returns null on any fetch. Type typedPropertyFetcher = typeof(TypedFetchProperty<,>); Type instantiatedTypedPropertyFetcher = typedPropertyFetcher.GetTypeInfo().MakeGenericType( propertyInfo.DeclaringType, propertyInfo.PropertyType); - return (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, propertyInfo); + Type propertyInfoType = typeof(PropertyInfo); + ConstructorInfo ctor = instantiatedTypedPropertyFetcher.GetConstructor(new[] {propertyInfoType}); + ParameterExpression ctorParameter = Expression.Parameter(propertyInfoType); + Delegate expression = Expression.Lambda(Expression.New(ctor, ctorParameter), ctorParameter) + .Compile(); + return (PropertyFetch)expression.DynamicInvoke(propertyInfo); } /// @@ -74,10 +81,12 @@ public TypedFetchProperty(PropertyInfo property) { _propertyFetch = (Func)property.GetMethod.CreateDelegate(typeof(Func)); } + public override object Fetch(object obj) { return _propertyFetch((TObject)obj); } + private readonly Func _propertyFetch; } } diff --git a/src/OpenTracing.Contrib.NetCore/MicrosoftSqlClient/MicrosoftSqlClientDiagnostics.cs b/src/OpenTracing.Contrib.NetCore/MicrosoftSqlClient/MicrosoftSqlClientDiagnostics.cs index e3be713..f5434b2 100644 --- a/src/OpenTracing.Contrib.NetCore/MicrosoftSqlClient/MicrosoftSqlClientDiagnostics.cs +++ b/src/OpenTracing.Contrib.NetCore/MicrosoftSqlClient/MicrosoftSqlClientDiagnostics.cs @@ -14,17 +14,27 @@ internal sealed class MicrosoftSqlClientDiagnostics : DiagnosticEventObserver { public const string DiagnosticListenerName = "SqlClientDiagnosticListener"; - private static readonly PropertyFetcher _activityCommand_RequestFetcher = new PropertyFetcher("Command"); - private static readonly PropertyFetcher _exception_ExceptionFetcher = new PropertyFetcher("Exception"); + private static Func CommandFetcherFactoryMethod => + _ => new PropertyFetcher("Command"); + + private static Func ExceptionFetcherFactoryMethod => + _ => new PropertyFetcher("Exception"); private readonly MicrosoftSqlClientDiagnosticOptions _options; private readonly ConcurrentDictionary _spanStorage; - - public MicrosoftSqlClientDiagnostics(ILoggerFactory loggerFactory, ITracer tracer, IOptions options) - : base(loggerFactory, tracer, options?.Value) + private readonly ConcurrentDictionary _activityCommandFetchers; + private readonly ConcurrentDictionary _exceptionFetchers; + + public MicrosoftSqlClientDiagnostics( + ILoggerFactory loggerFactory, + ITracer tracer, + IOptions options) + : base(loggerFactory, tracer, options?.Value) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _spanStorage = new ConcurrentDictionary(); + _activityCommandFetchers = new ConcurrentDictionary(); + _exceptionFetchers = new ConcurrentDictionary(); } protected override string GetListenerName() => DiagnosticListenerName; @@ -44,68 +54,76 @@ protected override IEnumerable HandledEventNames() protected override void HandleEvent(string eventName, object untypedArg) { + var untypedArgType = untypedArg.GetType(); switch (eventName) { case MicrosoftSqlClientDiagnosticOptions.EventNames.WriteCommandBefore: - { - var cmd = (SqlCommand)_activityCommand_RequestFetcher.Fetch(untypedArg); + { + var commandFetcher = _activityCommandFetchers.GetOrAdd(untypedArgType, CommandFetcherFactoryMethod); + var cmd = (SqlCommand)commandFetcher.Fetch(untypedArg); - var activeSpan = Tracer.ActiveSpan; + var activeSpan = Tracer.ActiveSpan; - if (activeSpan == null && !_options.StartRootSpans) + if (activeSpan == null && !_options.StartRootSpans) + { + if (IsLogLevelTraceEnabled) { - if (IsLogLevelTraceEnabled) - { - Logger.LogTrace("Ignoring SQL command due to missing parent span"); - } - return; + Logger.LogTrace("Ignoring SQL command due to missing parent span"); } - if (IgnoreEvent(cmd)) + return; + } + + if (IgnoreEvent(cmd)) + { + if (IsLogLevelTraceEnabled) { - if (IsLogLevelTraceEnabled) - { - Logger.LogTrace("Ignoring SQL command due to IgnorePatterns"); - } - return; + Logger.LogTrace("Ignoring SQL command due to IgnorePatterns"); } - string operationName = _options.OperationNameResolver(cmd); + return; + } - var span = Tracer.BuildSpan(operationName) - .AsChildOf(activeSpan) - .WithTag(Tags.SpanKind, Tags.SpanKindClient) - .WithTag(Tags.Component, _options.ComponentName) - .WithTag(Tags.DbInstance, cmd.Connection.Database) - .WithTag(Tags.DbStatement, cmd.CommandText) - .Start(); + string operationName = _options.OperationNameResolver(cmd); - _spanStorage.TryAdd(cmd, span); - } + var span = Tracer.BuildSpan(operationName) + .AsChildOf(activeSpan) + .WithTag(Tags.SpanKind, Tags.SpanKindClient) + .WithTag(Tags.Component, _options.ComponentName) + .WithTag(Tags.DbInstance, cmd.Connection.Database) + .WithTag(Tags.DbStatement, cmd.CommandText) + .Start(); + + _spanStorage.TryAdd(cmd, span); + } break; case MicrosoftSqlClientDiagnosticOptions.EventNames.WriteCommandError: - { - var cmd = (SqlCommand)_activityCommand_RequestFetcher.Fetch(untypedArg); - var ex = (Exception)_exception_ExceptionFetcher.Fetch(untypedArg); + { + var commandFetcher = _activityCommandFetchers[untypedArgType]; + var cmd = (SqlCommand)commandFetcher.Fetch(untypedArg); - if (_spanStorage.TryRemove(cmd, out var span)) - { - span.SetException(ex); - span.Finish(); - } + var exceptionFetcher = _exceptionFetchers.GetOrAdd(untypedArgType, ExceptionFetcherFactoryMethod); + var ex = (Exception)exceptionFetcher.Fetch(untypedArg); + + if (_spanStorage.TryRemove(cmd, out var span)) + { + span.SetException(ex); + span.Finish(); } + } break; case MicrosoftSqlClientDiagnosticOptions.EventNames.WriteCommandAfter: - { - var cmd = (SqlCommand)_activityCommand_RequestFetcher.Fetch(untypedArg); + { + var commandFetcher = _activityCommandFetchers[untypedArgType]; + var cmd = (SqlCommand)commandFetcher.Fetch(untypedArg); - if (_spanStorage.TryRemove(cmd, out var span)) - { - span.Finish(); - } + if (_spanStorage.TryRemove(cmd, out var span)) + { + span.Finish(); } + } break; default: diff --git a/test/OpenTracing.Contrib.NetCore.Tests/MicrosoftSqlClient/HandleEventTest.cs b/test/OpenTracing.Contrib.NetCore.Tests/MicrosoftSqlClient/HandleEventTest.cs new file mode 100644 index 0000000..3309fdb --- /dev/null +++ b/test/OpenTracing.Contrib.NetCore.Tests/MicrosoftSqlClient/HandleEventTest.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using OpenTracing.Contrib.NetCore.Configuration; +using OpenTracing.Contrib.NetCore.MicrosoftSqlClient; +using OpenTracing.Mock; +using OpenTracing.Noop; +using Xunit; + +namespace OpenTracing.Contrib.NetCore.Tests.MicrosoftSqlClient +{ + public class HandleEventTest + { + private readonly IObserver> _microsoftSqlClientDiagnostics; + + public HandleEventTest() + { + _microsoftSqlClientDiagnostics = new MicrosoftSqlClientDiagnostics( + NullLoggerFactory.Instance, + NoopTracerFactory.Create(), + Options.Create(new MicrosoftSqlClientDiagnosticOptions {StartRootSpans = false})); + } + + [Fact] + async Task CanHandleTwoDifferentTypesOfWriteCommandBeforeInParallel() + { + const string eventName = MicrosoftSqlClientDiagnosticOptions.EventNames.WriteCommandBefore; + var command1 = new {Command = new SqlCommand("Insert into"), Id = Guid.NewGuid()}; + var command2 = new {Command = new SqlCommand("Update where"), Id = Guid.NewGuid(), ExtraProp = "any value"}; + var kv1 = new KeyValuePair(eventName, command1); + var kv2 = new KeyValuePair(eventName, command2); + + var tasks1 = Enumerable.Range(0, 100) + .Select(i => Task.Run(() => _microsoftSqlClientDiagnostics.OnNext(i % 2 == 0 ? kv1 : kv2))); + + await Task.WhenAll(tasks1); + } + + [Fact] + void CanHandleWriteCommandAfter() + { + const string eventNameBefore = MicrosoftSqlClientDiagnosticOptions.EventNames.WriteCommandBefore; + var commandBefore = new {Command = new SqlCommand("Insert into"), Id = Guid.NewGuid()}; + var kvBefore = new KeyValuePair(eventNameBefore, commandBefore); + _microsoftSqlClientDiagnostics.OnNext(kvBefore); + + const string eventNameAfter = MicrosoftSqlClientDiagnosticOptions.EventNames.WriteCommandAfter; + var commandAfter = new {Command = new SqlCommand("Insert into"), Id = Guid.NewGuid()}; + var kvAfter = new KeyValuePair(eventNameAfter, commandAfter); + _microsoftSqlClientDiagnostics.OnNext(kvAfter); + } + } +} diff --git a/test/OpenTracing.Contrib.NetCore.Tests/OpenTracing.Contrib.NetCore.Tests.csproj b/test/OpenTracing.Contrib.NetCore.Tests/OpenTracing.Contrib.NetCore.Tests.csproj index ce1fedb..e91f663 100644 --- a/test/OpenTracing.Contrib.NetCore.Tests/OpenTracing.Contrib.NetCore.Tests.csproj +++ b/test/OpenTracing.Contrib.NetCore.Tests/OpenTracing.Contrib.NetCore.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.1 + netcoreapp2.1 @@ -17,6 +17,7 @@ + @@ -24,6 +25,10 @@ + + + +