Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,23 @@ Check [this](https://github.com/stbychkov/AutoLoggerMessage/wiki/Configuration)

## Benchmarks

You can achieve performance boosts of up to 90% according to my benchmarks just by including this source generator in your project.
You can achieve performance boosts of up to 90% just by including this source generator in your project.

| Configuration | Mean | Ratio | Allocated |
|------------------------|-----------|-------|-----------|
| Default implementation | 38.149 ns | 1.00 | 216 B |
| Default + AutoLogger | 3.734 ns | 0.10 | - |
| AutoLoggerMessage | 3.747 ns | 0.10 | - |
For `ILogger.Log*` methods:

| Configuration | Mean | Ratio | Allocated |
|-------------------------|-----------|-------|-----------|
| Default implementation | 41.419 ns | 1.00 | 216 B |
| Default + LoggerMessage | 4.004 ns | 0.10 | - |
| AutoLoggerMessage | 4.577 ns | 0.11 | - |

And for `ILogger.DefineScope`:

| Configuration | Mean | Ratio | Allocated |
|-------------------------|-----------|-------|-----------|
| BeginScope | 39.566 ns | 1.00 | 216 B |
| DefineScope | 5.197 ns | 0.13 | - |
| AutoLoggerMessage | 5.296 ns | 0.13 | - |

Take a look at [benchmark](https://github.com/stbychkov/AutoLoggerMessage/wiki/Benchmarks) page for more details.

Expand Down
1 change: 1 addition & 0 deletions benchmarks/AutoLoggerMessageGenerator.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\src\AutoLoggerMessageGenerator.BuildOutput\AutoLoggerMessageGenerator.BuildOutput.csproj" />
<ProjectReference Include="..\src\AutoLoggerMessageGenerator\AutoLoggerMessageGenerator.Roslyn4_11.csproj"/>
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
[RankColumn]
[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
public partial class ExecutionTimeBenchmark
public partial class LogCallBenchmark
{
private ILogger _logger = null!;
private ConfigurationExample _configuration = null!;

[GlobalSetup]
public void Setup()
{
_logger = CustomNullLogger<ExecutionTimeBenchmark>.Instance;
_logger = CustomNullLogger<LogCallBenchmark>.Instance;
_configuration = new ConfigurationExample(
0, "Root", new ConfigurationExample(
1, "First Level", new ConfigurationExample(
Expand All @@ -29,6 +29,8 @@ public void LogInformationWithoutParameters()
_logger.LogInformation("Hello world!");
}

#if DEFAULT && !TELEMETRY && !AUTO_LOGGER_MESSAGE

[Benchmark]
[BenchmarkCategory("Without parameters")]
public void LoggerMessageWithoutParameters()
Expand All @@ -39,13 +41,17 @@ public void LoggerMessageWithoutParameters()
[LoggerMessage(LogLevel.Information, Message = "Hello world!")]
partial void LoggerMessageWithoutParametersImpl();

#endif

[Benchmark(Baseline = true)]
[BenchmarkCategory("With 6 Parameters")]
public void LogInformationWith6PrimitiveParameters()
{
_logger.LogInformation("Hello world! {arg1} {arg2} {arg3} {arg4} {arg5} {arg6}", 1, 2, 3, 4, 5, 6);
}

#if DEFAULT && !TELEMETRY && !AUTO_LOGGER_MESSAGE

[Benchmark]
[BenchmarkCategory("With 6 Parameters")]
public void LoggerMessageWith6Parameters()
Expand All @@ -56,13 +62,17 @@ public void LoggerMessageWith6Parameters()
[LoggerMessage(LogLevel.Information, Message = "Hello world! {arg1} {arg2} {arg3} {arg4} {arg5} {arg6}")]
partial void LoggerMessageWith6ParametersImpl(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6);

#endif

[Benchmark(Baseline = true)]
[BenchmarkCategory("With 7 Parameters")]
public void LogInformationWith7PrimitiveParameters()
{
_logger.LogInformation("Hello world! {arg1} {arg2} {arg3} {arg4} {arg5} {arg6} {arg7}", 1, 2, 3, 4, 5, 6, 7);
}

#if DEFAULT && !TELEMETRY && !AUTO_LOGGER_MESSAGE

[Benchmark]
[BenchmarkCategory("With 7 Parameters")]
public void LoggerMessageWith7Parameters()
Expand All @@ -74,6 +84,8 @@ public void LoggerMessageWith7Parameters()
partial void LoggerMessageWith7ParametersImpl(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6,
int arg7);

#endif

[Benchmark(Baseline = true)]
[BenchmarkCategory("With Complex Types Parameters")]
public void LogInformationWithComplexParameters()
Expand All @@ -82,6 +94,7 @@ public void LogInformationWithComplexParameters()
_configuration, _configuration, _configuration, _configuration, _configuration, _configuration);
}

#if DEFAULT && !TELEMETRY && !AUTO_LOGGER_MESSAGE
[Benchmark]
[BenchmarkCategory("With Complex Types Parameters")]
public void LoggerMessageWithComplexParameters()
Expand All @@ -102,6 +115,8 @@ partial void LoggerMessageWithComplexParametersImpl(
ConfigurationExample arg6
);

#endif

#if TELEMETRY

[Benchmark]
Expand Down
109 changes: 109 additions & 0 deletions benchmarks/BenchmarkFiles/LogScopeBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using Microsoft.Extensions.Logging;

[RankColumn]
[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
public partial class LogScopeBenchmark
{
private ILogger _logger = null!;
private ConfigurationExample _configuration = null!;

[GlobalSetup]
public void Setup()
{
_logger = CustomNullLogger<LogScopeBenchmark>.Instance;
_configuration = new ConfigurationExample(
0, "Root", new ConfigurationExample(
1, "First Level", new ConfigurationExample(
2, "Second level", null)
)
);
}

#if !TELEMETRY

[Benchmark(Baseline = true)]
[BenchmarkCategory("Without parameters")]
public void BeginScopeWithoutParameters()
{
using var _ = _logger.BeginScope("Hello world!");
}

#endif

#if DEFAULT && !TELEMETRY && !AUTO_LOGGER_MESSAGE

[Benchmark]
[BenchmarkCategory("Without parameters")]
public void LoggerDefineScopeWithoutParameters()
{
using var _ = _loggerDefineScopeWithoutParameters(_logger);
}

private static readonly Func<ILogger, IDisposable?> _loggerDefineScopeWithoutParameters =
LoggerMessage.DefineScope("Hello world!");

#endif

#if !TELEMETRY
[Benchmark(Baseline = true)]
[BenchmarkCategory("With 6 Parameters")]
public void BeginScopeWith6PrimitiveParameters()
{
using var _ = _logger.BeginScope("Hello world! {arg1} {arg2} {arg3} {arg4} {arg5} {arg6}", 1, 2, 3, 4, 5, 6);
}
#endif

#if DEFAULT && !TELEMETRY && !AUTO_LOGGER_MESSAGE

[Benchmark]
[BenchmarkCategory("With 6 Parameters")]
public void LoggerDefineScopeWith6Parameters()
{
using var _ = _loggerDefineScopeWith6Parameters(_logger, 1, 2, 3, 4, 5, 6);
}

private static readonly Func<ILogger, int, int, int, int, int, int, IDisposable?> _loggerDefineScopeWith6Parameters =
LoggerMessage.DefineScope<int, int, int, int, int, int>("Hello world! {arg1} {arg2} {arg3} {arg4} {arg5} {arg6}");

#endif

#if !TELEMETRY

[Benchmark(Baseline = true)]
[BenchmarkCategory("With Complex Types Parameters")]
public void BeginScopeWithComplexParameters()
{
using var _ = _logger.BeginScope("Hello world! {arg1} {arg2} {arg3} {arg4} {arg5} {arg6}",
_configuration, _configuration, _configuration, _configuration, _configuration, _configuration);
}

#endif

#if DEFAULT && !TELEMETRY && !AUTO_LOGGER_MESSAGE

[Benchmark]
[BenchmarkCategory("With Complex Types Parameters")]
public void LoggerDefineScopeWithComplexParameters()
{
using var _ = _loggerDefineScopeWithComplexParameters(_logger,
_configuration, _configuration, _configuration,
_configuration, _configuration, _configuration
);
}

private static readonly Func<ILogger, ConfigurationExample, ConfigurationExample,
ConfigurationExample, ConfigurationExample, ConfigurationExample,
ConfigurationExample, IDisposable?> _loggerDefineScopeWithComplexParameters =
LoggerMessage.DefineScope<ConfigurationExample, ConfigurationExample,
ConfigurationExample, ConfigurationExample,
ConfigurationExample, ConfigurationExample>(
"Hello world! {arg1} {arg2} {arg3} {arg4} {arg5} {arg6}"
);

#endif

public record ConfigurationExample(int Id, string Name, ConfigurationExample? NestedConfiguration);
}
3 changes: 2 additions & 1 deletion benchmarks/PackagesProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ internal static class PackagesProvider
public const string BenchmarkPackage = """<PackageReference Include="BenchmarkDotNet"/>""";
public const string MicrosoftExtensionsLoggingPackage = """<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"/>""";
public const string MicrosoftExtensionsTelemetryPackage = """<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions"/>""";
public const string AutoLoggerMessagePackage = """<ProjectReference Include="..\..\..\..\..\..\AutoLoggerMessageGenerator\AutoLoggerMessageGenerator.csproj" ReferenceOutputAssembly="true" OutputItemType="Analyzer" />""";
public const string AutoLoggerMessageBuildOutput = """<Reference Include="AutoLoggerMessageGenerator.BuildOutput" HintPath="..\AutoLoggerMessageGenerator.BuildOutput.dll"/>""";
public const string AutoLoggerMessagePackage = """<Analyzer Include="..\AutoLoggerMessageGenerator.dll"/>""";
}
2 changes: 2 additions & 0 deletions benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
{
PackagesProvider.BenchmarkPackage,
PackagesProvider.MicrosoftExtensionsLoggingPackage,
PackagesProvider.AutoLoggerMessageBuildOutput,
PackagesProvider.AutoLoggerMessagePackage
}
},
Expand All @@ -38,6 +39,7 @@
{
PackagesProvider.BenchmarkPackage,
PackagesProvider.AutoLoggerMessagePackage,
PackagesProvider.AutoLoggerMessageBuildOutput,
PackagesProvider.MicrosoftExtensionsLoggingPackage,
PackagesProvider.MicrosoftExtensionsTelemetryPackage,
}
Expand Down
18 changes: 14 additions & 4 deletions benchmarks/ProjectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ private static void CopyBenchmarkFiles(string workingDirectory)

private async Task<(string OutputFolder, string projFilePath)> GenerateProjectFile(string projectName, string workingDirectory)
{
var telemetryConstant = projectConfiguration.References.Contains(PackagesProvider.MicrosoftExtensionsTelemetryPackage)
? "TELEMETRY"
: string.Empty;
var configurations = string.Join(";", GetProjectConfigurations());

var targetFramework = TargetFrameworkMonikerDetector.Detect();

Expand All @@ -74,7 +72,7 @@ private static void CopyBenchmarkFiles(string workingDirectory)
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<IntermediateOutputPath>{outputFolder}</IntermediateOutputPath>
<RootNamespace>{projectName}</RootNamespace>
<DefineConstants>$(DefineConstants);{telemetryConstant}</DefineConstants>
<DefineConstants>$(DefineConstants);{configurations}</DefineConstants>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -99,6 +97,18 @@ private static void CopyBenchmarkFiles(string workingDirectory)
return (outputFolder, projFilePath);
}

private IEnumerable<string> GetProjectConfigurations()
{
if (projectConfiguration.References.Contains(PackagesProvider.MicrosoftExtensionsLoggingPackage))
yield return "DEFAULT";

if (projectConfiguration.References.Contains(PackagesProvider.MicrosoftExtensionsTelemetryPackage))
yield return "TELEMETRY";

if (projectConfiguration.References.Contains(PackagesProvider.AutoLoggerMessagePackage))
yield return "AUTO_LOGGER_MESSAGE";
}

internal class BuildResult
{
public required string ProjectDirectory { get; init; }
Expand Down
46 changes: 46 additions & 0 deletions docs/ADR/ADR-07_Generation_of_BeginScope_methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
### Title: ADR-07 Generation of `BeginScope` methods
### Status: Accepted

### Context:

The existing `ILogger.BeginScope` method has the same problems as the `ILogger.Log*` methods:
1. Lack of compile-time checks for mismatches between template parameters and the actual arguments, which can lead to runtime exceptions.
2. Performance overhead due to the need to create a new scope object every time.
3. Unnecessary allocations due to boxing of template parameters, even with identical arguments.

`ILogger.BeginScope` is backed by `LoggerMessage.DefineScope`, which allows generation of strongly-typed, precompiled delegates with up to 6 parameters. This enables high-performance and allocation-free scope creation, similar to how `LoggerMessage.Define` is used for logging methods.

The main difference is that `ILogger.BeginScope` **with only one parameter** is an instance method, not an extension method. This means we cannot intercept or replace calls that only use a single message argument (i.e., `BeginScope("Starting operation")`) because the instance method takes precedence over extension methods. Therefore, generation will be limited to `BeginScope` calls with one or more structured parameters (i.e., key-value pairs or anonymous objects), which typically benefit the most from strongly-typed scope generation.
[Reference test](https://github.com/stbychkov/AutoLoggerMessage/blob/main/tests/AutoLoggerMessageGenerator.UnitTests/MethodSpecificityRules/InstanceCallVsExtensionCallTests.cs)

### Decision:

Extend the AutoLoggerMessage source generator to support generation of strongly-typed scope delegates using `LoggerMessage.DefineScope`.

Specifically:
- Identify all usages of `ILogger.BeginScope` in the codebase where the call includes **at least one structured argument** (excluding pure string messages).
- For each identified scope usage:
- Generate a static readonly field that contains the compiled scope delegate using `LoggerMessage.DefineScope`.
- Generate an extension method (or internal interceptor method) that redirects the original `BeginScope` call to the generated delegate, preserving structure and performance.
- Ensure that the generated methods follow the same naming, visibility, and partial class strategy as existing `Log*` method interceptors.

### Consequences:

* **Short-term**:
- Improves performance and reduces allocations for scoped logging with parameters.
- Introduces new source generation complexity; testing must be extended to validate generated scopes.

* **Long-term**:
- Moves the library closer to complete compile-time safety for all common logging patterns (`Log*` and `BeginScope`).

* **Risks**:
- May cause confusion if developers attempt to use `BeginScope(string message)` expecting interception (which is not supported).
- Reliance on exact call shapes (number and types of arguments) may introduce fragility unless thoroughly tested.

* **Maintenance**:
- Must track and test against future changes in `LoggerMessage.DefineScope` API (currently supports up to 6 parameters).
- Increases the surface area of generated code, potentially impacting future refactors or compatibility with downstream tools.

### Alternatives Considered

* **Do nothing**: Keep relying on `ILogger.BeginScope` as is. This maintains simplicity but misses out on performance and compile-time safety.
Loading