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..08239eac5d 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 e6428d823d..c206a60399 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Added `Filter` public API on `EntityFrameworkInstrumentationOptions` to enable filtering + of instrumentation based on the db command being executed. + ## 1.0.0-beta.6 Released 2023-Mar-13 diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/EntityFrameworkInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/EntityFrameworkInstrumentationOptions.cs index 722221306f..4d05651cec 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/EntityFrameworkInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/EntityFrameworkInstrumentationOptions.cs @@ -44,4 +44,24 @@ 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. + /// + /// + /// Notes: + /// + /// The first 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..c884923a7e 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs @@ -179,6 +179,24 @@ public override void OnCustom(string name, Activity activity, object payload) { var command = this.commandFetcher.Fetch(payload); + try + { + if (command is IDbCommand typedCommand && this.options.Filter?.Invoke(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..dc30baf662 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/README.md +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/README.md @@ -112,6 +112,28 @@ services.AddOpenTelemetry() .AddConsoleExporter()); ``` +### Filter + +This option can be used to filter out activities based on the properties of the +db command object being instrumented using a `Func`. The +function receives 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 = (command) => + { + return command.CommandType == System.Data.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..18dbf02586 100644 --- a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/EntityFrameworkDiagnosticListenerTests.cs +++ b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/EntityFrameworkDiagnosticListenerTests.cs @@ -131,6 +131,60 @@ public void EntityFrameworkContextExceptionEventsInstrumentedTest() VerifyActivityData(activity, isError: true); } + [Fact] + public void ShouldCollectTelemetryWhenFilterEvaluatesToTrue() + { + var activityProcessor = new Mock>(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddEntityFrameworkCoreInstrumentation(options => + { + options.Filter = (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)); + } + + [Fact] + public void ShouldCollectTelemetryWhenFilterEvaluatesToFalse() + { + var activityProcessor = new Mock>(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddEntityFrameworkCoreInstrumentation(options => + { + options.Filter = (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)); + } + public void Dispose() => this.connection.Dispose(); private static DbConnection CreateInMemoryDatabase()