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

Add CA1873: Avoid potentially expensive logging #7290

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

mpidash
Copy link
Contributor

@mpidash mpidash commented Apr 15, 2024

Fixes dotnet/runtime#78402.

Rule ID CA1873
Title Avoid potentially expensive logging
Message Evaluation of this argument may be expensive and unnecessary if logging is disabled
Description In many situations, logging is disabled or set to a log level that results in an unnecessary evaluation for this argument.
Category Performance
Severity Suggestion

This analyzer detects

  • calls to ILogger.Log,
  • calls to extension methods in Microsoft.Extensions.Logging.LoggerExtensions and
  • calls to methods decorated with [LoggerMessage]

and flags them if they evaluate expensive arguments without checking if logging is enabled with ILogger.IsEnabled.


There are 12 findings in dotnet/runtime (8 of them in Microsoft.Extensions.Logging.Abstractions which will probably disable this warning), 157 findings in dotnet/roslyn and 497 in dotnet/aspnetcore (including testing code).
I skimmed through them and could not find any false positives.

This analyzer detects calls to 'ILogger.Log', extension methods in
'Microsoft.Extensions.Logging.LoggerExtensions' and methods decorated
with '[LoggerMessage]'.
It then checks if they evaluate expensive arguments without checking if
logging is enabled with 'ILogger.IsEnabled'.
@mpidash mpidash requested a review from a team as a code owner April 15, 2024 01:49
return AreInvocationsOnSameInstance(logInvocation, invocation) && IsSameLogLevel(invocation.Arguments[0]);
}

static bool AreInvocationsOnSameInstance(IInvocationOperation invocation1, IInvocationOperation invocation2)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A variation of this is used in other analyzers. I think this could be a good candidate for another extension method.

Comment on lines 1584 to 1596
[Fact]
public async Task WrongLogLevelGuardedWorkInLog_ReportsDiagnostic_CS()
{
string source = """
using System;
using Microsoft.Extensions.Logging;

class C
{
void M(ILogger logger, EventId eventId, Exception exception, Func<string, Exception, string> formatter)
{
if (logger.IsEnabled(LogLevel.Critical))
logger.Log(LogLevel.Trace, eventId, [|ExpensiveMethodCall()|], exception, formatter);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was wondering if we should report another diagnostic if the log level does not match, as this is most likely a bug.

Comment on lines 1806 to 1820
[Fact]
public async Task WrongInstanceGuardedWorkInLog_ReportsDiagnostic_CS()
{
string source = """
using System;
using Microsoft.Extensions.Logging;

class C
{
private ILogger _otherLogger;

void M(ILogger logger, EventId eventId, Exception exception, Func<string, Exception, string> formatter)
{
if (_otherLogger.IsEnabled(LogLevel.Trace))
logger.Log(LogLevel.Trace, eventId, [|ExpensiveMethodCall()|], exception, formatter);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another candidate for a diagnostic: Checking the wrong logger instance.

@mpidash
Copy link
Contributor Author

mpidash commented Apr 15, 2024

cc @stephentoub @Youssef1313 (thanks for providing the prototype 👍)

Copy link

codecov bot commented Apr 15, 2024

Codecov Report

Attention: Patch coverage is 88.88889% with 27 lines in your changes missing coverage. Please review.

Project coverage is 96.54%. Comparing base (5ed3367) to head (b4247c7).
Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7290      +/-   ##
==========================================
+ Coverage   96.50%   96.54%   +0.04%     
==========================================
  Files        1452     1454       +2     
  Lines      347566   352505    +4939     
  Branches    11416    11478      +62     
==========================================
+ Hits       335413   340336    +4923     
+ Misses       9257     9255       -2     
- Partials     2896     2914      +18     

@stephentoub
Copy link
Member

I skimmed through them and could not find any false positives.

ca1873-runtime.txt

Thanks. The LoggerExtensions.cs ones are all false positives in that we'll want to suppress them, but that's also effectively the implementation of logging rather than consumption of logging, and we frequently have to suppress rules in such implementations. The others for runtime look like valid diagnostics.

@mpidash mpidash closed this Jul 5, 2024
@mpidash mpidash reopened this Jul 5, 2024
@mpidash
Copy link
Contributor Author

mpidash commented Jan 16, 2025

@drewnoakes @stephentoub Sorry for the ping, but I just wanted to follow up and see if you might have time to review this PR or know who the right person would be. Thank you very much!

or ILocalReferenceOperation
or IParameterReferenceOperation)
{
return false;
Copy link
Member

Choose a reason for hiding this comment

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

This is taking an approach of saying some specific things aren't expensive and everything else is. Should it be inverted to instead special-case the things we know to be expensive? I'm concerned otherwise that as the language evolves and more kinds of operations are introduced, more and more possibly inexpensive things could end up getting flagged here. We should be intentional about what kinds of operations are considered expensive.

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, I agree. I will change this to be inverted (to be future proof) and will also update what is considered expensive based on your other comments. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See 696f73e.

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

Thank you for working on this.

@jeffhandley, is there someone who can help shepherd this?

Previously, some specific operations were treated as non-expensive and
the rest were. This is not really future-proof as the language evolves,
and is generally more prone to false positives.

So this has now been changed to the opposite: Treat certain operations
as expensive, and the rest are not expensive by default and do not
trigger a diagnostic.
@mpidash
Copy link
Contributor Author

mpidash commented Jan 18, 2025

@stephentoub Thanks for the review, I rewrote the logic to find expensive argument evaluations like you suggested: Focus on the known expensive ones and treat the rest as non-expensive (to avoid false positives).

Detection of boxing operations and creation of implicit non-empty params arrays should now also work properly. I also allowed nameof inside interpolated strings (is heavily used in dotnet/aspnetcore).

Found diagnostics also seem better now, especially in runtime/runtime. Only the real findings remain, so no need for a suppression:

ca1873-runtime-2.txt
ca1873-roslyn-2.txt
ca1873-aspnetcore-2.txt
ca1873-roslyn-analyzers2.txt

@stephentoub
Copy link
Member

Thanks. I randomly sampled some of the cited hits. Why is this one getting triggered, for example?

W:\aspnetcore\src\Security\Authentication\OpenIdConnect\src\OpenIdConnectHandler.cs(1001,33): info CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled

It appears to be properly guarded.

@mpidash
Copy link
Contributor Author

mpidash commented Jan 23, 2025

Thanks. I randomly sampled some of the cited hits. Why is this one getting triggered, for example?

W:\aspnetcore\src\Security\Authentication\OpenIdConnect\src\OpenIdConnectHandler.cs(1001,33): info CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled

It appears to be properly guarded.

This was due to not comparing symbols correctly (using == instead of SymbolEqualityComparer when checking for same instance symbol). With the change in 5c0da56, the following two false positives are gone (the rest is the same as before):

W:\aspnetcore\src\Security\Authentication\OpenIdConnect\src\OpenIdConnectHandler.cs(1001,33): info CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled
W:\aspnetcore\src\Security\Authentication\OpenIdConnect\src\OpenIdConnectHandler.cs(1005,33): info CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled

This should have been flagged by RS1024 (Compare symbols correctly), but code analysis seems to be broken for me locally on this repo. There is no live preview of diagnostics, none when running dotnet build and when I try it inside VS (Version 17.13.0 Preview 3.0) via Build -> Run Code Analysis on Solution I get the following error: StreamJsonRpc.RemoteInvocationException: Argument contains duplicate analyzer instances. (Parameter 'analyzers') which seems to be a known issue?

@stephentoub
Copy link
Member

stephentoub commented Jan 23, 2025

Thanks.

W:\aspnetcore\src\Servers\Kestrel\Core\src\Middleware\Internal\LoggingStream.cs(224,26): info CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled is also a false positive. The analyzer isn't noticing the check at the beginning of the method that guards the whole method by returning if IsEnabled returns false.

@mpidash
Copy link
Contributor Author

mpidash commented Jan 28, 2025

Thanks for checking, I've added the early exit (i.e. before the logging invocation) as a recognized guard pattern. For this to work, I am now searching each ancestor block of the logging invocation for appropriate guard conditionals.

Re-running all tests leads to exactly the same findings, except the false positive in your last comment.

@stephentoub
Copy link
Member

@sharwell, could you review this as well? Thanks.

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.

[Analyzer] Usage of ToString in logging source generator method call sites
4 participants