Skip to content

Commit eab0a7c

Browse files
authored
Docs - logging best practice (#5246)
1 parent dca99b6 commit eab0a7c

File tree

3 files changed

+135
-31
lines changed

3 files changed

+135
-31
lines changed

docs/logs/README.md

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# OpenTelemetry .NET Logs
2+
3+
## Best Practices
4+
5+
The following tutorials have demonstrated the best practices for logging with
6+
OpenTelemetry .NET:
7+
8+
* [Getting Started - Console Application](./getting-started-console/README.md)
9+
* [Getting Started - ASP.NET Core
10+
Application](./getting-started-aspnetcore/README.md)
11+
* [Logging with Complex Objects](./complex-objects/README.md)
12+
13+
## Structured Logging
14+
15+
:heavy_check_mark: You should use structured logging.
16+
17+
* Structured logging is more efficient than unstructured logging.
18+
* Filtering and redaction can happen on invidual key-value pairs instead of
19+
the entire log message.
20+
* Storage and indexing are more efficient.
21+
* Structured logging makes it easier to manage and consume logs.
22+
23+
:stop_sign: You should avoid string interpolation.
24+
25+
> [!WARNING]
26+
> The following code has bad performance due to [string
27+
interpolation](https://learn.microsoft.com/dotnet/csharp/tutorials/string-interpolation):
28+
29+
```csharp
30+
var food = "tomato";
31+
var price = 2.99;
32+
33+
logger.LogInformation($"Hello from {food} {price}.");
34+
```
35+
36+
Refer to the [logging performance
37+
benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details.
38+
39+
## Package Version
40+
41+
:heavy_check_mark: You should always use the
42+
[`ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger)
43+
interface from the latest stable version of
44+
[Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging/)
45+
package, regardless of the .NET runtime version being used:
46+
47+
* If you're using the latest stable version of [OpenTelemetry .NET
48+
SDK](../../src/OpenTelemetry/README.md), you don't have to worry about the
49+
version of `Microsoft.Extensions.Logging` package because it is already taken
50+
care of for you via [package dependency](../../Directory.Packages.props).
51+
* Starting from version `3.1.0`, the .NET runtime team is holding a high bar for
52+
backward compatibility on `Microsoft.Extensions.Logging` even during major
53+
version bumps, so compatibility is not a concern here.
54+
55+
## Logging API
56+
57+
:heavy_check_mark: You should use [compile-time logging source
58+
generation](https://docs.microsoft.com/dotnet/core/extensions/logger-message-generator)
59+
pattern to achieve the best performance.
60+
61+
```csharp
62+
public static partial class Food
63+
{
64+
[LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
65+
public static partial void SayHello(ILogger logger, string food, double price);
66+
}
67+
68+
var food = "tomato";
69+
var price = 2.99;
70+
71+
Food.SayHello(logger, food, price);
72+
```
73+
74+
> [!NOTE]
75+
> There is no need to pass in an explicit
76+
[EventId](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.eventid)
77+
while using
78+
[LoggerMessageAttribute](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loggermessageattribute).
79+
A durable `EventId` will be automatically assigned based on the hash of the
80+
method name during code generation.
81+
82+
:heavy_check_mark: You can use
83+
[LogPropertiesAttribute](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.logpropertiesattribute)
84+
from
85+
[Microsoft.Extensions.Telemetry.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions/)
86+
if you need to log complex objects. Check out the [Logging with Complex
87+
Objects](./complex-objects/README.md) tutorial for more details.
88+
89+
:stop_sign: You should avoid the extension methods from
90+
[LoggerExtensions](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loggerextensions),
91+
these methods are not optimized for performance.
92+
93+
> [!WARNING]
94+
> The following code has bad performance due to
95+
[boxing](https://learn.microsoft.com/dotnet/csharp/programming-guide/types/boxing-and-unboxing):
96+
97+
```csharp
98+
var food = "tomato";
99+
var price = 2.99;
100+
101+
logger.LogInformation("Hello from {food} {price}.", food, price);
102+
```
103+
104+
Refer to the [logging performance
105+
benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details.

test/Benchmarks/Logs/Food.cs

+2-6
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ namespace Benchmarks.Logs;
77

88
public static partial class Food
99
{
10-
[LoggerMessage(
11-
EventId = 0,
12-
Level = LogLevel.Information,
13-
Message = "Hello from {food} {price}.")]
14-
public static partial void SayHello(
15-
ILogger logger, string food, double price);
10+
[LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
11+
public static partial void SayHello(ILogger logger, string food, double price);
1612
}

test/Benchmarks/Logs/LogBenchmarks.cs

+28-25
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,30 @@
77
using OpenTelemetry.Logs;
88

99
/*
10-
BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000)
11-
Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores
12-
.NET SDK 8.0.100
13-
[Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
14-
DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
15-
16-
17-
| Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
18-
|--------------------------------------- |-----------:|----------:|----------:|-----------:|-------:|----------:|
19-
| NoListener | 44.633 ns | 0.8442 ns | 1.9733 ns | 43.683 ns | 0.0102 | 64 B |
20-
| NoListenerWithLoggerMessageGenerator | 1.880 ns | 0.0141 ns | 0.0125 ns | 1.879 ns | - | - |
21-
| OneProcessor | 126.857 ns | 1.1861 ns | 1.0514 ns | 126.730 ns | 0.0165 | 104 B |
22-
| OneProcessorWithLoggerMessageGenerator | 112.677 ns | 1.0021 ns | 0.8884 ns | 112.605 ns | 0.0063 | 40 B |
23-
| TwoProcessors | 129.967 ns | 0.8315 ns | 0.7371 ns | 129.850 ns | 0.0165 | 104 B |
24-
| ThreeProcessors | 130.117 ns | 1.1359 ns | 1.0626 ns | 129.991 ns | 0.0165 | 104 B |
10+
BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.3007/22H2/2022Update/SunValley2)
11+
11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores
12+
.NET SDK 8.0.101
13+
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
14+
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
15+
16+
17+
| Method | Mean | Error | StdDev | Gen0 | Allocated |
18+
|------------------------------ |-----------:|----------:|----------:|-------:|----------:|
19+
| NoListenerStringInterpolation | 124.458 ns | 2.5188 ns | 2.2329 ns | 0.0114 | 72 B |
20+
| NoListenerExtensionMethod | 36.326 ns | 0.2916 ns | 0.2435 ns | 0.0102 | 64 B |
21+
| NoListener | 1.375 ns | 0.0586 ns | 0.0896 ns | - | - |
22+
| OneProcessor | 98.133 ns | 1.8805 ns | 1.5703 ns | 0.0063 | 40 B |
23+
| TwoProcessors | 105.414 ns | 0.4610 ns | 0.3850 ns | 0.0063 | 40 B |
24+
| ThreeProcessors | 102.023 ns | 1.4187 ns | 1.1847 ns | 0.0063 | 40 B |
2525
*/
2626

2727
namespace Benchmarks.Logs;
2828

2929
public class LogBenchmarks
3030
{
31+
private const double FoodPrice = 2.99;
32+
private static readonly string FoodName = "tomato";
33+
3134
private readonly ILogger loggerWithNoListener;
3235
private readonly ILogger loggerWithOneProcessor;
3336
private readonly ILogger loggerWithTwoProcessors;
@@ -64,39 +67,39 @@ public LogBenchmarks()
6467
}
6568

6669
[Benchmark]
67-
public void NoListener()
70+
public void NoListenerStringInterpolation()
6871
{
69-
this.loggerWithNoListener.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
72+
this.loggerWithNoListener.LogInformation($"Hello from {FoodName} {FoodPrice}.");
7073
}
7174

7275
[Benchmark]
73-
public void NoListenerWithLoggerMessageGenerator()
76+
public void NoListenerExtensionMethod()
7477
{
75-
Food.SayHello(this.loggerWithNoListener, "tomato", 2.99);
78+
this.loggerWithNoListener.LogInformation("Hello from {name} {price}.", FoodName, FoodPrice);
7679
}
7780

7881
[Benchmark]
79-
public void OneProcessor()
82+
public void NoListener()
8083
{
81-
this.loggerWithOneProcessor.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
84+
Food.SayHello(this.loggerWithNoListener, FoodName, FoodPrice);
8285
}
8386

8487
[Benchmark]
85-
public void OneProcessorWithLoggerMessageGenerator()
88+
public void OneProcessor()
8689
{
87-
Food.SayHello(this.loggerWithOneProcessor, "tomato", 2.99);
90+
Food.SayHello(this.loggerWithOneProcessor, FoodName, FoodPrice);
8891
}
8992

9093
[Benchmark]
9194
public void TwoProcessors()
9295
{
93-
this.loggerWithTwoProcessors.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
96+
Food.SayHello(this.loggerWithTwoProcessors, FoodName, FoodPrice);
9497
}
9598

9699
[Benchmark]
97100
public void ThreeProcessors()
98101
{
99-
this.loggerWithThreeProcessors.LogInformation("Hello from {name} {price}.", "tomato", 2.99);
102+
Food.SayHello(this.loggerWithThreeProcessors, FoodName, FoodPrice);
100103
}
101104

102105
internal class DummyLogProcessor : BaseProcessor<LogRecord>

0 commit comments

Comments
 (0)