Skip to content

Commit

Permalink
[Instrumentation.EntityFrameworkCore] Add Filter public API to enable…
Browse files Browse the repository at this point in the history
… filtering (#1203)
  • Loading branch information
akoken authored Jun 16, 2023
1 parent 326d071 commit 68e60db
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions
OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions.EntityFrameworkInstrumentationOptions() -> void
OpenTelemetry.Instrumentation.EntityFrameworkCore.EntityFrameworkInstrumentationOptions.Filter.get -> System.Func<string, System.Data.IDbCommand, bool>
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,25 @@ public class EntityFrameworkInstrumentationOptions
/// <para><see cref="IDbCommand"/>: db command to allow access to command.</para>
/// </remarks>
public Action<Activity, IDbCommand> EnrichWithIDbCommand { get; set; }

/// <summary>
/// Gets or sets a filter function that determines whether or not to
/// collect telemetry about a command from a particular provider.
/// </summary>
/// <remarks>
/// <b>Notes:</b>
/// <list type="bullet">
/// <item>The first parameter passed to the filter function is the provider name.</item>
/// <item>The second parameter passed to the filter function is <see cref="IDbCommand"/> from which additional
/// information can be extracted.</item>
/// <item>The return value for the filter:
/// <list type="number">
/// <item>If filter returns <see langword="true" />, the command is
/// collected.</item>
/// <item>If filter returns <see langword="false" /> or throws an
/// exception, the command is <b>NOT</b> collected.</item>
/// </list></item>
/// </list>
/// </remarks>
public Func<string, IDbCommand, bool> Filter { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
}
}
24 changes: 24 additions & 0 deletions src/OpenTelemetry.Instrumentation.EntityFrameworkCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, IDbCommand, bool>`. 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/)
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,132 @@ public void EntityFrameworkContextExceptionEventsInstrumentedTest()
VerifyActivityData(activity, isError: true);
}

[Fact]
public void ShouldNotCollectTelemetryWhenFilterEvaluatesToFalseByDbCommand()
{
var activityProcessor = new Mock<BaseProcessor<Activity>>();
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<Item>().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<BaseProcessor<Activity>>();
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<Item>().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<BaseProcessor<Activity>>();
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<Item>().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<BaseProcessor<Activity>>();
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<Item>().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()
Expand Down

0 comments on commit 68e60db

Please sign in to comment.