MetalamaCommented examplesLoggingStep 8.​ Avoiding infinite recursions
Open sandboxFocusImprove this doc

Logging example, step 8: Avoiding infinite recursion

Infinite recursion can occur when the logging logic of one method calls the logging logic of another method. Infinite recursion can cause stack overflow exceptions, resulting in crashes or unintended side effects. It consumes system resources, thus negatively affecting performance and potentially rendering the application, and any other application on the same device, unresponsive. In addition, excessive log entries generated by recursion make it difficult to locate the real cause of the problem, complicate log analysis, and increase storage requirements.

Therefore, infinite recursions in logging must be avoided at all costs.

To make this possible, a first step is to avoid logging the ToString method. However, sometimes a non-logged ToString method can access logged properties or methods, and indirectly cause an infinite recursion. The most reliable approach is to add the following code in the logging pattern:

using ( var guard = LoggingRecursionGuard.Begin() )
{
  if ( guard.CanLog )
  {
    this._logger.LogTrace( message );
  }
}

We can update the previous example with this new approach:

Source Code



1public class LoginService
2{
3    // The 'password' parameter will not be logged because of its name.
4    public bool VerifyPassword( string account, string password ) => account == password;








































5
6    [return: NotLogged]
7    public string GetSaltedHash( string account, string password, [NotLogged] string salt ) =>
8        account + password + salt;















































9}
Transformed Code
1using System;
2using Microsoft.Extensions.Logging;
3
4public class LoginService
5{
6    // The 'password' parameter will not be logged because of its name.
7    public bool VerifyPassword( string account, string password ) { var isTracingEnabled = this._logger.IsEnabled(LogLevel.Trace);
8        if (isTracingEnabled)
9        {
10            using (var guard = global::LoggingRecursionGuard.Begin())
11            {
12                if (guard.CanLog)
13                {
14                    LoggerExtensions.LogTrace(this._logger, $"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) started.");
15                }
16            }
17        }
18
19        try
20        {
21            bool result;
22            result = account == password;if (isTracingEnabled)
23            {
24                using (var guard_1 = global::LoggingRecursionGuard.Begin())
25                {
26                    if (guard_1.CanLog)
27                    {
28                        LoggerExtensions.LogTrace(this._logger, $"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) returned {result}.");
29                    }
30                }
31            }
32
33            return result;
34        }
35        catch (Exception e) when (this._logger.IsEnabled(LogLevel.Warning))
36        {
37            using (var guard_2 = global::LoggingRecursionGuard.Begin())
38            {
39                if (guard_2.CanLog)
40                {
41                    LoggerExtensions.LogWarning(this._logger, $"LoginService.VerifyPassword(account = {{{account}}}, password = <redacted> ) failed: {e.Message}");
42                }
43            }
44
45            throw;
46        }
47    }
48
49    [return: NotLogged]
50    public string GetSaltedHash( string account, string password, [NotLogged] string salt ) {
51var isTracingEnabled = this._logger.IsEnabled(LogLevel.Trace);
52        if (isTracingEnabled)
53        {
54            using (var guard = global::LoggingRecursionGuard.Begin())
55            {
56                if (guard.CanLog)
57                {
58                    LoggerExtensions.LogTrace(this._logger, $"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) started.");
59                }
60            }
61        }
62
63        try
64        {
65            string result;
66            result =         account + password + salt;if (isTracingEnabled)
67            {
68                using (var guard_1 = global::LoggingRecursionGuard.Begin())
69                {
70                    if (guard_1.CanLog)
71                    {
72                        LoggerExtensions.LogTrace(this._logger, $"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) returned <redacted>.");
73                    }
74                }
75            }
76
77            return result;
78        }
79        catch (Exception e) when (this._logger.IsEnabled(LogLevel.Warning))
80        {
81            using (var guard_2 = global::LoggingRecursionGuard.Begin())
82            {
83                if (guard_2.CanLog)
84                {
85                    LoggerExtensions.LogWarning(this._logger, $"LoginService.GetSaltedHash(account = {{{account}}}, password = <redacted> , salt = <redacted> ) failed: {e.Message}");
86                }
87            }
88
89            throw;
90        }
91    }
92private ILogger _logger;
93
94    public LoginService
95(ILogger<LoginService> logger = default(global::Microsoft.Extensions.Logging.ILogger<global::LoginService>))
96    {
97        this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
98    }
99}

Infrastructure code

LoggingRecursionGuard uses a thread-static field to indicate whether logging is currently occurring:

1internal static class LoggingRecursionGuard
2{
3    [ThreadStatic]
4    public static bool IsLogging;
5
6    public static DisposeCookie Begin()
7    {
8        if ( IsLogging )
9        {
10            return new DisposeCookie( false );
11        }
12        else
13        {
14            IsLogging = true;
15            return new DisposeCookie( true );
16        }
17    }
18
19    internal class DisposeCookie : IDisposable
20    {
21        public DisposeCookie( bool canLog )
22        {
23            this.CanLog = canLog;
24        }
25
26        public bool CanLog { get; }
27
28        public void Dispose()
29        {
30            if ( this.CanLog )
31            {
32                IsLogging = false;
33            }
34        }
35    }
36}

Aspect code

The LogAttribute code has been updated as follows:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Code.SyntaxBuilders;
5using Microsoft.Extensions.Logging;
6
7#pragma warning disable CS8618, CS0649
8
9public class LogAttribute : OverrideMethodAspect
10{
11    [IntroduceDependency]
12    private readonly ILogger _logger;
13
14    public override dynamic? OverrideMethod()
15    {
16        // Determine if tracing is enabled.
17        var isTracingEnabled = this._logger.IsEnabled( LogLevel.Trace );
18
19        // Write entry message.
20        if ( isTracingEnabled )
21        {
22            var entryMessage = BuildInterpolatedString( false );
23            entryMessage.AddText( " started." );
24
25            using ( var guard = LoggingRecursionGuard.Begin() )
26            {
27                if ( guard.CanLog )
28                {
29                    this._logger.LogTrace( (string) entryMessage.ToValue() );
30                }
31            }
32        }
33
34        try
35        {
36            // Invoke the method and store the result in a variable.
37            var result = meta.Proceed();
38
39            if ( isTracingEnabled )
40            {
41                // Display the success message. The message is different when the method is void.
42                var successMessage = BuildInterpolatedString( true );
43
44                if ( meta.Target.Method.ReturnType.Is( typeof(void) ) )
45                {
46                    // When the method is void, display a constant text.
47                    successMessage.AddText( " succeeded." );
48                }
49                else
50                {
51                    // When the method has a return value, add it to the message.
52                    successMessage.AddText( " returned " );
53
54                    if ( SensitiveParameterFilter.IsSensitive( meta.Target.Method.ReturnParameter ) )
55                    {
56                        successMessage.AddText( "<redacted>" );
57                    }
58                    else
59                    {
60                        successMessage.AddExpression( result );
61                    }
62
63                    successMessage.AddText( "." );
64                }
65
66                using ( var guard = LoggingRecursionGuard.Begin() )
67                {
68                    if ( guard.CanLog )
69                    {
70                        this._logger.LogTrace( (string) successMessage.ToValue() );
71                    }
72                }
73            }
74
75            return result;
76        }
77        catch ( Exception e ) when ( this._logger.IsEnabled( LogLevel.Warning ) )
78        {
79            // Display the failure message.
80            var failureMessage = BuildInterpolatedString( false );
81            failureMessage.AddText( " failed: " );
82            failureMessage.AddExpression( e.Message );
83
84            using ( var guard = LoggingRecursionGuard.Begin() )
85            {
86                if ( guard.CanLog )
87                {
88                    this._logger.LogWarning( (string) failureMessage.ToValue() );
89                }
90            }
91
92            throw;
93        }
94    }
95
96    // Builds an InterpolatedStringBuilder with the beginning of the message.
97    private static InterpolatedStringBuilder BuildInterpolatedString( bool includeOutParameters )
98    {
99        var stringBuilder = new InterpolatedStringBuilder();
100
101        // Include the type and method name.
102        stringBuilder.AddText( meta.Target.Type.ToDisplayString( CodeDisplayFormat.MinimallyQualified ) );
103        stringBuilder.AddText( "." );
104        stringBuilder.AddText( meta.Target.Method.Name );
105        stringBuilder.AddText( "(" );
106        var i = meta.CompileTime( 0 );
107
108        // Include a placeholder for each parameter.
109        foreach ( var p in meta.Target.Parameters )
110        {
111            var comma = i > 0 ? ", " : "";
112
113            if ( SensitiveParameterFilter.IsSensitive( p ) )
114            {
115                // Do not log sensitive parameters.
116                stringBuilder.AddText( $"{comma}{p.Name} = <redacted> " );
117            }
118            else if ( p.RefKind == RefKind.Out && !includeOutParameters )
119            {
120                // When the parameter is 'out', we cannot read the value.
121                stringBuilder.AddText( $"{comma}{p.Name} = <out> " );
122            }
123            else
124            {
125                // Otherwise, add the parameter value.
126                stringBuilder.AddText( $"{comma}{p.Name} = {{" );
127                stringBuilder.AddExpression( p.Value );
128                stringBuilder.AddText( "}" );
129            }
130
131            i++;
132        }
133
134        stringBuilder.AddText( ")" );
135
136        return stringBuilder;
137    }
138}