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

Akka.Event: add log filtering system to prevent Akka.NET logs from being emitted in first place #7179

Merged
merged 20 commits into from May 22, 2024

Conversation

Aaronontheweb
Copy link
Member

@Aaronontheweb Aaronontheweb commented Apr 29, 2024

Changes

Fixes #7097 and probably lots of other usability issues.

The goal behind this system is to prevent Akka.NET LogEvents from being emitted in the first place in the event that their data is not useful. This should allow developers to do things like akka.LOGLEVEL=DEBUG without getting absolutely flooded with noise from busy parts of the system like Akka.Remote, Akka.Streams, and so on.

The general idea is to pass in a new Setup class:

/// <summary>
/// Used to specify filters that can be used to curtail noise from sources in the Akka.NET log stream.
/// </summary>
public sealed class LogFilterSetup : Setup
{
    public LogFilterBase[] Filters { get; }
    
    public LogFilterEvaluator CreateEvaluator() => new(Filters);

    public LogFilterSetup(LogFilterBase[] filters)
    {
        Filters = filters;
    }
}

These LogFilterBase types are intended to be simple functions, but while taking into account some easy performance considerations:

public abstract class LogFilterBase : INoSerializationVerificationNeeded, IDeadLetterSuppression
{
    /// <summary>
    /// Which part of the log message this filter is evaluating?
    /// </summary>
    /// <remarks>
    /// This actually has a performance implication - if we're filtering on the source, which
    /// is already fully "expanded" into its final string representation, we can try to fail fast
    /// on that without any additional allocations.
    ///
    /// If we're filtering on the message, we have to fully expand the log message first which
    /// involves allocations. Users on really tight performance budgets should be aware of this.
    /// </remarks>
    public abstract LogFilterType FilterType { get; }

    /// <summary>
    /// Returns <c>true</c> if the message should be kept, <c>false</c> if it should be dropped.
    /// </summary>
    /// <param name="evt">The <see cref="LogEvent"/> being evaluated.</param>
    /// <remarks>
    /// If there are multiple filters, all of them must return <c>true</c> for the message to be kept.
    /// </remarks>
    public abstract bool ShouldKeepMessage(LogEvent evt);

    /// <summary>
    /// Fast path designed to avoid allocating strings if we're filtering on the message content.
    /// </summary>
    /// <param name="part">The part of the message to evaluate.</param>
    /// <param name="content">Usually the fully expanded message content.</param>
    public abstract LogFilterDecision ShouldKeepMessage(LogFilterType part, string content);
}

I'm even including a LogFilterBuilder that should make it easy to construct these using a simple builder pattern. For a log to be printed, all filters must evaluate to true. Will include some examples of this below.

Checklist

For significant changes, please ensure that the following have been completed (delete if not relevant):

Copy link
Member Author

@Aaronontheweb Aaronontheweb left a comment

Choose a reason for hiding this comment

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

Detailed my changes

@@ -1725,6 +1725,7 @@ namespace Akka.Actor
public int LogDeadLetters { get; }
public bool LogDeadLettersDuringShutdown { get; }
public System.TimeSpan LogDeadLettersSuspendDuration { get; }
public Akka.Event.LogFilterEvaluator LogFilter { get; }
Copy link
Member Author

Choose a reason for hiding this comment

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

New setting - this is where the log filter actually lives. Any custom logging plugin can access it this way.

// ReSharper disable once ClassNeverInstantiated.Global
public class LogFilterEvaluatorSpecs
{
public class LogFilterSetupSpecs : AkkaSpec
Copy link
Member Author

Choose a reason for hiding this comment

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

End to end specs - I can't easily intercept the default output from the StandardOutWriter so I'm swapping in my own MinimalLogger implementation and testing the results using that at the end. Not 100% clean, but close enough.

loggingAdapter2.Warning("baz");

// expect only the last message to be received
ReceiveN(3);
Copy link
Member Author

Choose a reason for hiding this comment

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

Log events will still hit the EventStream - the filter runs at the layer beneath this. AND THAT IS IMPORTANT: if we have the log filter layer running at this layer, at the point where LogEvents are EMITTED, this WILL KILL PERFORMANCE.

Instead, we make the decision about whether or not to keep / include a log at the layer right before the log is rendered. This is the correct place to do this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Anyway, that's why we still receive all 3 log events.

ReceiveN(3);

// check that the last message was the one that was allowed through
_logger.Events.Count.Should().Be(1);
Copy link
Member Author

Choose a reason for hiding this comment

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

The real logger should only have recorded the 1 log that passes both filters.

}
}

public class LogSourceCases
Copy link
Member Author

Choose a reason for hiding this comment

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

The rest of these test cases are really just testing the default Regex used by the LogFilterBuilder.

{
switch (filter.FilterType)
{
case LogFilterType.Source:
Copy link
Member Author

Choose a reason for hiding this comment

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

A single vote cast for LogFilterDecision.Drop is all it takes for us to bail out.

// expand the message if we haven't already
// NOTE: might result in duplicate allocations in third party logging libraries. They'll have to adjust their
// code accordingly after this feature ships.
expandedLogMessage = (string.IsNullOrEmpty(expandedLogMessage) ? evt.Message.ToString() : expandedLogMessage)!;
Copy link
Member Author

Choose a reason for hiding this comment

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

Have to expand the log down here, since we're keeping it.


namespace Akka.Event
{
public abstract class MinimalLogger : MinimalActorRef
{
public LogFilterEvaluator Filter { get; internal set; } = LogFilterEvaluator.NoFilters;
Copy link
Member Author

Choose a reason for hiding this comment

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

Default setting, which maintains previous behavior.

{
try
{
// short circuit if we're not going to print this message
if (!filter.ShouldTryKeepMessage(logEvent, out var expandedLogMessage))
return;
Copy link
Member Author

Choose a reason for hiding this comment

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

Self-explanatory.

@@ -144,7 +149,7 @@ internal static void PrintLogEvent(LogEvent logEvent)
}
}

StandardOutWriter.WriteLine(logEvent.ToString(), color);
StandardOutWriter.WriteLine(expandedLogMessage, color);
Copy link
Member Author

Choose a reason for hiding this comment

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

Avoid expanding the log a second time.

@Aaronontheweb Aaronontheweb marked this pull request as ready for review April 29, 2024 19:58
@Aaronontheweb Aaronontheweb changed the title [WIP] Akka.Event: add log filtering system to prevent Akka.NET logs from being emitted in first place Akka.Event: add log filtering system to prevent Akka.NET logs from being emitted in first place Apr 29, 2024
@Aaronontheweb
Copy link
Member Author

Going to leave this PR open for comments for a while before it gets merged in

@Aaronontheweb Aaronontheweb added this to the 1.5.21 milestone Apr 29, 2024
@Aaronontheweb
Copy link
Member Author

Also: documentation. Will add it once I get review comments.

Copy link
Contributor

@Arkatufus Arkatufus left a comment

Choose a reason for hiding this comment

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

LGTM

@Aaronontheweb
Copy link
Member Author

I still need to do API approvals here

@Arkatufus Arkatufus enabled auto-merge (squash) May 22, 2024 20:33
@Arkatufus Arkatufus disabled auto-merge May 22, 2024 20:50
@Aaronontheweb Aaronontheweb enabled auto-merge (squash) May 22, 2024 20:59
@Aaronontheweb Aaronontheweb merged commit d1ed226 into akkadotnet:dev May 22, 2024
9 of 12 checks passed
@Aaronontheweb Aaronontheweb deleted the log-filtering-system branch May 22, 2024 21:52
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.

Enable debug logging for ShardCoordinator/ShardRegion/Shard actors
2 participants