Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Instrumentation.EntityFrameworkCore] Add Filter public API to enable filtering #1203

Merged
merged 8 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
akoken marked this conversation as resolved.
Show resolved Hide resolved
/// </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