diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index 097e255db3b..efaab3dc114 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -3,6 +3,8 @@ OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.EnableCo OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.EnableConnectionLevelAttributes.set -> void OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Enrich.get -> System.Action OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Enrich.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Filter.get -> System.Func +OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Filter.set -> void OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.RecordException.get -> bool OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.RecordException.set -> void OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForStoredProcedure.get -> bool diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md index 309777b215c..63b5dec1c1c 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md @@ -11,6 +11,8 @@ Released 2022-Oct-17 respectively to set activity status. ([#3118](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3118)) ([#3751](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3751)) +* Add support for Filter option for non .NET Framework Targets + ([#3743](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3743)) ## 1.0.0-rc9.7 diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs index aa6f2ccc4e3..b383ab03896 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs @@ -80,6 +80,24 @@ public override void OnEventWritten(string name, object payload) if (activity.IsAllDataRequested) { + try + { + if (this.options.Filter?.Invoke(command) == false) + { + SqlClientInstrumentationEventSource.Log.CommandIsFilteredOut(activity.OperationName); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + } + catch (Exception ex) + { + SqlClientInstrumentationEventSource.Log.CommandFilterException(ex); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + _ = this.connectionFetcher.TryFetch(command, out var connection); _ = this.databaseFetcher.TryFetch(connection, out var database); diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs index 75e4c7f55af..2011b4b74bf 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs @@ -75,5 +75,26 @@ public void EnrichmentException(string exception) { this.WriteEvent(5, exception); } + + [Event(6, Message = "Command is filtered out. Activity {0}", Level = EventLevel.Verbose)] + public void CommandIsFilteredOut(string activityName) + { + this.WriteEvent(6, activityName); + } + + [NonEvent] + public void CommandFilterException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.CommandFilterException(ex.ToInvariantString()); + } + } + + [Event(7, Message = "Command filter threw exception. Command will not be collected. Exception {0}.", Level = EventLevel.Error)] + public void CommandFilterException(string exception) + { + this.WriteEvent(7, exception); + } } } diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/README.md b/src/OpenTelemetry.Instrumentation.SqlClient/README.md index 82005adc737..7999dfe91a5 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/README.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/README.md @@ -221,6 +221,37 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .Build(); ``` +## Filter + +This option allows to filter out activities based on the properties of the +`SqlCommand` object being instrumented using a `Func`. +The function receives an instance of the raw `SqlCommand` and should return +`true` if the telemetry is to be collected, and `false` if it should not. +The parameter of the Func delegate is of type `object` and needs to +be cast to the appropriate type of `SqlCommand`, either +`Microsoft.Data.SqlClient.SqlCommand` or `System.Data.SqlClient.SqlCommand`. +The example below filters out all commands that are not stored procedures. + +```csharp +using var traceProvider = Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + opt => + { + opt.Filter = cmd => + { + if (cmd is SqlCommand command) + { + return command.CommandType == CommandType.StoredProcedure; + } + + return false; + }; + }) + .AddConsoleExporter() + .Build(); +{ +``` + ## References * [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentationOptions.cs index d3f2fd1ab1f..3074bf2b372 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentationOptions.cs @@ -130,6 +130,18 @@ public class SqlClientInstrumentationOptions public Action Enrich { get; set; } #if !NETFRAMEWORK + /// + /// Gets or sets a Filter function that determines whether or not to collect telemetry about a command + /// The Filter gets the SqlCommand, and should return a boolean. + /// If Filter returns true, the request is collected. + /// If Filter returns false or throw exception, the request is filtered out. + /// + /// + /// object: the raw SqlCommand object to interrogate for a decision on whether to trace or not. + /// + /// true to collect request, false to filter out. + public Func Filter { get; set; } + /// /// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. Default value: False. /// diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs index 68cc737246d..7164ba8d18e 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs @@ -15,6 +15,7 @@ // using System; +using System.Collections; using System.Collections.Generic; using System.Data; using System.Diagnostics; @@ -308,6 +309,63 @@ public void SqlClientCreatesActivityWithDbSystem( VerifySamplingParameters(sampler.LatestSamplingParameters); } + + [Fact] + public void ShouldCollectTelemetryWhenFilterEvaluatesToTrue() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 2"; + }, + cmd => + { + if (cmd is SqlCommand command) + { + return command.CommandText == "select 2"; + } + + return true; + }); + + Assert.Single(activities); + Assert.True(activities[0].IsAllDataRequested); + Assert.True(activities[0].ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + } + + [Fact] + public void ShouldNotCollectTelemetryWhenFilterEvaluatesToFalse() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 1"; + }, + cmd => + { + if (cmd is SqlCommand command) + { + return command.CommandText == "select 2"; + } + + return true; + }); + + Assert.Empty(activities); + } + + [Fact] + public void ShouldNotCollectTelemetryAndShouldNotPropagateExceptionWhenFilterThrowsException() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 1"; + }, + cmd => throw new InvalidOperationException("foobar")); + + Assert.Empty(activities); + } #endif private static void VerifyActivityData( @@ -414,6 +472,52 @@ private static void ActivityEnrichment(Activity activity, string method, object } } +#if !NETFRAMEWORK + private Activity[] RunCommandWithFilter(Action sqlCommandSetup, Func filter) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); + + var activities = new List(); + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + options => + { + options.Filter = filter; + }) + .AddInMemoryExporter(activities) + .Build()) + { + var operationId = Guid.NewGuid(); + sqlCommandSetup(sqlCommand); + + var beforeExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = (long?)1000000L, + }; + + this.fakeSqlClientDiagnosticSource.Write( + SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, + beforeExecuteEventData); + + var afterExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = 2000000L, + }; + + this.fakeSqlClientDiagnosticSource.Write( + SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, + afterExecuteEventData); + } + + return activities.ToArray(); + } +#endif + private class FakeSqlClientDiagnosticSource : IDisposable { private readonly DiagnosticListener listener;