Skip to content

Commit

Permalink
[SqlClient] Add support for Filter expression (#3743)
Browse files Browse the repository at this point in the history
  • Loading branch information
Driedas authored Oct 18, 2022
1 parent 1a2103d commit 3f53be6
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.EnableCo
OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.EnableConnectionLevelAttributes.set -> void
OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Enrich.get -> System.Action<System.Diagnostics.Activity, string, object>
OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Enrich.set -> void
OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Filter.get -> System.Func<object, bool>
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
Expand Down
2 changes: 2 additions & 0 deletions src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
31 changes: 31 additions & 0 deletions src/OpenTelemetry.Instrumentation.SqlClient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<object, bool>`.
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/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ public class SqlClientInstrumentationOptions
public Action<Activity, string, object> Enrich { get; set; }

#if !NETFRAMEWORK
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>object: the raw <c>SqlCommand</c> object to interrogate for a decision on whether to trace or not.</para>
/// </remarks>
/// <returns>true to collect request, false to filter out.</returns>
public Func<object, bool> Filter { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. Default value: False.
/// </summary>
Expand Down
104 changes: 104 additions & 0 deletions test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// </copyright>

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -414,6 +472,52 @@ private static void ActivityEnrichment(Activity activity, string method, object
}
}

#if !NETFRAMEWORK
private Activity[] RunCommandWithFilter(Action<SqlCommand> sqlCommandSetup, Func<object, bool> filter)
{
using var sqlConnection = new SqlConnection(TestConnectionString);
using var sqlCommand = sqlConnection.CreateCommand();

var activities = new List<Activity>();
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;
Expand Down

0 comments on commit 3f53be6

Please sign in to comment.