[docs] Add exception logging best practices (#6037)

This commit is contained in:
Mikel Blanchard 2025-01-16 11:02:16 -08:00 committed by GitHub
parent f7df3e1714
commit dcdaaae6e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 145 additions and 35 deletions

View File

@ -25,33 +25,17 @@ OpenTelemetry .NET:
* [Getting Started - Console Application](./getting-started-console/README.md)
* [Logging with Complex Objects](./complex-objects/README.md)
## Structured Logging
## Logging API
:heavy_check_mark: You should use structured logging.
### ILogger
* Structured logging is more efficient than unstructured logging.
* Filtering and redaction can happen on individual key-value pairs instead of
the entire log message.
* Storage and indexing are more efficient.
* Structured logging makes it easier to manage and consume logs.
.NET supports high performance, structured logging via the
[`Microsoft.Extensions.Logging.ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger)
interface (including
[`ILogger<TCategoryName>`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger-1))
to help monitor application behavior and diagnose issues.
:stop_sign: You should avoid string interpolation.
> [!WARNING]
> The following code has bad performance due to [string
interpolation](https://learn.microsoft.com/dotnet/csharp/tutorials/string-interpolation):
```csharp
var food = "tomato";
var price = 2.99;
logger.LogInformation($"Hello from {food} {price}.");
```
Refer to the [logging performance
benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details.
## Package Version
#### Package Version
:heavy_check_mark: You should always use the
[`ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger)
@ -69,16 +53,6 @@ package, regardless of the .NET runtime version being used:
backward compatibility on `Microsoft.Extensions.Logging` even during major
version bumps, so compatibility is not a concern here.
## Logging API
### ILogger
.NET supports high performance, structured logging via the
[`Microsoft.Extensions.Logging.ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger)
interface (including
[`ILogger<TCategoryName>`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger-1))
to help monitor application behavior and diagnose issues.
#### Get Logger
In order to use the `ILogger` interface, you need to first get a logger. How to
@ -125,7 +99,38 @@ are not super expensive, they still come with CPU and memory cost, and are meant
to be reused throughout the application. Refer to the [logging performance
benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details.
#### Use Logger
#### Write log messages
:heavy_check_mark: You should use structured logging.
* Structured logging is more efficient than unstructured logging.
* Filtering and redaction can happen on individual key-value pairs instead of
the entire log message.
* Storage and indexing are more efficient.
* Structured logging makes it easier to manage and consume logs.
```csharp
var food = "tomato";
var price = 2.99;
logger.LogInformation("Hello from {food} {price}.", food, price);
```
:stop_sign: You should avoid string interpolation.
> [!WARNING]
> The following code has bad performance due to [string
interpolation](https://learn.microsoft.com/dotnet/csharp/tutorials/string-interpolation):
```csharp
var food = "tomato";
var price = 2.99;
logger.LogInformation($"Hello from {food} {price}.");
```
Refer to the [logging performance
benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details.
:heavy_check_mark: You should use [compile-time logging source
generation](https://docs.microsoft.com/dotnet/core/extensions/logger-message-generator)
@ -222,6 +227,111 @@ code is now depending on which logger is being enabled, not to mention the
argument evaluation might have significant side effects that are now depending
on the logging configuration.
:heavy_check_mark: You should use a dedicated parameter to log exceptions when
using the compile-time source generator.
```csharp
var food = "tomato";
var price = 2.99;
try
{
// Execute some logic
logger.SayHello(food, price);
}
catch (Exception ex)
{
logger.SayHelloFailure(ex, food, price);
}
internal static partial class LoggerExtensions
{
[LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
public static partial void SayHello(this ILogger logger, string food, double price);
[LoggerMessage(Level = LogLevel.Error, Message = "Could not say hello from {food} {price}.")]
public static partial void SayHelloFailure(this ILogger logger, Exception exception, string food, double price);
}
```
> [!NOTE]
> When using the compile-time source generator the first `Exception` parameter
> detected is automatically given special handling. It **SHOULD NOT** be part of
> the message template. For details see: [Log method
> anatomy](https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator#log-method-anatomy).
:heavy_check_mark: You should use the dedicated overloads to log exceptions when
using the logging extensions methods.
```csharp
var food = "tomato";
var price = 2.99;
try
{
// Execute some logic
logger.LogInformation("Hello from {food} {price}.", food, price);
}
catch (Exception ex)
{
logger.LogError(ex, "Could not say hello from {food} {price}.", food, price);
}
```
:stop_sign: You should avoid adding exception details into the message template.
You want to use the correct `Exception` APIs because the OpenTelemetry
Specification [defines dedicated
attributes](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-logs.md)
for `Exception` details. The following examples show what **NOT** to do. In
these cases the details won't be lost, but the dedicated attributes also won't
be added.
```csharp
var food = "tomato";
var price = 2.99;
try
{
// Execute some logic
logger.SayHello(food, price);
}
catch (Exception ex)
{
logger.SayHelloFailure(food, price, ex.Message);
}
internal static partial class LoggerExtensions
{
[LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
public static partial void SayHello(this ILogger logger, string food, double price);
// BAD - Exception should not be part of the message template. Use the dedicated parameter.
[LoggerMessage(Level = LogLevel.Error, Message = "Could not say hello from {food} {price} {message}.")]
public static partial void SayHelloFailure(this ILogger logger, string food, double price, string message);
}
```
```csharp
var food = "tomato";
var price = 2.99;
try
{
// Execute some logic
logger.LogInformation("Hello from {food} {price}.", food, price);
}
catch (Exception ex)
{
// BAD - Exception should not be part of the message template. Use the dedicated parameter.
logger.LogError("Could not say hello from {food} {price} {message}.", food, price, ex.Message);
}
```
## LoggerFactory
In many cases, you can use [ILogger](#ilogger) without having to interact with