diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..acd3bc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +# If this file is renamed, the incrementing run attempt number will be reset. + +name: CI + +on: + push: + branches: [ "dev", "main" ] + pull_request: + branches: [ "dev", "main" ] + +env: + CI_BUILD_NUMBER_BASE: ${{ github.run_number }} + CI_TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + +jobs: + build: + + # The build must run on Windows so that .NET Framework targets can be built and tested. + runs-on: windows-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Setup + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Compute build number + shell: bash + run: | + echo "CI_BUILD_NUMBER=$(($CI_BUILD_NUMBER_BASE+2300))" >> $GITHUB_ENV + - name: Build and Publish + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: pwsh + run: | + ./Build.ps1 diff --git a/.gitignore b/.gitignore index 7b17e06..febeee0 100644 --- a/.gitignore +++ b/.gitignore @@ -200,4 +200,7 @@ project.lock.json # JetBrains Rider .idea -.vscode \ No newline at end of file +.vscode + +.DS_Store + diff --git a/Build.ps1 b/Build.ps1 index 431f4e2..e798284 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,58 +1,79 @@ -echo "build: Build started" +Write-Output "build: Tool versions follow" -$env:Path = "$pwd/.dotnetcli;$env:Path" +dotnet --version +dotnet --list-sdks + +Write-Output "build: Build started" Push-Location $PSScriptRoot +try { + if(Test-Path .\artifacts) { + Write-Output "build: Cleaning ./artifacts" + Remove-Item ./artifacts -Force -Recurse + } -if(Test-Path .\artifacts) { - echo "build: Cleaning .\artifacts" - Remove-Item .\artifacts -Force -Recurse -} + & dotnet restore --no-cache -& dotnet restore --no-cache + $dbp = [Xml] (Get-Content .\Directory.Version.props) + $versionPrefix = $dbp.Project.PropertyGroup.VersionPrefix -$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; -$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; -$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"] + Write-Output "build: Package version prefix is $versionPrefix" -echo "build: Version suffix is $suffix" + $branch = @{ $true = $env:CI_TARGET_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:CI_TARGET_BRANCH]; + $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:CI_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:CI_BUILD_NUMBER]; + $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)) -replace '([^a-zA-Z0-9\-]*)', '')-$revision"}[$branch -eq "main" -and $revision -ne "local"] + $commitHash = $(git rev-parse --short HEAD) + $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] -foreach ($src in ls src/*) { - Push-Location $src + Write-Output "build: Package version suffix is $suffix" + Write-Output "build: Build version suffix is $buildSuffix" - echo "build: Packaging project in $src" + & dotnet build -c Release --version-suffix=$buildSuffix /p:ContinuousIntegrationBuild=true + if($LASTEXITCODE -ne 0) { throw "Build failed" } - if($suffix) { - & dotnet pack -c Release --include-source -o ..\..\artifacts --version-suffix=$suffix - } else { - & dotnet pack -c Release --include-source -o ..\..\artifacts - } + foreach ($src in Get-ChildItem src/*) { + Push-Location $src - if($LASTEXITCODE -ne 0) { throw "build failed" } + Write-Output "build: Packaging project in $src" - Pop-Location -} + if ($suffix) { + & dotnet pack -c Release --no-build --no-restore -o ../../artifacts --version-suffix=$suffix + } else { + & dotnet pack -c Release --no-build --no-restore -o ../../artifacts + } + if($LASTEXITCODE -ne 0) { throw "Packaging failed" } + + Pop-Location + } -foreach ($test in ls test/*.PerformanceTests) { - Push-Location $test + foreach ($test in Get-ChildItem test/*.Tests) { + Push-Location $test - echo "build: Building performance test project in $test" + Write-Output "build: Testing project in $test" - & dotnet build -c Release - if($LASTEXITCODE -ne 0) { throw "test failed" } + & dotnet test -c Release --no-build --no-restore + if($LASTEXITCODE -ne 0) { throw "Testing failed" } - Pop-Location -} + Pop-Location + } -foreach ($test in ls test/*.Tests) { - Push-Location $test + if ($env:NUGET_API_KEY) { + # GitHub Actions will only supply this to branch builds and not PRs. We publish + # builds from any branch this action targets (i.e. main and dev). - echo "build: Testing project in $test" + Write-Output "build: Publishing NuGet packages" - & dotnet test -c Release - if($LASTEXITCODE -ne 0) { throw "test failed" } + foreach ($nupkg in Get-ChildItem artifacts/*.nupkg) { + & dotnet nuget push -k $env:NUGET_API_KEY -s https://api.nuget.org/v3/index.json "$nupkg" + if($LASTEXITCODE -ne 0) { throw "Publishing failed" } + } + if (!($suffix)) { + Write-Output "build: Creating release for version $versionPrefix" + + iex "gh release create v$versionPrefix --title v$versionPrefix --generate-notes $(get-item ./artifacts/*.nupkg) $(get-item ./artifacts/*.snupkg)" + } + } +} finally { Pop-Location } - -Pop-Location diff --git a/Directory.Build.props b/Directory.Build.props index 2b620bc..c114992 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,12 +1,25 @@ - + + latest True - true + + true $(MSBuildThisFileDirectory)assets/Serilog.snk - true + false enable + enable + true + true + true + true + snupkg - + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets deleted file mode 100644 index faf2349..0000000 --- a/Directory.Build.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Directory.Version.props b/Directory.Version.props new file mode 100644 index 0000000..3077571 --- /dev/null +++ b/Directory.Version.props @@ -0,0 +1,6 @@ + + + + 9.0.0 + + diff --git a/README.md b/README.md index bbed04e..ff1278c 100644 --- a/README.md +++ b/README.md @@ -28,41 +28,41 @@ using Serilog; public class Startup { - public Startup(IHostingEnvironment env) - { - Log.Logger = new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); - - // Other startup code + public Startup(IHostingEnvironment env) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + // Other startup code ``` **Finally, for .NET Core 2.0+**, in your `Startup` class's `Configure()` method, remove the existing logger configuration entries and call `AddSerilog()` on the provided `loggingBuilder`. ```csharp - public void ConfigureServices(IServiceCollection services) - { - services.AddLogging(loggingBuilder => - loggingBuilder.AddSerilog(dispose: true)); +public void ConfigureServices(IServiceCollection services) +{ + services.AddLogging(loggingBuilder => + loggingBuilder.AddSerilog(dispose: true)); - // Other services ... - } + // Other services ... +} ``` **For .NET Core 1.0 or 1.1**, in your `Startup` class's `Configure()` method, remove the existing logger configuration entries and call `AddSerilog()` on the provided `loggerFactory`. ``` - public void Configure(IApplicationBuilder app, - IHostingEnvironment env, - ILoggerFactory loggerfactory, - IApplicationLifetime appLifetime) - { - loggerfactory.AddSerilog(); - - // Ensure any buffered events are sent at shutdown - appLifetime.ApplicationStopped.Register(Log.CloseAndFlush); +public void Configure(IApplicationBuilder app, + IHostingEnvironment env, + ILoggerFactory loggerfactory, + IApplicationLifetime appLifetime) +{ + loggerfactory.AddSerilog(); + + // Ensure any buffered events are sent at shutdown + appLifetime.ApplicationStopped.Register(Log.CloseAndFlush); ``` That's it! With the level bumped up a little you should see log output like: @@ -80,6 +80,19 @@ That's it! With the level bumped up a little you should see log output like: [22:14:45.741 DBG] Handled. Status code: 304 File: /css/site.css ``` +### Including the log category in text-format sink output +All _Microsoft.Extensions.Logging.ILogger_ implementations are created with a specified [_log category_](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line#log-category) string, which is then attached as structured data to each log message created by that `ILogger` instance. Typically, the log category is the fully-qualified name of the class generating the log messages. This convention is implemented by the [`ILogger`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) interface, which is commonly used as an injected dependency in frameworks that use _Microsoft.Extensions.Logging_. + +_Serilog.Extensions.Logging_ captures the `ILogger`'s log category, but it's not included in the default output templates for text-based sinks, such as [Console](https://github.com/serilog/serilog-sinks-console), [File](https://github.com/serilog/serilog-sinks-file) and [Debug](https://github.com/serilog/serilog-sinks-debug). + +To include the log category in the final written messages, add the `{SourceContext}` named hole to a customised `outputTemplate` parameter value when configuring the relevant sink(s). For example: +```csharp +.WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") +.WriteTo.File("log.txt", + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}") +``` + ### Notes on Log Scopes _Microsoft.Extensions.Logging_ provides the `BeginScope` API, which can be used to add arbitrary properties to log events within a certain region of code. The API comes in two forms: @@ -90,7 +103,8 @@ _Microsoft.Extensions.Logging_ provides the `BeginScope` API, which can be used Using the extension method will add a `Scope` property to your log events. This is most useful for adding simple "scope strings" to your events, as in the following code: ```csharp -using (_logger.BeginScope("Transaction")) { +using (_logger.BeginScope("Transaction")) +{ _logger.LogInformation("Beginning..."); _logger.LogInformation("Completed in {DurationMs}ms...", 30); } @@ -102,8 +116,9 @@ using (_logger.BeginScope("Transaction")) { If you simply want to add a "bag" of additional properties to your log events, however, this extension method approach can be overly verbose. For example, to add `TransactionId` and `ResponseJson` properties to your log events, you would have to do something like the following: ```csharp -// WRONG! Prefer the dictionary approach below instead -using (_logger.BeginScope("TransactionId: {TransactionId}, ResponseJson: {ResponseJson}", 12345, jsonString)) { +// WRONG! Prefer the dictionary or value tuple approach below instead +using (_logger.BeginScope("TransactionId: {TransactionId}, ResponseJson: {ResponseJson}", 12345, jsonString)) +{ _logger.LogInformation("Completed in {DurationMs}ms...", 30); } // Example JSON output: @@ -125,11 +140,13 @@ Moreover, the template string within `BeginScope` is rather arbitrary when all y A far better alternative is to use the `BeginScope(TState state)` method. If you provide any `IEnumerable>` to this method, then Serilog will output the key/value pairs as structured properties _without_ the `Scope` property, as in this example: ```csharp -var scopeProps = new Dictionary { +var scopeProps = new Dictionary +{ { "TransactionId", 12345 }, { "ResponseJson", jsonString }, }; -using (_logger.BeginScope(scopeProps) { +using (_logger.BeginScope(scopeProps) +{ _logger.LogInformation("Transaction completed in {DurationMs}ms...", 30); } // Example JSON output: @@ -144,6 +161,25 @@ using (_logger.BeginScope(scopeProps) { // } ``` +Alternatively provide a `ValueTuple` to this method, where `Item1` is the property name and `Item2` is the property value. +Note that `T2` _must_ be `object?` if your target platform is net462 or netstandard2.0. + +```csharp +using (_logger.BeginScope(("TransactionId", 12345)) +{ + _logger.LogInformation("Transaction completed in {DurationMs}ms...", 30); +} +// Example JSON output: +// { +// "@t":"2020-10-29T19:05:56.4176816Z", +// "@m":"Completed in 30ms...", +// "@i":"51812baa", +// "DurationMs":30, +// "SourceContext":"SomeNamespace.SomeService", +// "TransactionId": 12345 +// } +``` + ### Versioning This package tracks the versioning and target framework support of its [_Microsoft.Extensions.Logging_](https://nuget.org/packages/Microsoft.Extensions.Logging) dependency. diff --git a/Setup.ps1 b/Setup.ps1 deleted file mode 100644 index aa90b5b..0000000 --- a/Setup.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -$ErrorActionPreference = "Stop" - -$RequiredDotnetVersion = $(cat ./global.json | convertfrom-json).sdk.version - -New-Item -ItemType Directory -Force "./build/" | Out-Null - -Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./build/installcli.ps1" -& ./build/installcli.ps1 -InstallDir "$pwd/.dotnetcli" -NoPath -Version $RequiredDotnetVersion -if ($LASTEXITCODE) { throw ".NET install failed" } diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index ae5bee4..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: '{build}' -skip_tags: true -image: Visual Studio 2022 -install: -- pwsh: ./Setup.ps1 -- pwsh: mkdir -Force ".\build\" | Out-Null -build_script: -- pwsh: ./Build.ps1 -test: off -artifacts: -- path: artifacts/Serilog.*.nupkg -deploy: -- provider: NuGet - api_key: - secure: EN9f+XXE3fW+ebL4wxrIbafdtbNvRfddBN8UUixvctYh4qMBHzr1JdnM83QsM1zo - skip_symbols: true - on: - branch: /^(main|dev)$/ -- provider: GitHub - auth_token: - secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX - artifact: /Serilog.*\.nupkg/ - tag: v$(appveyor_build_version) - on: - branch: main - diff --git a/build.sh b/build.sh deleted file mode 100644 index 6d1ff38..0000000 --- a/build.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -dotnet --info -dotnet restore - -for path in src/**/*.csproj; do - dotnet build -f netstandard2.0 -c Release ${path} - -done - -for path in test/*.Tests/*.csproj; do - dotnet test -f netcoreapp2.0 -c Release ${path} -done \ No newline at end of file diff --git a/global.json b/global.json index 5ce8495..db8627a 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,7 @@ { "sdk": { - "version": "8.0.100" + "version": "9.0.100", + "allowPrerelease": false, + "rollForward": "latestFeature" } } diff --git a/samples/Sample/Program.cs b/samples/Sample/Program.cs index d70b9aa..b369510 100644 --- a/samples/Sample/Program.cs +++ b/samples/Sample/Program.cs @@ -3,33 +3,16 @@ using Serilog; using Serilog.Extensions.Logging; -// Creating a `LoggerProviderCollection` lets Serilog optionally write -// events through other dynamically-added MEL ILoggerProviders. -var providers = new LoggerProviderCollection(); - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() .WriteTo.Console() - .WriteTo.Providers(providers) .CreateLogger(); var services = new ServiceCollection(); -services.AddSingleton(providers); -services.AddSingleton(sc => -{ - var providerCollection = sc.GetService(); - var factory = new SerilogLoggerFactory(null, true, providerCollection); - - foreach (var provider in sc.GetServices()) - factory.AddProvider(provider); - - return factory; -}); - -services.AddLogging(l => l.AddConsole()); +services.AddLogging(); +services.AddSingleton(new SerilogLoggerFactory()); -var serviceProvider = services.BuildServiceProvider(); +using var serviceProvider = services.BuildServiceProvider(); var logger = serviceProvider.GetRequiredService>(); var startTime = DateTimeOffset.UtcNow; @@ -65,4 +48,3 @@ logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "------", "----- ----", "--- ----", "------------"); logger.LogInformation("{Result,-10:l}{StartTime,15:mm:s tt}{EndTime,15:mm:s tt}{Duration,15}", "SUCCESS", startTime, endTime, (endTime - startTime).TotalMilliseconds); -serviceProvider.Dispose(); diff --git a/samples/Sample/Properties/launchSettings.json b/samples/Sample/Properties/launchSettings.json deleted file mode 100644 index 43e0c99..0000000 --- a/samples/Sample/Properties/launchSettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "profiles": {} -} \ No newline at end of file diff --git a/samples/Sample/Sample.csproj b/samples/Sample/Sample.csproj index 951b742..793b13a 100644 --- a/samples/Sample/Sample.csproj +++ b/samples/Sample/Sample.csproj @@ -1,10 +1,8 @@  - net8.0 - Sample + net9.0 Exe - enable @@ -12,9 +10,9 @@ - - - + + + diff --git a/samples/SampleWithExternalScope/Program.cs b/samples/SampleWithExternalScope/Program.cs new file mode 100644 index 0000000..2b0aad7 --- /dev/null +++ b/samples/SampleWithExternalScope/Program.cs @@ -0,0 +1,55 @@ +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Formatting.Json; + +// Configure a JsonFormatter to log out scope to the console +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console(new JsonFormatter()) + .CreateLogger(); + +// Setup Serilog with M.E.L, and configure the appropriate ActivityTrackingOptions +var services = new ServiceCollection(); + +services.AddLogging(l => l + .AddSerilog() + .Configure(options => + { + options.ActivityTrackingOptions = + ActivityTrackingOptions.SpanId + | ActivityTrackingOptions.TraceId + | ActivityTrackingOptions.ParentId + | ActivityTrackingOptions.TraceState + | ActivityTrackingOptions.TraceFlags + | ActivityTrackingOptions.Tags + | ActivityTrackingOptions.Baggage; + })); + +// Add an ActivityListener (required, otherwise Activities don't actually get created if nothing is listening to them) +ActivitySource.AddActivityListener(new ActivityListener +{ + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded +}); + +// Run our test +var activitySource = new ActivitySource("SomeActivitySource"); + +using var serviceProvider = services.BuildServiceProvider(); +var logger = serviceProvider.GetRequiredService>(); + +using var activity = activitySource.StartActivity(); + +activity?.SetTag("tag.domain.id", 1234); +activity?.SetBaggage("baggage.environment", "uat"); + +using var scope = logger.BeginScope(new +{ + User = "Hugh Mann", + Time = DateTimeOffset.UtcNow +}); + +logger.LogInformation("Hello world!"); + diff --git a/samples/SampleWithExternalScope/SampleWithExternalScope.csproj b/samples/SampleWithExternalScope/SampleWithExternalScope.csproj new file mode 100644 index 0000000..84cb0ac --- /dev/null +++ b/samples/SampleWithExternalScope/SampleWithExternalScope.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0 + + + + + + + + + + + + + diff --git a/samples/SampleWithMelProviders/Program.cs b/samples/SampleWithMelProviders/Program.cs new file mode 100644 index 0000000..c8b88b1 --- /dev/null +++ b/samples/SampleWithMelProviders/Program.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Extensions.Logging; + +// Creating a `LoggerProviderCollection` lets Serilog optionally write +// events through other dynamically-added MEL ILoggerProviders. +var providers = new LoggerProviderCollection(); + +// The sample sets up Serilog's console sink here: +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .WriteTo.Providers(providers) + .CreateLogger(); + +var services = new ServiceCollection(); + +services.AddSingleton(providers); +services.AddSingleton(sc => +{ + var providerCollection = sc.GetService(); + var factory = new SerilogLoggerFactory(null, true, providerCollection); + + foreach (var provider in sc.GetServices()) + factory.AddProvider(provider); + + return factory; +}); + +// ..and MEL's console provider here: +services.AddLogging(l => l.AddConsole()); + +using var serviceProvider = services.BuildServiceProvider(); +var logger = serviceProvider.GetRequiredService>(); + +var startTime = DateTimeOffset.UtcNow; +logger.LogInformation(1, "Started at {StartTime} and 0x{Hello:X} is hex of 42", startTime, 42); + +try +{ + throw new Exception("Boom!"); +} +catch (Exception ex) +{ + logger.LogCritical(ex, "Unexpected critical error starting application"); + logger.Log(LogLevel.Critical, 0, "Unexpected critical error", ex, null!); + // This write should not log anything + logger.Log(LogLevel.Critical, 0, null!, null, null!); + logger.LogError(ex, "Unexpected error"); + logger.LogWarning(ex, "Unexpected warning"); +} + +using (logger.BeginScope("Main")) +{ + logger.LogInformation("Waiting for user input"); + var key = Console.Read(); + logger.LogInformation("User pressed {@KeyInfo}", new { Key = key, KeyChar = (char)key }); +} + +var endTime = DateTimeOffset.UtcNow; +logger.LogInformation(2, "Stopping at {StopTime}", endTime); + +logger.LogInformation("Stopping"); + +logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "RESULT", "START TIME", "END TIME", "DURATION(ms)"); +logger.LogInformation("{Result,-10:l}{StartTime,15:l}{EndTime,15:l}{Duration,15:l}", "------", "----- ----", "--- ----", "------------"); +logger.LogInformation("{Result,-10:l}{StartTime,15:mm:s tt}{EndTime,15:mm:s tt}{Duration,15}", "SUCCESS", startTime, endTime, (endTime - startTime).TotalMilliseconds); + diff --git a/samples/SampleWithMelProviders/SampleWithMelProviders.csproj b/samples/SampleWithMelProviders/SampleWithMelProviders.csproj new file mode 100644 index 0000000..793b13a --- /dev/null +++ b/samples/SampleWithMelProviders/SampleWithMelProviders.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + Exe + + + + + + + + + + + + + diff --git a/serilog-extensions-logging.sln b/serilog-extensions-logging.sln index 776ae3c..baca24a 100644 --- a/serilog-extensions-logging.sln +++ b/serilog-extensions-logging.sln @@ -18,19 +18,20 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{9C21B9DF-AEDD-4AA6-BEA4-912DEF3E5B8E}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 Directory.Build.props = Directory.Build.props - Directory.Build.targets = Directory.Build.targets README.md = README.md assets\Serilog.snk = assets\Serilog.snk - build.sh = build.sh - Setup.ps1 = Setup.ps1 global.json = global.json + Directory.Version.props = Directory.Version.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Extensions.Logging.Benchmarks", "test\Serilog.Extensions.Logging.Benchmarks\Serilog.Extensions.Logging.Benchmarks.csproj", "{6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWithExternalScope", "samples\SampleWithExternalScope\SampleWithExternalScope.csproj", "{653092A8-CBAD-40AA-A4CE-F8B19D6492C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWithMelProviders", "samples\SampleWithMelProviders\SampleWithMelProviders.csproj", "{B1454759-126F-4F33-84EE-C8E19541DF79}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,6 +54,14 @@ Global {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2}.Release|Any CPU.Build.0 = Release|Any CPU + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2}.Release|Any CPU.Build.0 = Release|Any CPU + {B1454759-126F-4F33-84EE-C8E19541DF79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1454759-126F-4F33-84EE-C8E19541DF79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1454759-126F-4F33-84EE-C8E19541DF79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1454759-126F-4F33-84EE-C8E19541DF79}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -62,6 +71,8 @@ Global {37EADF84-5E41-4224-A194-1E3299DCD0B8} = {E30F638E-BBBE-4AD1-93CE-48CC69CFEFE1} {65357FBC-9BC4-466D-B621-1C3A19BC2A78} = {F2407211-6043-439C-8E06-3641634332E7} {6D5986FF-EECD-4E75-8BC6-A5F78AB549B2} = {E30F638E-BBBE-4AD1-93CE-48CC69CFEFE1} + {653092A8-CBAD-40AA-A4CE-F8B19D6492C2} = {F2407211-6043-439C-8E06-3641634332E7} + {B1454759-126F-4F33-84EE-C8E19541DF79} = {F2407211-6043-439C-8E06-3641634332E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {811E61C5-3871-4633-AFAE-B35B619C8A10} diff --git a/serilog-extensions-logging.sln.DotSettings b/serilog-extensions-logging.sln.DotSettings index 23a0f99..6fd2e62 100644 --- a/serilog-extensions-logging.sln.DotSettings +++ b/serilog-extensions-logging.sln.DotSettings @@ -1,2 +1,3 @@  + True True \ No newline at end of file diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/CachingMessageTemplateParser.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/CachingMessageTemplateParser.cs index 8966ef7..01b87f6 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/CachingMessageTemplateParser.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/CachingMessageTemplateParser.cs @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. - using Serilog.Events; using Serilog.Parsing; using System.Collections; namespace Serilog.Extensions.Logging; -class CachingMessageTemplateParser +sealed class CachingMessageTemplateParser { readonly MessageTemplateParser _innerParser = new(); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs new file mode 100644 index 0000000..60d7efe --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs @@ -0,0 +1,97 @@ +// Copyright (c) Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Serilog.Extensions.Logging; + +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Events; + +sealed class EventIdPropertyCache +{ + readonly int _maxCachedProperties; + readonly ConcurrentDictionary _propertyCache = new(); + + int _count; + + public EventIdPropertyCache(int maxCachedProperties = 1024) + { + _maxCachedProperties = maxCachedProperties; + } + + public LogEventPropertyValue GetOrCreatePropertyValue(in EventId eventId) + { + var eventKey = new EventKey(eventId); + + LogEventPropertyValue? propertyValue; + + if (_count >= _maxCachedProperties) + { + if (!_propertyCache.TryGetValue(eventKey, out propertyValue)) + { + propertyValue = CreatePropertyValue(in eventKey); + } + } + else + { + if (!_propertyCache.TryGetValue(eventKey, out propertyValue)) + { + // GetOrAdd is moved to a separate method to prevent closure allocation + propertyValue = GetOrAddCore(in eventKey); + } + } + + return propertyValue; + } + + static LogEventPropertyValue CreatePropertyValue(in EventKey eventKey) + { + var properties = new List(2); + + if (eventKey.Id != 0) + { + properties.Add(new LogEventProperty("Id", new ScalarValue(eventKey.Id))); + } + + if (eventKey.Name != null) + { + properties.Add(new LogEventProperty("Name", new ScalarValue(eventKey.Name))); + } + + return new StructureValue(properties); + } + + LogEventPropertyValue GetOrAddCore(in EventKey eventKey) => + _propertyCache.GetOrAdd( + eventKey, + key => + { + Interlocked.Increment(ref _count); + + return CreatePropertyValue(in key); + }); + + readonly record struct EventKey + { + public EventKey(EventId eventId) + { + Id = eventId.Id; + Name = eventId.Name; + } + + public int Id { get; } + + public string? Name { get; } + } +} diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LevelConvert.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LevelConvert.cs index a77494e..cce77da 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/LevelConvert.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LevelConvert.cs @@ -36,7 +36,8 @@ public static LogEventLevel ToSerilogLevel(LogLevel logLevel) { return logLevel switch { - LogLevel.None or LogLevel.Critical => LogEventLevel.Fatal, + LogLevel.None => LevelAlias.Off, + LogLevel.Critical => LogEventLevel.Fatal, LogLevel.Error => LogEventLevel.Error, LogLevel.Warning => LogEventLevel.Warning, LogLevel.Information => LogEventLevel.Information, diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs index b944040..8799d7b 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollection.cs @@ -20,9 +20,9 @@ namespace Serilog.Extensions.Logging; /// /// A dynamically-modifiable collection of s. /// -public class LoggerProviderCollection : IDisposable +public sealed class LoggerProviderCollection : IDisposable { - volatile ILoggerProvider[] _providers = Array.Empty(); + volatile ILoggerProvider[] _providers = []; /// /// Add to the collection. @@ -37,7 +37,7 @@ public void AddProvider(ILoggerProvider provider) do { existing = _providers; - added = existing.Concat(new[] { provider }).ToArray(); + added = [..existing, provider]; } #pragma warning disable 420 // ref to a volatile field while (Interlocked.CompareExchange(ref _providers, added, existing) != existing); diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs index 0febb52..5609905 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/LoggerProviderCollectionSink.cs @@ -18,7 +18,7 @@ namespace Serilog.Extensions.Logging; -class LoggerProviderCollectionSink : ILogEventSink, IDisposable +sealed class LoggerProviderCollectionSink : ILogEventSink, IDisposable { readonly LoggerProviderCollection _providers; diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs index 20253f8..287608b 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogValues.cs @@ -44,10 +44,7 @@ public SerilogLogValues(MessageTemplate messageTemplate, IReadOnlyDictionary("{OriginalFormat}", _messageTemplate.Text); } - public KeyValuePair this[int index] - { - get => _values[index]; - } + public KeyValuePair this[int index] => _values[index]; public int Count => _properties.Count + 1; diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs index 06efe62..cae926c 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs @@ -12,7 +12,7 @@ namespace Serilog.Extensions.Logging; -class SerilogLogger : FrameworkLogger +sealed class SerilogLogger : FrameworkLogger { internal static readonly ConcurrentDictionary DestructureDictionary = new(); internal static readonly ConcurrentDictionary StringifyDictionary = new(); @@ -28,14 +28,10 @@ internal static string GetKeyWithoutFirstSymbol(ConcurrentDictionary new LogEventProperty("Id", new ScalarValue(n))) - .ToArray(); - public SerilogLogger( SerilogLoggerProvider provider, ILogger? logger = null, @@ -93,30 +89,33 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { string? messageTemplate = null; - var properties = new List(); + var properties = new Dictionary(); if (state is IEnumerable> structure) { foreach (var property in structure) { - if (property.Key == SerilogLoggerProvider.OriginalFormatPropertyName && property.Value is string value) + if (property is { Key: SerilogLoggerProvider.OriginalFormatPropertyName, Value: string value }) { messageTemplate = value; } - else if (property.Key.StartsWith("@")) + else if (property.Key.StartsWith('@')) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(DestructureDictionary, property.Key), property.Value, true, out var destructured)) - properties.Add(destructured); + properties[destructured.Name] = destructured.Value; } - else if (property.Key.StartsWith("$")) + else if (property.Key.StartsWith('$')) { if (_logger.BindProperty(GetKeyWithoutFirstSymbol(StringifyDictionary, property.Key), property.Value?.ToString(), true, out var stringified)) - properties.Add(stringified); + properties[stringified.Name] = stringified.Value; } else { - if (_logger.BindProperty(property.Key, property.Value, false, out var bound)) - properties.Add(bound); + // Simple micro-optimization for the most common and reliably scalar values; could go further here. + if (property.Value is null or string or int or long && LogEventProperty.IsValidName(property.Key)) + properties[property.Key] = new ScalarValue(property.Value); + else if (_logger.BindProperty(property.Key, property.Value, false, out var bound)) + properties[bound.Name] = bound.Value; } } @@ -127,7 +126,7 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state { messageTemplate = "{" + stateType.Name + ":l}"; if (_logger.BindProperty(stateType.Name, AsLoggableValue(state, formatter), false, out var stateTypeProperty)) - properties.Add(stateTypeProperty); + properties[stateTypeProperty.Name] = stateTypeProperty.Value; } } @@ -150,47 +149,27 @@ LogEvent PrepareWrite(LogEventLevel level, EventId eventId, TState state if (propertyName != null) { if (_logger.BindProperty(propertyName, AsLoggableValue(state, formatter!), false, out var property)) - properties.Add(property); + properties[property.Name] = property.Value; } } + // The overridden `!=` operator on this type ignores `Name`. if (eventId.Id != 0 || eventId.Name != null) - properties.Add(CreateEventIdProperty(eventId)); + properties.Add("EventId", _eventIdPropertyCache.GetOrCreatePropertyValue(in eventId)); var (traceId, spanId) = Activity.Current is { } activity ? (activity.TraceId, activity.SpanId) : (default(ActivityTraceId), default(ActivitySpanId)); - var parsedTemplate = MessageTemplateParser.Parse(messageTemplate ?? ""); - return new LogEvent(DateTimeOffset.Now, level, exception, parsedTemplate, properties, traceId, spanId); + var parsedTemplate = messageTemplate != null ? MessageTemplateParser.Parse(messageTemplate) : MessageTemplate.Empty; + return LogEvent.UnstableAssembleFromParts(DateTimeOffset.Now, level, exception, parsedTemplate, properties, traceId, spanId); } static object? AsLoggableValue(TState state, Func? formatter) { - object? stateObj = state; + object? stateObj = null; if (formatter != null) stateObj = formatter(state, null); - return stateObj; - } - - internal static LogEventProperty CreateEventIdProperty(EventId eventId) - { - var properties = new List(2); - - if (eventId.Id != 0) - { - if (eventId.Id >= 0 && eventId.Id < LowEventIdValues.Length) - // Avoid some allocations - properties.Add(LowEventIdValues[eventId.Id]); - else - properties.Add(new LogEventProperty("Id", new ScalarValue(eventId.Id))); - } - - if (eventId.Name != null) - { - properties.Add(new LogEventProperty("Name", new ScalarValue(eventId.Name))); - } - - return new LogEventProperty("EventId", new StructureValue(properties)); + return stateObj ?? state; } } diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs index b85dbcb..12ece4c 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerFactory.cs @@ -20,7 +20,7 @@ namespace Serilog.Extensions.Logging; /// /// A complete Serilog-backed implementation of the .NET Core logging infrastructure. /// -public class SerilogLoggerFactory : ILoggerFactory +public sealed class SerilogLoggerFactory : ILoggerFactory { readonly LoggerProviderCollection? _providerCollection; readonly SerilogLoggerProvider _provider; diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs index 3fead38..d6b18d1 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerProvider.cs @@ -13,7 +13,10 @@ namespace Serilog.Extensions.Logging; /// An that pipes events through Serilog. /// [ProviderAlias("Serilog")] -public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher +public sealed class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher, ISupportExternalScope +#if FEATURE_ASYNCDISPOSABLE + , IAsyncDisposable +#endif { internal const string OriginalFormatPropertyName = "{OriginalFormat}"; internal const string ScopePropertyName = "Scope"; @@ -21,6 +24,10 @@ public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher // May be null; if it is, Log.Logger will be lazily used readonly ILogger? _logger; readonly Action? _dispose; +#if FEATURE_ASYNCDISPOSABLE + readonly Func? _disposeAsync; +#endif + IExternalScopeProvider? _externalScopeProvider; /// /// Construct a . @@ -30,14 +37,34 @@ public class SerilogLoggerProvider : ILoggerProvider, ILogEventEnricher public SerilogLoggerProvider(ILogger? logger = null, bool dispose = false) { if (logger != null) - _logger = logger.ForContext(new[] { this }); + _logger = logger.ForContext([this]); if (dispose) { if (logger != null) + { _dispose = () => (logger as IDisposable)?.Dispose(); +#if FEATURE_ASYNCDISPOSABLE + _disposeAsync = async () => + { + if (logger is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + (logger as IDisposable)?.Dispose(); + } + }; +#endif + } else + { _dispose = Log.CloseAndFlush; +#if FEATURE_ASYNCDISPOSABLE + _disposeAsync = Log.CloseAndFlushAsync; +#endif + } } } @@ -70,11 +97,22 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) if (scopeItem != null) { - scopeItems ??= new List(); + scopeItems ??= []; scopeItems.Add(scopeItem); } } + _externalScopeProvider?.ForEachScope((state, accumulatingLogEvent) => + { + SerilogLoggerScope.EnrichWithStateAndCreateScopeItem(accumulatingLogEvent, propertyFactory, state, out var scopeItem); + + if (scopeItem != null) + { + scopeItems ??= new List(); + scopeItems.Add(scopeItem); + } + }, logEvent); + if (scopeItems != null) { scopeItems.Reverse(); @@ -82,6 +120,12 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) } } + /// + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _externalScopeProvider = scopeProvider; + } + readonly AsyncLocal _value = new(); internal SerilogLoggerScope? CurrentScope @@ -95,4 +139,12 @@ public void Dispose() { _dispose?.Invoke(); } + +#if FEATURE_ASYNCDISPOSABLE + /// + public ValueTask DisposeAsync() + { + return _disposeAsync?.Invoke() ?? default; + } +#endif } diff --git a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs index 9d13190..b03e3e1 100644 --- a/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs +++ b/src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLoggerScope.cs @@ -6,7 +6,7 @@ namespace Serilog.Extensions.Logging; -class SerilogLoggerScope : IDisposable +sealed class SerilogLoggerScope : IDisposable { const string NoName = "None"; @@ -47,63 +47,77 @@ public void Dispose() } } - public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, out LogEventPropertyValue? scopeItem) - { - void AddProperty(KeyValuePair stateProperty) - { - var key = stateProperty.Key; - var destructureObject = false; - var value = stateProperty.Value; - - if (key.StartsWith("@")) - { - key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.DestructureDictionary, key); - destructureObject = true; - } - else if (key.StartsWith("$")) - { - key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.StringifyDictionary, key); - value = value?.ToString(); - } - - var property = propertyFactory.CreateProperty(key, value, destructureObject); - logEvent.AddPropertyIfAbsent(property); - } + public void EnrichAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, out LogEventPropertyValue? scopeItem) => EnrichWithStateAndCreateScopeItem(logEvent, propertyFactory, _state, out scopeItem); - if (_state == null) + public static void EnrichWithStateAndCreateScopeItem(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, object? state, out LogEventPropertyValue? scopeItem) + { + if (state == null) { scopeItem = null; return; } // Eliminates boxing of Dictionary.Enumerator for the most common use case - if (_state is Dictionary dictionary) + if (state is Dictionary dictionary) { - scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. - + // Separate handling of this case eliminates boxing of Dictionary.Enumerator. + scopeItem = null; foreach (var stateProperty in dictionary) - { - if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) - scopeItem = new ScalarValue(_state.ToString()); - else - AddProperty(stateProperty); + { + AddProperty(logEvent, propertyFactory, stateProperty.Key, stateProperty.Value); } } - else if (_state is IEnumerable> stateProperties) + else if (state is IEnumerable> stateProperties) { - scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. - + scopeItem = null; foreach (var stateProperty in stateProperties) { - if (stateProperty.Key == SerilogLoggerProvider.OriginalFormatPropertyName && stateProperty.Value is string) - scopeItem = new ScalarValue(_state.ToString()); + if (stateProperty is { Key: SerilogLoggerProvider.OriginalFormatPropertyName, Value: string }) + { + // `_state` is most likely `FormattedLogValues` (a MEL internal type). + scopeItem = new ScalarValue(state.ToString()); + } else - AddProperty(stateProperty); + { + AddProperty(logEvent, propertyFactory, stateProperty.Key, stateProperty.Value); + } } } +#if FEATURE_ITUPLE + else if (state is System.Runtime.CompilerServices.ITuple tuple && tuple.Length == 2 && tuple[0] is string s) + { + scopeItem = null; // Unless it's `FormattedLogValues`, these are treated as property bags rather than scope items. + AddProperty(logEvent, propertyFactory, s, tuple[1]); + } +#else + else if (state is ValueTuple tuple) + { + scopeItem = null; + AddProperty(logEvent, propertyFactory, tuple.Item1, tuple.Item2); + } +#endif else { - scopeItem = propertyFactory.CreateProperty(NoName, _state).Value; + scopeItem = propertyFactory.CreateProperty(NoName, state).Value; + } + } + + static void AddProperty(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, string key, object? value) + { + var destructureObject = false; + + if (key.StartsWith('@')) + { + key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.DestructureDictionary, key); + destructureObject = true; + } + else if (key.StartsWith('$')) + { + key = SerilogLogger.GetKeyWithoutFirstSymbol(SerilogLogger.StringifyDictionary, key); + value = value?.ToString(); } + + var property = propertyFactory.CreateProperty(key, value, destructureObject); + logEvent.AddPropertyIfAbsent(property); } } diff --git a/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs b/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs new file mode 100644 index 0000000..ac772fd --- /dev/null +++ b/src/Serilog.Extensions.Logging/Extensions/StringExtensions.cs @@ -0,0 +1,14 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER +using System.Runtime.CompilerServices; + +namespace Serilog.Extensions; + +static class StringExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool StartsWith(this string str, char value) + { + return str.Length > 0 && str[0] == value; + } +} +#endif diff --git a/src/Serilog.Extensions.Logging/Properties/AssemblyInfo.cs b/src/Serilog.Extensions.Logging/Properties/AssemblyInfo.cs index 50afb45..468e64f 100644 --- a/src/Serilog.Extensions.Logging/Properties/AssemblyInfo.cs +++ b/src/Serilog.Extensions.Logging/Properties/AssemblyInfo.cs @@ -6,8 +6,6 @@ using System.Reflection; using System.Runtime.CompilerServices; -[assembly: AssemblyVersion("7.0.0.0")] - [assembly: InternalsVisibleTo("Serilog.Extensions.Logging.Tests, PublicKey=" + "0024000004800000940000000602000000240000525341310004000001000100fb8d13fd344a1c" + "6fe0fe83ef33c1080bf30690765bc6eb0df26ebfdf8f21670c64265b30db09f73a0dea5b3db4c9" + diff --git a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj index c37866f..3030973 100644 --- a/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj +++ b/src/Serilog.Extensions.Logging/Serilog.Extensions.Logging.csproj @@ -2,24 +2,16 @@ Low-level Serilog provider for Microsoft.Extensions.Logging - - 8.0.0 Microsoft;Serilog Contributors - net462;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 + net462;netstandard2.0;netstandard2.1;net8.0;net9.0 true serilog;Microsoft.Extensions.Logging serilog-extension-nuget.png https://github.com/serilog/serilog-extensions-logging Apache-2.0 - false Serilog - git - embedded - true - true - True README.md NU5118 @@ -28,11 +20,21 @@ - - + - + + + $(DefineConstants);FEATURE_ITUPLE + + + + $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE + + + + $(DefineConstants);FEATURE_ITUPLE;FEATURE_ASYNCDISPOSABLE + diff --git a/src/Serilog.Extensions.Logging/SerilogLoggingBuilderExtensions.cs b/src/Serilog.Extensions.Logging/SerilogLoggingBuilderExtensions.cs index 1757de0..e7fe105 100644 --- a/src/Serilog.Extensions.Logging/SerilogLoggingBuilderExtensions.cs +++ b/src/Serilog.Extensions.Logging/SerilogLoggingBuilderExtensions.cs @@ -38,7 +38,7 @@ public static ILoggingBuilder AddSerilog(this ILoggingBuilder builder, ILogger? if (dispose) { - builder.Services.AddSingleton(services => new SerilogLoggerProvider(logger, true)); + builder.Services.AddSingleton(_ => new SerilogLoggerProvider(logger, true)); } else { diff --git a/test/Serilog.Extensions.Logging.Benchmarks/LogEventConstructionBenchmark.cs b/test/Serilog.Extensions.Logging.Benchmarks/EventIdCapturingBenchmark.cs similarity index 92% rename from test/Serilog.Extensions.Logging.Benchmarks/LogEventConstructionBenchmark.cs rename to test/Serilog.Extensions.Logging.Benchmarks/EventIdCapturingBenchmark.cs index 415867a..a16f38f 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/LogEventConstructionBenchmark.cs +++ b/test/Serilog.Extensions.Logging.Benchmarks/EventIdCapturingBenchmark.cs @@ -23,7 +23,7 @@ namespace Serilog.Extensions.Logging.Benchmarks; [MemoryDiagnoser] -public class LogEventConstructionBenchmark +public class EventIdCapturingBenchmark { readonly IMelLogger _melLogger; readonly ILogger _serilogContextualLogger; @@ -31,11 +31,11 @@ public class LogEventConstructionBenchmark const int LowId = 10, HighId = 101; const string Template = "This is an event"; - public LogEventConstructionBenchmark() + public EventIdCapturingBenchmark() { _sink = new CapturingSink(); var underlyingLogger = new LoggerConfiguration().WriteTo.Sink(_sink).CreateLogger(); - _serilogContextualLogger = underlyingLogger.ForContext(); + _serilogContextualLogger = underlyingLogger.ForContext(); _melLogger = new SerilogLoggerProvider(underlyingLogger).CreateLogger(GetType().FullName!); } @@ -68,7 +68,7 @@ public void Verify() [Fact] public void Benchmark() { - BenchmarkRunner.Run(); + BenchmarkRunner.Run(); } [Benchmark(Baseline = true)] diff --git a/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs b/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs index 995d462..f08738a 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs +++ b/test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs @@ -20,46 +20,112 @@ #pragma warning disable xUnit1013 // Public method should be marked as test -namespace Serilog.Extensions.Logging.Benchmarks +namespace Serilog.Extensions.Logging.Benchmarks; + +[MemoryDiagnoser] +public class LogEventBenchmark { - [MemoryDiagnoser] - public class LogEventBenchmark + class Person + { + public string? Name { get; set; } + public int Age { get; set; } + public override string ToString() => "Fixed text"; + } + + readonly IMelLogger _melLogger; + readonly Person _bob, _alice; + readonly ILogger _underlyingLogger; + readonly EventId _eventId = new EventId(1, "Test"); + + public LogEventBenchmark() + { + _underlyingLogger = new LoggerConfiguration().CreateLogger(); + _melLogger = new SerilogLoggerProvider(_underlyingLogger).CreateLogger(GetType().FullName!); + _bob = new Person { Name = "Bob", Age = 42 }; + _alice = new Person { Name = "Alice", Age = 42 }; + } + + [Fact] + public void Benchmark() + { + BenchmarkRunner.Run(); + } + + [Benchmark(Baseline = true)] + public void SerilogOnly() + { + _underlyingLogger.Information("Hello!"); + } + + [Benchmark] + public void SimpleEvent() + { + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void Template() + { + _melLogger.LogInformation("Hello, {Property1}!", 42); + } + + [Benchmark] + public void StringScope() + { + using var scope = _melLogger.BeginScope("Scope1"); + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void TemplateScope() + { + using var scope = _melLogger.BeginScope("Scope1 {Property1}", 42); + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void TupleScope() + { + using var scope = _melLogger.BeginScope(("Property1", 42)); + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void DictionaryScope() + { + // Note that allocations here include the dictionary and boxed int. + using var scope = _melLogger.BeginScope(new Dictionary { ["Property1"] = 42 }); + _melLogger.LogInformation("Hello!"); + } + + [Benchmark] + public void Capturing() + { + _melLogger.LogInformation("Hi {@User} from {$Me}", _bob, _alice); + } + + [Benchmark] + public void CapturingScope() + { + using var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice); + _melLogger.LogInformation("Hi"); + } + + [Benchmark] + public void LogInformationScoped() + { + using (var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice)) + _melLogger.LogInformation("Hi"); + } + + [Benchmark] + public void LogInformation_WithEventId() { - private class Person - { - public string? Name { get; set; } - public int Age { get; set; } - public override string ToString() => "Fixed text"; - } - - readonly IMelLogger _melLogger; - readonly Person _bob, _alice; - - public LogEventBenchmark() - { - var underlyingLogger = new LoggerConfiguration().CreateLogger(); - _melLogger = new SerilogLoggerProvider(underlyingLogger).CreateLogger(GetType().FullName!); - _bob = new Person { Name = "Bob", Age = 42 }; - _alice = new Person { Name = "Alice", Age = 42 }; - } - - [Fact] - public void Benchmark() - { - BenchmarkRunner.Run(); - } - - [Benchmark] - public void LogInformation() - { - _melLogger.LogInformation("Hi {@User} from {$Me}", _bob, _alice); - } - - [Benchmark] - public void LogInformationScoped() - { - using (var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice)) - _melLogger.LogInformation("Hi"); - } + this._melLogger.Log( + LogLevel.Information, + _eventId, + "Hi {@User} from {$Me}", + _bob, + _alice); } } diff --git a/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj b/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj index 155847e..34aa874 100644 --- a/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj +++ b/test/Serilog.Extensions.Logging.Benchmarks/Serilog.Extensions.Logging.Benchmarks.csproj @@ -1,8 +1,8 @@ - net8.0 - enable + net8.0;net9.0 + false @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs b/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs new file mode 100644 index 0000000..0e7448e --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/DisposeTests.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog.Core; +using Serilog.Events; +using Xunit; + +namespace Serilog.Extensions.Logging.Tests; + +public class DisposeTests +{ + private readonly DisposableSink _sink; + private readonly Logger _serilogLogger; + + public DisposeTests() + { + _sink = new DisposableSink(); + _serilogLogger = new LoggerConfiguration() + .WriteTo.Sink(_sink) + .CreateLogger(); + } + + [Fact] + public void DisposesProviderWhenDisposeIsTrue() + { + var services = new ServiceCollection() + .AddLogging(builder => builder.AddSerilog(logger: _serilogLogger, dispose: true)) + .BuildServiceProvider(); + + // Get a logger so that we ensure SerilogLoggerProvider is created + var logger = services.GetRequiredService>(); + logger.LogInformation("Hello, world!"); + + services.Dispose(); + Assert.True(_sink.DisposeCalled); + Assert.False(_sink.DisposeAsyncCalled); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task DisposesProviderAsyncWhenDisposeIsTrue() + { + var services = new ServiceCollection() + .AddLogging(builder => builder.AddSerilog(logger: _serilogLogger, dispose: true)) + .BuildServiceProvider(); + + // Get a logger so that we ensure SerilogLoggerProvider is created + var logger = services.GetRequiredService>(); + logger.LogInformation("Hello, world!"); + + await services.DisposeAsync(); + Assert.False(_sink.DisposeCalled); + Assert.True(_sink.DisposeAsyncCalled); + } +#endif + + private sealed class DisposableSink : ILogEventSink, IDisposable, IAsyncDisposable + { + public bool DisposeAsyncCalled { get; private set; } + public bool DisposeCalled { get; private set; } + + public void Dispose() => DisposeCalled = true; + public ValueTask DisposeAsync() + { + DisposeAsyncCalled = true; + return default; + } + + public void Emit(LogEvent logEvent) + { + } + } +} diff --git a/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs new file mode 100644 index 0000000..3cb7bd1 --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/EventIdPropertyCacheTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.Extensions.Logging; +using Serilog.Events; +using Xunit; + +namespace Serilog.Extensions.Logging.Tests; + +public class EventIdPropertyCacheTests +{ + [Fact] + public void CreatesPropertyValueWithCorrectIdAndName() + { + // Arrange + const int id = 101; + const string name = "TestEvent"; + var eventId = new EventId(id, name); + + var cache = new EventIdPropertyCache(); + + // Act + var eventPropertyValue = cache.GetOrCreatePropertyValue(eventId); + + // Assert + var value = Assert.IsType(eventPropertyValue); + + Assert.Equal(2, value.Properties.Count); + + var idValue = value.Properties.Single(property => property.Name == "Id").Value; + var nameValue = value.Properties.Single(property => property.Name == "Name").Value; + + var scalarId = Assert.IsType(idValue); + var scalarName = Assert.IsType(nameValue); + + Assert.Equal(id, scalarId.Value); + Assert.Equal(name, scalarName.Value); + } + + [Fact] + public void EventsWithDSameKeysHaveSameReferences() + { + // Arrange + var cache = new EventIdPropertyCache(); + + // Act + var propertyValue1 = cache.GetOrCreatePropertyValue(new EventId(1, "Name1")); + var propertyValue2 = cache.GetOrCreatePropertyValue(new EventId(1, "Name1")); + + // Assert + Assert.Same(propertyValue1, propertyValue2); + } + + [Theory] + [InlineData(1, "SomeName", 1, "AnotherName")] + [InlineData(1, "SomeName", 2, "SomeName")] + [InlineData(1, "SomeName", 2, "AnotherName")] + public void EventsWithDifferentKeysHaveDifferentReferences(int firstId, string firstName, int secondId, string secondName) + { + // Arrange + var cache = new EventIdPropertyCache(); + + // Act + var propertyValue1 = cache.GetOrCreatePropertyValue(new EventId(firstId, firstName)); + var propertyValue2 = cache.GetOrCreatePropertyValue(new EventId(secondId, secondName)); + + // Assert + Assert.NotSame(propertyValue1, propertyValue2); + } + + + [Fact] + public void WhenLimitIsNotOverSameEventsHaveSameReferences() + { + // Arrange + var eventId = new EventId(101, "test"); + var cache = new EventIdPropertyCache(); + + // Act + var propertyValue1 = cache.GetOrCreatePropertyValue(eventId); + var propertyValue2 = cache.GetOrCreatePropertyValue(eventId); + + // Assert + Assert.Same(propertyValue1, propertyValue2); + } + + [Fact] + public void WhenLimitIsOverSameEventsHaveDifferentReferences() + { + // Arrange + var cache = new EventIdPropertyCache(maxCachedProperties: 1); + cache.GetOrCreatePropertyValue(new EventId(1, "InitialEvent")); + + var eventId = new EventId(101, "DifferentEvent"); + + // Act + var propertyValue1 = cache.GetOrCreatePropertyValue(eventId); + var propertyValue2 = cache.GetOrCreatePropertyValue(eventId); + + // Assert + Assert.NotSame(propertyValue1, propertyValue2); + } +} diff --git a/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj b/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj index 7c1a2f8..aace2e4 100644 --- a/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj +++ b/test/Serilog.Extensions.Logging.Tests/Serilog.Extensions.Logging.Tests.csproj @@ -1,8 +1,8 @@ - net8.0;net48 - enable + net8.0;net9.0;net48 + false @@ -14,11 +14,11 @@ - - - + + + - + diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs new file mode 100644 index 0000000..1a212c2 --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerScopeTests.cs @@ -0,0 +1,103 @@ +// Copyright © Serilog Contributors +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Serilog.Events; +using Serilog.Extensions.Logging.Tests.Support; + +using Xunit; + +namespace Serilog.Extensions.Logging.Tests; +public class SerilogLoggerScopeTests +{ + static (SerilogLoggerProvider, LogEventPropertyFactory, LogEvent) SetUp() + { + var loggerProvider = new SerilogLoggerProvider(); + + var logEventPropertyFactory = new LogEventPropertyFactory(); + + var dateTimeOffset = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); + var messageTemplate = new MessageTemplate(Enumerable.Empty()); + var properties = Enumerable.Empty(); + var logEvent = new LogEvent(dateTimeOffset, LogEventLevel.Information, null, messageTemplate, properties); + + return (loggerProvider, logEventPropertyFactory, logEvent); + } + + [Fact] + public void EnrichWithDictionaryStringObject() + { + const string propertyName = "Foo"; + const string expectedValue = "Bar"; + + var(loggerProvider, logEventPropertyFactory, logEvent) = SetUp(); + + + var state = new Dictionary() { { propertyName, expectedValue } }; + + var loggerScope = new SerilogLoggerScope(loggerProvider, state); + + loggerScope.EnrichAndCreateScopeItem(logEvent, logEventPropertyFactory, out LogEventPropertyValue? scopeItem); + + Assert.Contains(propertyName, logEvent.Properties); + + var scalarValue = logEvent.Properties[propertyName] as ScalarValue; + Assert.NotNull(scalarValue); + + var actualValue = scalarValue.Value as string; + Assert.NotNull(actualValue); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void EnrichWithIEnumerableKeyValuePairStringObject() + { + const string propertyName = "Foo"; + const string expectedValue = "Bar"; + + var (loggerProvider, logEventPropertyFactory, logEvent) = SetUp(); + + + var state = new KeyValuePair[] { new KeyValuePair(propertyName, expectedValue) }; + + var loggerScope = new SerilogLoggerScope(loggerProvider, state); + + loggerScope.EnrichAndCreateScopeItem(logEvent, logEventPropertyFactory, out LogEventPropertyValue? scopeItem); + + Assert.Contains(propertyName, logEvent.Properties); + + var scalarValue = logEvent.Properties[propertyName] as ScalarValue; + Assert.NotNull(scalarValue); + + var actualValue = scalarValue.Value as string; + Assert.NotNull(actualValue); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void EnrichWithTupleStringObject() + { + const string propertyName = "Foo"; + const string expectedValue = "Bar"; + + var (loggerProvider, logEventPropertyFactory, logEvent) = SetUp(); + +#if NET48 + var state = (propertyName, (object)expectedValue); +#else + var state = (propertyName, expectedValue); +#endif + + var loggerScope = new SerilogLoggerScope(loggerProvider, state); + + loggerScope.EnrichAndCreateScopeItem(logEvent, logEventPropertyFactory, out LogEventPropertyValue? scopeItem); + + Assert.Contains(propertyName, logEvent.Properties); + + var scalarValue = logEvent.Properties[propertyName] as ScalarValue; + Assert.NotNull(scalarValue); + + var actualValue = scalarValue.Value as string; + Assert.NotNull(actualValue); + Assert.Equal(expectedValue, actualValue); + } +} diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs index 179fd56..274d8ed 100644 --- a/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs @@ -17,7 +17,7 @@ public class SerilogLoggerTest const string Name = "test"; const string TestMessage = "This is a test"; - static Tuple SetUp(LogLevel logLevel) + static Tuple SetUp(LogLevel logLevel, IExternalScopeProvider? externalScopeProvider = null) { var sink = new SerilogSink(); @@ -29,6 +29,11 @@ static Tuple SetUp(LogLevel logLevel) var provider = new SerilogLoggerProvider(serilogLogger); var logger = (SerilogLogger)provider.CreateLogger(Name); + if (externalScopeProvider is not null) + { + provider.SetScopeProvider(externalScopeProvider); + } + return new Tuple(logger, sink); } @@ -397,6 +402,35 @@ public void NamedScopesAreCaptured() Assert.Equal("Inner", items[1]); } + [Fact] + public void ExternalScopesAreCaptured() + { + var externalScopeProvider = new FakeExternalScopeProvider(); + var (logger, sink) = SetUp(LogLevel.Trace, externalScopeProvider); + + externalScopeProvider.Push(new Dictionary() + { + { "FirstKey", 1 }, + { "SecondKey", 2 } + }); + + var scopeObject = new { ObjectKey = "Some value" }; + externalScopeProvider.Push(scopeObject); + + logger.Log(LogLevel.Information, 0, TestMessage, null!, null!); + + Assert.Single(sink.Writes); + Assert.True(sink.Writes[0].Properties.TryGetValue(SerilogLoggerProvider.ScopePropertyName, out var scopeValue)); + var sequence = Assert.IsType(scopeValue); + + var objectScope = (ScalarValue) sequence.Elements.Single(e => e is ScalarValue); + Assert.Equal(scopeObject.ToString(), (string?)objectScope.Value); + + var dictionaryScope = (DictionaryValue) sequence.Elements.Single(e => e is DictionaryValue); + Assert.Equal(1, ((ScalarValue)dictionaryScope.Elements.Single(pair => pair.Key.Value!.Equals("FirstKey")).Value).Value); + Assert.Equal(2, ((ScalarValue)dictionaryScope.Elements.Single(pair => pair.Key.Value!.Equals("SecondKey")).Value).Value); + } + class FoodScope : IEnumerable> { readonly string _name; @@ -446,22 +480,43 @@ class Person public string? LastName { get; set; } } - [Theory] - [InlineData(1)] - [InlineData(10)] - [InlineData(48)] - [InlineData(100)] - public void LowAndHighNumberedEventIdsAreMapped(int id) + class FakeExternalScopeProvider : IExternalScopeProvider { - var orig = new EventId(id, "test"); - var mapped = SerilogLogger.CreateEventIdProperty(orig); - var value = Assert.IsType(mapped.Value); - Assert.Equal(2, value.Properties.Count); - var idValue = value.Properties.Single(p => p.Name == "Id").Value; - var scalar = Assert.IsType(idValue); - Assert.Equal(id, scalar.Value); - } + private readonly List _scopes = new List(); + + public void ForEachScope(Action callback, TState state) + { + foreach (var scope in _scopes) + { + if (scope.IsDisposed) continue; + callback(scope.Value, state); + } + } + + public IDisposable Push(object? state) + { + var scope = new Scope(state); + _scopes.Add(scope); + return scope; + } + class Scope : IDisposable + { + public bool IsDisposed { get; set; } + public object? Value { get; set; } + + public Scope(object? value) + { + Value = value; + } + + public void Dispose() + { + IsDisposed = true; + } + } + } + [Fact] public void MismatchedMessageTemplateParameterCountIsHandled() { diff --git a/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs b/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs new file mode 100644 index 0000000..e30d017 --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/SerilogLoggingBuilderExtensionsTests.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog.Extensions.Logging.Tests.Support; +using Xunit; + +namespace Serilog.Extensions.Logging.Tests; + +public class SerilogLoggingBuilderExtensionsTests +{ + [Fact] + public void AddSerilogMustRegisterAnILoggerProvider() + { + var services = new ServiceCollection() + .AddLogging(builder => { builder.AddSerilog(); }) + .BuildServiceProvider(); + + var loggerProviders = services.GetServices(); + Assert.Contains(loggerProviders, provider => provider is SerilogLoggerProvider); + } + + [Fact] + public void AddSerilogMustRegisterAnILoggerProviderThatForwardsLogsToStaticSerilogLogger() + { + var sink = new SerilogSink(); + Log.Logger = new LoggerConfiguration() + .WriteTo.Sink(sink) + .CreateLogger(); + + var services = new ServiceCollection() + .AddLogging(builder => { builder.AddSerilog(); }) + .BuildServiceProvider(); + + var logger = services.GetRequiredService>(); + logger.LogInformation("Hello, world!"); + + Assert.Single(sink.Writes); + } + + [Fact] + public void AddSerilogMustRegisterAnILoggerProviderThatForwardsLogsToProvidedLogger() + { + var sink = new SerilogSink(); + var serilogLogger = new LoggerConfiguration() + .WriteTo.Sink(sink) + .CreateLogger(); + + var services = new ServiceCollection() + .AddLogging(builder => { builder.AddSerilog(logger: serilogLogger); }) + .BuildServiceProvider(); + + var logger = services.GetRequiredService>(); + logger.LogInformation("Hello, world!"); + + Assert.Single(sink.Writes); + } +} diff --git a/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs b/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs new file mode 100644 index 0000000..12d34af --- /dev/null +++ b/test/Serilog.Extensions.Logging.Tests/Support/LogEventPropertyFactory.cs @@ -0,0 +1,15 @@ +// Copyright © Serilog Contributors +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Serilog.Core; +using Serilog.Events; + +namespace Serilog.Extensions.Logging.Tests.Support; +internal class LogEventPropertyFactory : ILogEventPropertyFactory +{ + public LogEventProperty CreateProperty(string name, object? value, bool destructureObjects = false) + { + var scalarValue = new ScalarValue(value); + return new LogEventProperty(name, scalarValue); + } +}