Exception arguments to LoggerMessage methods are ignored by the generated formatter
- Dominant language
- C#
- Stars
- 3.2k
- Forks
- 894
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 23
Description
### Description
In the generated implementations for methods with the `LoggerMessage` attribute, exceptions are discarded by the formatter. Regardless of position among the arguments, the formatter `Func` that is generated puts the discard operator `_` in place of the exception argument to the lambda expression. We are using the generators from `Microsoft.Extensions.Telemetry`.
### Reproduction Steps
Create a method like so:
```C#
public static partial class FileName
{
[LoggerMessage(Level = LogLevel.Information, Message = "Temperature: {Temp}")]
public static partial void LogMessage(this ILogger logger, string temp, Exception ex);
}
```
The following implementation is generated.
```C#
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "9.4.0.0")]
public static partial void LogMessage(this global::Microsoft.Extensions.Logging.ILogger logger, string temp, global::System.Exception ex)
{
if (!logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
{
return;
}
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(2);
state.TagArray[1] = new("{OriginalFormat}", "Temperature: {Temp}");
state.TagArray[0] = new("Temp", temp);
logger.Log(
global::Microsoft.Extensions.Logging.LogLevel.Information,
new(752341003, nameof(LogMessage)),
state,
ex,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "9.4.0.0")] static string (s, _) =>
{
var temp = s.TagArray[0].Value ?? "(null)";
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Temperature: {temp}");
#else
return global::System.FormattableString.Invariant($"Temperature: {temp}");
#endif
});
state.Clear();
}
```
Furthermore, if I modify the method in an effort to redact the exception, the generated implementation does not change, but a new `DataClassification` is created.
```C#
[LoggerMessage(Level = LogLevel.Information, Message = "Temperature: {Temp}")]
public static partial void LogMessage(this ILogger logger, string temp, [EUPI] Exception ex);
```
```C#
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "9.4.0.0")]
private static readonly Microsoft.Extensions.Compliance.Classification.DataClassification _Microsoft_Identity_ServiceEssentials_EUPIAttribute = new Microsoft.Identity.ServiceEssentials.EUPIAttribute().Classification;
///
/// Logs "Temperature: {Temp}" at "Information" level.
///
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "9.4.0.0")]
public static partial void LogMessage(this global::Microsoft.Extensions.Logging.ILogger logger, string temp, global::System.Exception ex)
{
if (!logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
{
return;
}
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(2);
state.TagArray[1] = new("{OriginalFormat}", "Temperature: {Temp}");
state.TagArray[0] = new("Temp", temp);
logger.Log(
global::Microsoft.Extensions.Logging.LogLevel.Information,
new(752341003, nameof(LogMessage)),
state,
ex,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "9.4.0.0")] static string (s, _) =>
{
var temp = s.TagArray[0].Value ?? "(null)";
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Temperature: {temp}");
#else
return global::System.FormattableString.Invariant($"Temperature: {temp}");
#endif
});
state.Clear();
}
```
With an ILogger implementation along the lines of the following, the exception will not be included in the logs.
```C#
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
{
Consol.WriteLine($"[{logLevel}] {_categoryName}: {formatter(state, exception)}");
}
```
### Expected behavior
I expect the exception to be included in the logs and redacted if applicable.
### Actual behavior
The exception is not present in any logs and, judging from the generated code, is not redacted.
### Regression?
_No response_
### Known Workarounds
One workaround that I've found is to add another exception to the arguments and explicitly add it to the template.
```C#
[LoggerMessage(Level = LogLevel.Information, Message = "Temperature: {Temp} . {ex2}")]
public static partial void LogMessage(this ILogger logger, string temp, Exception ex, [EUPI] Exception ex2);
```
Another is to add a placeholder for the exception outside of the formatter. Note that this does not help if the exception needs to be redacted.
```C#
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
{
Consol.WriteLine($"[{logLevel}] {_categoryName}: {formatter(state, exception)}. {exception}");
}
```
### Configuration
.NET SDK:
Version: 9.0.300-preview.0.25177.5
Commit: 3d5b396331
Workload version: 9.0.300-manifests.1e5233e8
MSBuild version: 17.14.0-preview-25175-08+5880e1c75
Runtime Environment:
OS Name: Windows
OS Version: 10.0.26100
OS Platform: Windows
RID: win-x64
### Other information
The code that would replace the discard in the generated code is behind [this condition](https://github.com/dotnet/extensions/blob/fa2d656fba04cdd7ac06cea6dadbc9aee73b1d5d/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs#L252) which, I think, checks for the argument in the `Message`, but it is specifically warned against to explicitly list the exception as a placeholder in the template (`LOGGEN009`). The tests expect this behavior, that the exception is not in the formatted message and another exception must be supplied in order to make it into the formatted message.
Contributor guide
Assessment
This issue has not been assessed yet.