diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e1de3a6e74..79d38853ff 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,5 +1,7 @@ OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions.EntityFrameworkInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions.Filter.get -> System.Func +OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions.Filter.set -> void OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions.SetDbStatementForStoredProcedure.get -> bool OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions.SetDbStatementForStoredProcedure.set -> void OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions.SetDbStatementForText.get -> bool diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md index 85d11d800c..bfebdc306c 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Added `Filter` public API on `EntityFrameworkInstrumentationOptions` to + enable filtering of instrumentation. + ([#1203](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1203)) + ## 1.0.0-beta.7 Released 2023-Jun-09 diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/EntityFrameworkInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/EntityFrameworkInstrumentationOptions.cs index 722221306f..82a7f36cd1 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/EntityFrameworkInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/EntityFrameworkInstrumentationOptions.cs @@ -44,4 +44,25 @@ public class EntityFrameworkInstrumentationOptions /// : db command to allow access to command. /// public Action EnrichWithIDbCommand { get; set; } + + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry about a command from a particular provider. + /// + /// + /// Notes: + /// + /// The first parameter passed to the filter function is the provider name. + /// The second parameter passed to the filter function is from which additional + /// information can be extracted. + /// The return value for the filter: + /// + /// If filter returns , the command is + /// collected. + /// If filter returns or throws an + /// exception, the command is NOT collected. + /// + /// + /// + public Func Filter { get; set; } } diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs index 1740e012c6..0e9cc1e622 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs @@ -179,6 +179,28 @@ public override void OnCustom(string name, Activity activity, object payload) { var command = this.commandFetcher.Fetch(payload); + try + { + var dbContext = this.dbContextFetcher.Fetch(payload); + var dbContextDatabase = this.dbContextDatabaseFetcher.Fetch(dbContext); + var providerName = this.providerNameFetcher.Fetch(dbContextDatabase); + + if (command is IDbCommand typedCommand && this.options.Filter?.Invoke(providerName, typedCommand) == false) + { + EntityFrameworkInstrumentationEventSource.Log.CommandIsFilteredOut(activity.OperationName); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + } + catch (Exception ex) + { + EntityFrameworkInstrumentationEventSource.Log.CommandFilterException(ex); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + if (this.commandTypeFetcher.Fetch(command) is CommandType commandType) { var commandText = this.commandTextFetcher.Fetch(command); diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkInstrumentationEventSource.cs index 520c3063a3..4736821a16 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkInstrumentationEventSource.cs @@ -43,6 +43,15 @@ public void EnrichmentException(string eventName, Exception ex) } } + [NonEvent] + public void CommandFilterException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.CommandFilterException(ex.ToInvariantString()); + } + } + [Event(1, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) { @@ -75,4 +84,16 @@ public void EnrichmentException(string eventName, string exception) this.WriteEvent(5, eventName, exception); } } + + [Event(6, Message = "Command is filtered out. Activity {0}", Level = EventLevel.Verbose)] + public void CommandIsFilteredOut(string activityName) + { + this.WriteEvent(6, activityName); + } + + [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.EntityFrameworkCore/README.md b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/README.md index 48a3cd970b..aaca89c2ce 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/README.md +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/README.md @@ -112,6 +112,30 @@ services.AddOpenTelemetry() .AddConsoleExporter()); ``` +### Filter + +This option can be used to filter out activities based on the provider name and +the properties of the db command object being instrumented +using a `Func`. The function receives a provider name +and an instance of the db command and should return `true` +if the telemetry is to be collected, and `false` if it should not. + +The following code snippet shows how to use `Filter` to collect traces +for stored procedures only. + +```csharp +services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddEntityFrameworkCoreInstrumentation(options => + { + options.Filter = (providerName, command) => + { + return command.CommandType == CommandType.StoredProcedure; + }; + }) + .AddConsoleExporter()); +``` + ## References * [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/EntityFrameworkDiagnosticListenerTests.cs b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/EntityFrameworkDiagnosticListenerTests.cs index 02897938a7..bd8daf0c1f 100644 --- a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/EntityFrameworkDiagnosticListenerTests.cs +++ b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/EntityFrameworkDiagnosticListenerTests.cs @@ -131,6 +131,132 @@ public void EntityFrameworkContextExceptionEventsInstrumentedTest() VerifyActivityData(activity, isError: true); } + [Fact] + public void ShouldNotCollectTelemetryWhenFilterEvaluatesToFalseByDbCommand() + { + var activityProcessor = new Mock>(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddEntityFrameworkCoreInstrumentation(options => + { + options.Filter = (providerName, command) => + { + return !command.CommandText.Contains("Item", StringComparison.OrdinalIgnoreCase); + }; + }).Build(); + + using (var context = new ItemsContext(this.contextOptions)) + { + _ = context.Set().OrderBy(e => e.Name).ToList(); + } + + Assert.Equal(2, activityProcessor.Invocations.Count); + + var activity = (Activity)activityProcessor.Invocations[1].Arguments[0]; + + Assert.False(activity.IsAllDataRequested); + Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.None)); + } + + [Fact] + public void ShouldCollectTelemetryWhenFilterEvaluatesToTrueByDbCommand() + { + var activityProcessor = new Mock>(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddEntityFrameworkCoreInstrumentation(options => + { + options.Filter = (providerName, command) => + { + return command.CommandText.Contains("Item", StringComparison.OrdinalIgnoreCase); + }; + }).Build(); + + using (var context = new ItemsContext(this.contextOptions)) + { + _ = context.Set().OrderBy(e => e.Name).ToList(); + } + + Assert.Equal(3, activityProcessor.Invocations.Count); + + var activity = (Activity)activityProcessor.Invocations[1].Arguments[0]; + + Assert.True(activity.IsAllDataRequested); + Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + } + + [Theory] + [InlineData("Microsoft.EntityFrameworkCore.SqlServer")] + [InlineData("Microsoft.EntityFrameworkCore.Cosmos")] + [InlineData("Devart.Data.SQLite.EFCore")] + [InlineData("MySql.Data.EntityFrameworkCore")] + [InlineData("Pomelo.EntityFrameworkCore.MySql")] + [InlineData("Devart.Data.MySql.EFCore")] + [InlineData("Npgsql.EntityFrameworkCore.PostgreSQL")] + [InlineData("Devart.Data.PostgreSql.EFCore")] + [InlineData("Oracle.EntityFrameworkCore")] + [InlineData("Devart.Data.Oracle.EFCore")] + [InlineData("Microsoft.EntityFrameworkCore.InMemory")] + [InlineData("FirebirdSql.EntityFrameworkCore.Firebird")] + [InlineData("FileContextCore")] + [InlineData("EntityFrameworkCore.SqlServerCompact35")] + [InlineData("EntityFrameworkCore.SqlServerCompact40")] + [InlineData("EntityFrameworkCore.OpenEdge")] + [InlineData("EntityFrameworkCore.Jet")] + [InlineData("Google.Cloud.EntityFrameworkCore.Spanner")] + [InlineData("Teradata.EntityFrameworkCore")] + public void ShouldNotCollectTelemetryWhenFilterEvaluatesToFalseByProviderName(string provider) + { + var activityProcessor = new Mock>(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddEntityFrameworkCoreInstrumentation(options => + { + options.Filter = (providerName, command) => + { + return providerName.Equals(provider, StringComparison.OrdinalIgnoreCase); + }; + }).Build(); + + using (var context = new ItemsContext(this.contextOptions)) + { + _ = context.Set().OrderBy(e => e.Name).ToList(); + } + + Assert.Equal(2, activityProcessor.Invocations.Count); + + var activity = (Activity)activityProcessor.Invocations[1].Arguments[0]; + + Assert.False(activity.IsAllDataRequested); + Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.None)); + } + + [Fact] + public void ShouldCollectTelemetryWhenFilterEvaluatesToTrueByProviderName() + { + var activityProcessor = new Mock>(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddEntityFrameworkCoreInstrumentation(options => + { + options.Filter = (providerName, command) => + { + return providerName.Equals("Microsoft.EntityFrameworkCore.Sqlite", StringComparison.OrdinalIgnoreCase); + }; + }).Build(); + + using (var context = new ItemsContext(this.contextOptions)) + { + _ = context.Set().OrderBy(e => e.Name).ToList(); + } + + Assert.Equal(3, activityProcessor.Invocations.Count); + + var activity = (Activity)activityProcessor.Invocations[1].Arguments[0]; + Assert.True(activity.IsAllDataRequested); + Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + } + public void Dispose() => this.connection.Dispose(); private static DbConnection CreateInMemoryDatabase()