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

Allow custom filtering logic for FakeLogger #5848

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

Demo30
Copy link
Contributor

@Demo30 Demo30 commented Feb 5, 2025

Fixes #5615

Microsoft Reviewers: Open in CodeFlow

@Demo30
Copy link
Contributor Author

Demo30 commented Feb 5, 2025

@dotnet-policy-service agree company="Microsoft"

@dotnet-comment-bot
Copy link
Collaborator

‼️ Found issues ‼️

Project Coverage Type Expected Actual
Microsoft.Extensions.Caching.Hybrid Line 86 82.77 🔻
Microsoft.Gen.MetadataExtractor Line 98 57.35 🔻
Microsoft.Gen.MetadataExtractor Branch 98 62.5 🔻
Microsoft.Extensions.AI.Ollama Line 80 78.25 🔻

🎉 Good job! The coverage increased 🎉
Update MinCodeCoverage in the project files.

Project Expected Actual
Microsoft.Extensions.AI.Abstractions 83 84
Microsoft.Extensions.AI.OpenAI 77 78
Microsoft.Extensions.AI 88 89

Full code coverage report: https://dev.azure.com/dnceng-public/public/_build/results?buildId=941204&view=codecoverage-tab

/// If not empty, only records for which the filter function returns <see langword="true" /> will be collected by the fake logger.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.Telemetry)]
public IEnumerable<Func<FakeLogRecord, bool>> CustomFilters { get; set; } = Array.Empty<Func<FakeLogRecord, bool>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setter will allow setting the property to null. Is this desirable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Nullable is enabled, attempt to setting it to null issues a warning. It is true that by supplying null the logic ends up crashing in runtime, which is awkward, however this seems to be the case for e.g. TimeProvider prop as well.

How about defining it as nullable, defaulting to null and having a null check at the place of usage?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Nullable is enabled, attempt to setting it to null issues a warning.

"Nullable" only provides annotations which only work at compile-time. That is, those may result in warnings, however, they don't block the user code from assigning null (which results in NRE at runtime and may crash the app).

How about defining it as nullable, defaulting to null and having a null check at the place of usage?

It's definitely an option to expect a developer to implement null-checks, but since the API is annotated the good developer won't do that (because they trust us) and a mediocre developer won't do that (because they are mediocre). Either way, the likelihood of a user receiving an NRE is pretty high because our code isn't living up to the annotation and not handling nulls.

Null handling either implies throwing if null is assigned or setting (or just returning) an empty array (like we're doing for the init state). Which implementation better depends on usecases you're trying to address.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's want I meant, let's define the contract as nullable with null as default and handle the null gracefully on our side. This way the user will never reach NRE.

var options = new FakeLogCollectorOptions
{
CustomFilters = [
r => !r.Message.Equals(IgnoredMessage, StringComparison.Ordinal),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're using Ordinal comparison, then more idiomatic to write as follows:

Suggested change
r => !r.Message.Equals(IgnoredMessage, StringComparison.Ordinal),
r => r.Message != IgnoredMessage,

Comment on lines +132 to +139
foreach (var condition in _options.CustomFilters)
{
if (!condition(record))
{
// record was filtered out by a custom filter
return;
}
}
Copy link
Member

@RussKie RussKie Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this work?

Suggested change
foreach (var condition in _options.CustomFilters)
{
if (!condition(record))
{
// record was filtered out by a custom filter
return;
}
}
if (_options.CustomFilters.Any(filter => !filter(record))
{
// record was filtered out by a custom filter
return;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks sus - if any record hasn't been filtered (i.e., !filter(record)) then exit because "record was filtered out by a custom filter". Don't you think?

Copy link
Contributor Author

@Demo30 Demo30 Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ad Linq/foreach: 👍 will switch to Linq

ad !filter(): Do I understand correctly you would find a reversed semantic better? Currently the predicate evaluates whether the record should be allowed to pass through each filter, that is: "if the record fails to pass any of the filters, it is filtered out", when reversed, the predicate would evaluate whether the record should be caught by each filter, that is: "if the record is caught by any of the filters, it is filtered out" - both seem ok to me, but since the usage will more likely be to "filter this and that out", the reversed semantic might work better

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RE: linq vs not-linq

It's may worth checking perf switching to linq. If this is a hot path, then non-linq version may behave better. ¯\_(ツ)_/¯

the reversed semantic might work better

To me the current implementation looks confusing to me (which means it'll likely confuse others)...
I sense the predicate's logic is inverse - i.e., "is the record caught by the filter => false", whereas perhaps it should be "is the record handled/filtered by the filter => true". This will remove the inversion in the iterator, i.e., if (_options.CustomFilters.Any(filter)) return true.

WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea under the issue was that only those records will be collected that are allowed by the filter, i.e. those that the filter returns true for (and if not filter is specified, then all records are allowed). This is following the logic of the other filtering here above: e.g., if FilteredLevels contains items, then only those records that have the level contained there will be allowed.

So, what's currently in the PR is IMO correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inverse was proposed in this comment: #5848 (comment)

I think both versions are possible, but I suggested going with the original proposal as that seems to be more widely used (e.g.: .Where in LINQ, .filter in javascript, stream.filter in Java) and also consistent with the behavior of the other filters in the same options type as mentioned by @Piedone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, this API proposal needs to pass through API review first anyway, so both options can be mentioned during the process and considered there?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess indeed. My concern is really only about keeping it consistent with the other filter options here, what the current implementation is.

@RussKie
Copy link
Member

RussKie commented Feb 6, 2025

@Demo30 when you're get closer to completion, please provide more details in the body of the commit message as well as in the original message explaining the nature of the changes. This is to allow future maintainers to look at the git commit and understand why the change was done instead of needing to locate the original CR.

Also, in the future please use GitHub keywords in the body of the git commit and not in the subject.

Thank you!

@RussKie RussKie changed the title Allow custom filtering logic for FakeLogger #5615 Allow custom filtering logic for FakeLogger Feb 6, 2025
@@ -34,6 +35,17 @@ public class FakeLogCollectorOptions
/// </remarks>
public ISet<LogLevel> FilteredLevels { get; set; } = new HashSet<LogLevel>();

/// <summary>
/// Gets or sets custom filters used to filter out records to be collected.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Gets or sets custom filters used to filter out records to be collected.
/// Gets or sets filters which control what records would be collected by the fake logger.

/// <value>Default is an empty array.</value>
/// <remarks>
/// Defaults to an empty array, which doesn't filter any records.
/// If not empty, only records for which the filter function returns <see langword="true" /> will be collected by the fake logger.
Copy link
Member

@RussKie RussKie Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the point of the disconnect for me: "only records for which the filter function returns <see langword="true" /> will be collected by the fake logger.".
To me if a filter function returns true, it means the record was filtered out.

I recommend inverting the meaning here, which will invert the logic in the collector implementation too.

Comment on lines +38 to +47
/// <summary>
/// Gets or sets custom filters used to filter out records to be collected.
/// </summary>
/// <value>Default is an empty array.</value>
/// <remarks>
/// Defaults to an empty array, which doesn't filter any records.
/// If not empty, only records for which the filter function returns <see langword="true" /> will be collected by the fake logger.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.Telemetry)]
public IEnumerable<Func<FakeLogRecord, bool>> CustomFilters { get; set; } = Array.Empty<Func<FakeLogRecord, bool>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <summary>
/// Gets or sets custom filters used to filter out records to be collected.
/// </summary>
/// <value>Default is an empty array.</value>
/// <remarks>
/// Defaults to an empty array, which doesn't filter any records.
/// If not empty, only records for which the filter function returns <see langword="true" /> will be collected by the fake logger.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.Telemetry)]
public IEnumerable<Func<FakeLogRecord, bool>> CustomFilters { get; set; } = Array.Empty<Func<FakeLogRecord, bool>>();
/// <summary>
/// Gets or sets filters which control what records would be collected by the fake logger.
/// </summary>
/// <remarks>
/// The default value is an empty array, which means all records would be collected.
/// To filter a record out, the filter function must return <see langword="true" />.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.Telemetry)]
public IEnumerable<Func<FakeLogRecord, bool>> CustomFilters { get; set; } = Array.Empty<Func<FakeLogRecord, bool>>();
Suggested change
/// <summary>
/// Gets or sets custom filters used to filter out records to be collected.
/// </summary>
/// <value>Default is an empty array.</value>
/// <remarks>
/// Defaults to an empty array, which doesn't filter any records.
/// If not empty, only records for which the filter function returns <see langword="true" /> will be collected by the fake logger.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.Telemetry)]
public IEnumerable<Func<FakeLogRecord, bool>> CustomFilters { get; set; } = Array.Empty<Func<FakeLogRecord, bool>>();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[API Proposal]: Allow custom filtering logic for FakeLogger
4 participants