diff --git a/.editorconfig b/.editorconfig index f16002a..3687ef4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,8 +5,9 @@ trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 4 +end_of_line = lf -[*.{csproj,json,config,yml}] +[*.{csproj,json,config,yml,props}] indent_size = 2 [*.sh] diff --git a/.gitignore b/.gitignore index ff4324d..29e6b22 100644 --- a/.gitignore +++ b/.gitignore @@ -201,4 +201,6 @@ FakesAssemblies/ project.lock.json #Test files -*.txt \ No newline at end of file +*.txt + +artifacts/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d70d01d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: csharp - -matrix: - include: - - os: linux - dist: trusty - sudo: required - dotnet: 2.1.4 - group: edge -script: - - ./build.sh \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..921ec11 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/sample/Sample/bin/Debug/netcoreapp3.1/Sample.dll", + "args": [], + "cwd": "${workspaceFolder}/sample/Sample", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "stopAtEntry": false, + "linux": { + "env": { + "TEMP": "/tmp" + } + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..56efa0c --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/sample/Sample/Sample.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/sample/Sample/Sample.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/sample/Sample/Sample.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Build.ps1 b/Build.ps1 index 4830b07..5dcd00a 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -2,48 +2,70 @@ echo "build: Build started" Push-Location $PSScriptRoot -if(Test-Path .\artifacts) { - echo "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 $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 "master" -and $revision -ne "local"] +$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"] echo "build: Version suffix is $suffix" -foreach ($src in ls src/*) { +foreach ($src in dir src/*) { Push-Location $src - echo "build: Packaging project in $src" + echo "build: Packaging project in $src" - & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --include-source - if($LASTEXITCODE -ne 0) { exit 1 } + & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix -p:ContinuousIntegrationBuild=true + if ($LASTEXITCODE -ne 0) { exit 1 } Pop-Location } -foreach ($test in ls test/*.PerformanceTests) { +foreach ($test in dir test/*.PerformanceTests) { Push-Location $test - echo "build: Building performance test project in $test" + echo "build: Building performance test project in $test" & dotnet build -c Release - if($LASTEXITCODE -ne 0) { exit 2 } + if ($LASTEXITCODE -ne 0) { exit 2 } Pop-Location } -foreach ($test in ls test/*.Tests) { +foreach ($test in dir test/*.Tests) { Push-Location $test - echo "build: Testing project in $test" + echo "build: Testing project in $test" - & dotnet test -c Release - if($LASTEXITCODE -ne 0) { exit 3 } + if ($PSVersionTable.Platform -eq "Unix") { + & dotnet test -c Release -f netcoreapp2.1 + & dotnet test -c Release -f netcoreapp3.1 + & dotnet test -c Release -f net50 + } else { + & dotnet test -c Release + } + + if ($LASTEXITCODE -ne 0) { exit 3 } + + Pop-Location +} + +if ($PSVersionTable.Platform -eq "Unix") { + Push-Location sample/Sample + + & dotnet run -f netcoreapp2.1 -c Release --run-once + if ($LASTEXITCODE -ne 0) { exit 4 } + + & dotnet run -f netcoreapp3.1 -c Release --run-once + if ($LASTEXITCODE -ne 0) { exit 4 } + + & dotnet run -f net50 -c Release --run-once + if ($LASTEXITCODE -ne 0) { exit 4 } Pop-Location } diff --git a/CHANGES.md b/CHANGES.md index 8589a7e..8753e6e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,90 @@ +# Changelog + +3.2.0 (pre-release) + +* #162 - LoggingFilterSwitch support +* #202 - added support to AuditTo.Logger +* #203 - added support for custom types in arrays and custom collections +* #218 - fixed an issue with `dotnet restore` with `rid` specified if referenced from `netstandard` project +* #219 - reduced search graph for configuration dlls to avoid native assets +* #221 - added support for conditional/leveled enrichers from Serilog 2.9+ +* #222 - updated Microsoft.Extensions.DependencyModel +* #231 - make '$' sign optional for minimum level / filter switch declarations +* #237 - DependencyContextAssemblyFinder fix: check `serilog` at the start of the name for any dependent package +* #239 - handle NotSupportedException for .net 5.0 single file applications +* #260 - skip static constructor on binding for complex parameters types + +3.1.0 + +* #155 - improve SelfLog output when misconfigured +* #160 - respect dynamic logging level changes for LevelSwitch section +* #158 - update NuGet package license format to new format +* #159 - DllScanningAssemblyFinder fixes #157, #150, #122, #156 +* #161 - support simple type names for Serilog types +* #151 - no longer rely on static state in ConfigurationReader +* #179 - added missing null checks for settingConfiguration +* #163 - added new ReadFrom.Configuration(...) overloads; marked old as obsolete +* #176 - added test to show how to filter child contexts + +3.0.1 + +* #142 - Fix IConfiguration parameters not being populated +* #143 - Fix ReadFrom.ConfigurationSection() looking for sections below a root Serilog section + +3.0.0 + +* #91 & #92 - Fix cherrypick from master +* #97 - Support of IConfiguration parameters & IConfigurationSection parameters +* #83 - Updated dependencies of Microsoft.Extensions.DependencyModel, + Microsoft.Extensions.Configuration.Abstraction & Microsoft.Extensions.Options.ConfigurationExtensions per TFM +* #98 - specify string array params +* Target Framework change to netcoreapp2.0 +* Build updates including addition of Travis Build +* #105 - detect and fail on ambiguous configurations +* #110 - destructure support +* #111 - case-insensitive argument matching +* #132 - choose string overloads to resolve binding ambiguities +* #134 - specify repository URL in package +* #124 - build a .NET 4.6.1 target +* #136 - control assembly source +* #138 - remove unnecessary package ref +* #139 - remove unused class +* #140 - expand support for destructure/enrich/filter configuration + +2.6.1 + +* #92 - fix WriteTo.Logger handling + +2.6.0 + +* #67 - improve error reporting when trying to convert from a missing class +* #74 - support abstract classes (in addition to interfaces) as values +* #84 - (documentation update) +* #88 - LoggingLevelSwitch support + +2.4.0 + +* #46 - configure sub-loggers through JSON settings +* #48 - permit multiple sinks of the same kind + +2.3.1 + +* #44 - fix ReadFrom.Configuration() on AWS Lambda; VS 2017 tooling + +2.3.0 + +* #40 - fix loading of configuration assemblies with names differing from their packages +* #36 - "Filter" support + +2.2.0 + +* #20 - support MSBuild (non-project.json) projects + 2.1.0 - * #14 - MinimumLevel.Override() - * #15 - Overload selection fix + +* #14 - MinimumLevel.Override() +* #15 - Overload selection fix 2.0.0 - * Initial version - + +* Initial version diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..03b68ca --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,11 @@ + + + latest + True + + + + + + + diff --git a/README.md b/README.md index 26d133b..0e8a10e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # Serilog.Settings.Configuration [![Build status](https://ci.appveyor.com/api/projects/status/r2bgfimd9ocr61px/branch/master?svg=true)](https://ci.appveyor.com/project/serilog/serilog-settings-configuration/branch/master) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Settings.Configuration.svg?style=flat)](https://www.nuget.org/packages/Serilog.Settings.Configuration/) -A Serilog settings provider that reads from _Microsoft.Extensions.Configuration_, .NET Core's `appsettings.json` file. +A Serilog settings provider that reads from [Microsoft.Extensions.Configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1) sources, including .NET Core's `appsettings.json` file. -Configuration is read from the `Serilog` section. +By default, configuration is read from the `Serilog` section. ```json { "Serilog": { - "Using": ["Serilog.Sinks.Console"], + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], "MinimumLevel": "Debug", "WriteTo": [ { "Name": "Console" }, - { "Name": "File", "Args": { "path": "%TEMP%\\Logs\\serilog-configuration-sample.txt" } } + { "Name": "File", "Args": { "path": "Logs/log.txt" } } ], - "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], "Destructure": [ { "Name": "With", "Args": { "policy": "Sample.CustomPolicy, Sample" } }, { "Name": "ToMaximumDepth", "Args": { "maximumDestructuringDepth": 4 } }, @@ -21,86 +21,231 @@ Configuration is read from the `Serilog` section. { "Name": "ToMaximumCollectionCount", "Args": { "maximumCollectionCount": 10 } } ], "Properties": { - "Application": "Sample" + "Application": "Sample" } } } ``` -This example relies on the _Serilog.Sinks.Console_, _Serilog.Sinks.File_, _Serilog.Enrichers.Environment_, _Serilog.Settings.Configuration_ and _Serilog.Enrichers.Thread_ packages also being installed. - After installing this package, use `ReadFrom.Configuration()` and pass an `IConfiguration` object. ```csharp -public class Program +static void Main(string[] args) { - public static void Main(string[] args) - { - var configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true) + .Build(); - var logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .CreateLogger(); + var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); - logger.Information("Hello, world!"); - } + logger.Information("Hello, world!"); } ``` -The `WriteTo` and `Enrich` sections support the same syntax, for example the following is valid if no arguments are needed by the sinks: +This example relies on the _[Microsoft.Extensions.Configuration.Json](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Json/)_, _[Serilog.Sinks.Console](https://github.com/serilog/serilog-sinks-console)_, _[Serilog.Sinks.File](https://github.com/serilog/serilog-sinks-file)_, _[Serilog.Enrichers.Environment](https://github.com/serilog/serilog-enrichers-environment)_ and _[Serilog.Enrichers.Thread](https://github.com/serilog/serilog-enrichers-thread)_ packages also being installed. + +For a more sophisticated example go to the [sample](sample/Sample) folder. + +## Syntax description + +### Root section name + +Root section name can be changed: ```json -"WriteTo": ["Console", "DiagnosticTrace"] +{ + "CustomSection": { + ... + } +} +``` + +```csharp +var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration, sectionName: "CustomSection") + .CreateLogger(); ``` -Or alternatively, the long-form (`"Name":` ...) syntax from the first example can be used when arguments need to be supplied. +### Using section and auto-discovery of configuration assemblies -(This package implements a convention using `DependencyContext` to find any package with `Serilog` anywhere in the name and pulls configuration methods from it, so the `Using` example above is redundant.) +`Using` section contains list of **assemblies** in which configuration methods (`WriteTo.File()`, `Enrich.WithThreadId()`) reside. -### .NET 4.x +```json +"Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Enrichers.Thread", /* ... */ ], + // ... +} +``` -To use this package in .NET 4.x applications, add `preserveCompilationContext` to `buildOptions` in _project.json_. +For .NET Core projects build tools produce `.deps.json` files and this package implements a convention using `Microsoft.Extensions.DependencyModel` to find any package among dependencies with `Serilog` anywhere in the name and pulls configuration methods from it, so the `Using` section in example above can be omitted: ```json -"net4.6": { - "buildOptions": { - "preserveCompilationContext": true - } -}, +{ + "Serilog": { + "MinimumLevel": "Debug", + "WriteTo": [ "Console" ], + ... + } +} +``` + +In order to utilize this convention for .NET Framework projects which are built with .NET Core CLI tools specify `PreserveCompilationContext` to `true` in the csproj properties: + +```xml + + true + ``` -### Level overrides +In case of [non-standard](#azure-functions-v2-v3) dependency management you can pass a custom `DependencyContext` object: + +```csharp +var functionDependencyContext = DependencyContext.Load(typeof(Startup).Assembly); + +var logger = new LoggerConfiguration() + .ReadFrom.Configuration(hostConfig, sectionName: "AzureFunctionsJobHost:Serilog", dependencyContext: functionDependencyContext) + .CreateLogger(); +``` + +For legacy .NET Framework projects it also scans default probing path(s). + +For all other cases, as well as in the case of non-conventional configuration assembly names **DO** use [Using](#using-section-and-auto-discovery-of-configuration-assemblies) section. + +#### .NET 5.0 Single File Applications + +Currently, auto-discovery of configuration assemblies is not supported in bundled mode. **DO** use [Using](#using-section-and-auto-discovery-of-configuration-assemblies) section for workaround. + +### MinimumLevel, LevelSwitches, overrides and dynamic reload The `MinimumLevel` configuration property can be set to a single value as in the sample above, or, levels can be overridden per logging source. This is useful in ASP.NET Core applications, which will often specify minimum level as: ```json - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Warning", - "System": "Warning" - } +"MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" } +} +``` + +`MinimumLevel` section also respects dynamic reload if the underlying provider supports it. + +```csharp +var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile(path: "appsettings.json", reloadOnChange: true) + .Build(); +``` + +Any changes for `Default`, `Microsoft`, `System` sources will be applied at runtime. + +(Note: only existing sources are respected for a dynamic update. Inserting new records in `Override` section is **not** supported.) + +You can also declare `LoggingLevelSwitch`-es in custom section and reference them for sink parameters: + +```json +{ + "Serilog": { + "LevelSwitches": { "controlSwitch": "Verbose" }, + "WriteTo": [ + { + "Name": "Seq", + "Args": { + "serverUrl": "http://localhost:5341", + "apiKey": "yeEZyL3SMcxEKUijBjN", + "controlLevelSwitch": "$controlSwitch" + } + } + ] + } +} +``` + +Level updates to switches are also respected for a dynamic update. + +### WriteTo, Enrich, AuditTo, Destructure sections + +These sections support simplified syntax, for example the following is valid if no arguments are needed by the sinks: + +```json +"WriteTo": [ "Console", "DiagnosticTrace" ] +``` + +Or alternatively, the long-form (`"Name":` ...) syntax from the example above can be used when arguments need to be supplied. + +By `Microsoft.Extensions.Configuration.Json` convention, array syntax implicitly defines index for each element in order to make unique paths for configuration keys. So the example above is equivalent to: + +```json +"WriteTo": { + "0": "Console", + "1": "DiagnosticTrace" +} ``` -### Environment variables +And -If your application enables the environment variable configuration source (`AddEnvironmentVariables()`) you can add or override Serilog configuration through the environment. +```json +"WriteTo:0": "Console", +"WriteTo:1": "DiagnosticTrace" +``` + +(The result paths for the keys will be the same, i.e. `Serilog:WriteTo:0` and `Serilog:WriteTo:1`) -For example, to set the minimum log level using the _Windows_ command prompt: +When overriding settings with [environment variables](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#environment-variables) it becomes less convenient and fragile, so you can specify custom names: +```json +"WriteTo": { + "ConsoleSink": "Console", + "DiagnosticTraceSink": { "Name": "DiagnosticTrace" } +} ``` -set Serilog:MinimumLevel=Debug -dotnet run + +### Properties section + +This section defines a static list of key-value pairs that will enrich log events. + +### Filter section + +This section defines filters that will be applied to log events. It is especially usefull in combination with _[Serilog.Expressions](https://github.com/serilog/serilog-expressions)_ (or legacy _[Serilog.Filters.Expressions](https://github.com/serilog/serilog-filters-expressions)_) package so you can write expression in text form: + +```json +"Filter": [{ + "Name": "ByIncludingOnly", + "Args": { + "expression": "Application = 'Sample'" + } +}] ``` +Using this package you can also declare `LoggingFilterSwitch`-es in custom section and reference them for filter parameters: + +```json +{ + "Serilog": { + "FilterSwitches": { "filterSwitch": "Application = 'Sample'" }, + "Filter": [ + { + "Name": "ControlledBy", + "Args": { + "switch": "$filterSwitch" + } + } + ] +} +``` + +Level updates to switches are also respected for a dynamic update. + ### Nested configuration sections -Some Serilog packages require a reference to a logger configuration object. The sample program in this project illustrates this with the following entry configuring the _Serilog.Sinks.Async_ package to wrap the _Serilog.Sinks.File_ package. The `configure` parameter references the File sink configuration: +Some Serilog packages require a reference to a logger configuration object. The sample program in this project illustrates this with the following entry configuring the _[Serilog.Sinks.Async](https://github.com/serilog/serilog-sinks-async)_ package to wrap the _[Serilog.Sinks.File](https://github.com/serilog/serilog-sinks-file)_ package. The `configure` parameter references the File sink configuration: ```json "WriteTo:Async": { @@ -110,8 +255,9 @@ Some Serilog packages require a reference to a logger configuration object. The { "Name": "File", "Args": { - "path": "%TEMP%\\Logs\\serilog-configuration-sample.txt", - "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}" + "path": "%TEMP%/Logs/serilog-configuration-sample.txt", + "outputTemplate": + "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}" } } ] @@ -119,17 +265,103 @@ Some Serilog packages require a reference to a logger configuration object. The }, ``` -### IConfiguration parameter +## Arguments binding -If a Serilog package requires additional external configuration information (for example, access to a `ConnectionStrings` section, which would be outside of the `Serilog` section), the sink should include an `IConfiguration` parameter in the configuration extension method. This package will automatically populate that parameter. It should not be declared in the argument list in the configuration source. +When the configuration specifies a discrete value for a parameter (such as a string literal), the package will attempt to convert that value to the target method's declared CLR type of the parameter. Additional explicit handling is provided for parsing strings to `Uri`, `TimeSpan`, `enum`, arrays and custom collections. ### Complex parameter value binding -When the configuration specifies a discrete value for a parameter (such as a string literal), the package will attempt to convert that value to the target method's declared CLR type of the parameter. Additional explicit handling is provided for parsing strings to `Uri` and `TimeSpan` objects and `enum` elements. +If the parameter value is not a discrete value, the package will use the configuration binding system provided by _[Microsoft.Extensions.Options.ConfigurationExtensions](https://www.nuget.org/packages/Microsoft.Extensions.Options.ConfigurationExtensions/)_ to attempt to populate the parameter. Almost anything that can be bound by `IConfiguration.Get` should work with this package. An example of this is the optional `List` parameter used to configure the .NET Standard version of the _[Serilog.Sinks.MSSqlServer](https://github.com/serilog/serilog-sinks-mssqlserver)_ package. + +### Abstract parameter types + +If parameter type is an interface or an abstract class you need to specify the full type name that implements abstract type. The implementation type should have parameterless constructor. + +```json +"Destructure": [ + { "Name": "With", "Args": { "policy": "Sample.CustomPolicy, Sample" } }, + ... +], +``` + +### IConfiguration parameter -If the parameter value is not a discrete value, the package will use the configuration binding system provided by _Microsoft.Extensions.Options.ConfigurationExtensions_ to attempt to populate the parameter. Almost anything that can be bound by `IConfiguration.Get` should work with this package. An example of this is the optional `List` parameter used to configure the .NET Standard version of the _Serilog.Sinks.MSSqlServer_ package. +If a Serilog package requires additional external configuration information (for example, access to a `ConnectionStrings` section, which would be outside of the `Serilog` section), the sink should include an `IConfiguration` parameter in the configuration extension method. This package will automatically populate that parameter. It should not be declared in the argument list in the configuration source. ### IConfigurationSection parameters Certain Serilog packages may require configuration information that can't be easily represented by discrete values or direct binding-friendly representations. An example might be lists of values to remove from a collection of default values. In this case the method can accept an entire `IConfigurationSection` as a call parameter and this package will recognize that and populate the parameter. In this way, Serilog packages can support arbitrarily complex configuration scenarios. +## Samples + +### Azure Functions (v2, v3) + +hosts.json + +```json +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingExcludedTypes": "Request", + "samplingSettings": { + "isEnabled": true + } + } + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "Enrich": [ "FromLogContext" ], + "WriteTo": [ + { "Name": "Seq", "Args": { "serverUrl": "http://localhost:5341" } } + ] + } +} +``` + +In `Startup.cs` section name should be prefixed with [AzureFunctionsJobHost](https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings#azurefunctionsjobhost__) + +```csharp +public class Startup : FunctionsStartup +{ + public override void Configure(IFunctionsHostBuilder builder) + { + builder.Services.AddSingleton(sp => + { + var functionDependencyContext = DependencyContext.Load(typeof(Startup).Assembly); + + var hostConfig = sp.GetRequiredService(); + var logger = new LoggerConfiguration() + .ReadFrom.Configuration(hostConfig, sectionName: "AzureFunctionsJobHost:Serilog", dependencyContext: functionDependencyContext) + .CreateLogger(); + + return new SerilogLoggerProvider(logger, dispose: true); + }); + } +} +``` + +In order to make auto-discovery of configuration assemblies work, modify Function's csproj file + +```xml + + + + + + + + + + + + + + +``` diff --git a/appveyor.yml b/appveyor.yml index 396e0ef..5aae7af 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,23 +1,34 @@ version: '{build}' skip_tags: true -image: Visual Studio 2017 +image: + - Visual Studio 2019 + - Ubuntu configuration: Release build_script: - ps: ./Build.ps1 +for: +- + matrix: + only: + - image: Ubuntu + build_script: + - pwsh ./Build.ps1 test: off artifacts: - path: artifacts/Serilog.*.nupkg +- path: artifacts/Serilog.*.snupkg deploy: - provider: NuGet api_key: - secure: N59tiJECUYpip6tEn0xvdmDAEiP9SIzyLEFLpwiigm/8WhJvBNs13QxzT1/3/JW/ - skip_symbols: true + secure: 6WetFj2k7TEactDaHhg0m0q/WpCldFAUtgAjN8VK9Qn2fsY1vdufRB8XIKnPX9zn on: - branch: /^(master|dev)$/ + branch: /^(main|dev)$/ - provider: GitHub auth_token: secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX - artifact: /Serilog.*\.nupkg/ + artifacts: + /Serilog.*\.nupkg/ + /Serilog.*\.snupkg/ tag: v$(appveyor_build_version) on: - branch: master + branch: main diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..0ffa019 Binary files /dev/null and b/assets/icon.png differ diff --git a/build.sh b/build.sh deleted file mode 100755 index 19ab21c..0000000 --- a/build.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -e -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 - -cd sample/Sample/ -dotnet build -f netcoreapp2.0 -c Release -dotnet bin/Release/netcoreapp2.0/Sample.dll --run-once diff --git a/sample/Sample/Program.cs b/sample/Sample/Program.cs index 562e427..726df35 100644 --- a/sample/Sample/Program.cs +++ b/sample/Sample/Program.cs @@ -1,12 +1,16 @@ using System; -using Microsoft.Extensions.Configuration; -using Serilog; using System.IO; using System.Linq; +using System.Collections.Generic; +using System.Threading; + +using Microsoft.Extensions.Configuration; + +using Serilog; using Serilog.Core; using Serilog.Events; -using System.Collections.Generic; +using Serilog.Debugging; namespace Sample { @@ -14,6 +18,10 @@ public class Program { public static void Main(string[] args) { + SelfLog.Enable(Console.Error); + + Thread.CurrentThread.Name = "Main thread"; + var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true) @@ -47,7 +55,7 @@ public static void Main(string[] args) Console.WriteLine("\nPress \"q\" to quit, or any other key to run again.\n"); } - while(!args.Contains("--run-once") && (Console.ReadKey().KeyChar != 'q')); + while (!args.Contains("--run-once") && (Console.ReadKey().KeyChar != 'q')); } } @@ -73,7 +81,7 @@ public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyV { result = null; - if(value is LoginData) + if (value is LoginData) { result = new StructureValue( new List diff --git a/sample/Sample/Sample.csproj b/sample/Sample/Sample.csproj index 1b69583..d64b702 100644 --- a/sample/Sample/Sample.csproj +++ b/sample/Sample/Sample.csproj @@ -1,7 +1,7 @@  - net46;netcoreapp2.0 + net50;netcoreapp3.1;netcoreapp2.1;net46 Exe @@ -15,19 +15,30 @@ + - - + + + + + + + + + + + + + - + - - - - + + + diff --git a/sample/Sample/appsettings.json b/sample/Sample/appsettings.json index 2771a7f..0d89e71 100644 --- a/sample/Sample/appsettings.json +++ b/sample/Sample/appsettings.json @@ -1,7 +1,8 @@ { "Serilog": { "Using": [ "Serilog.Sinks.Console" ], - "LevelSwitches": { "$controlSwitch": "Verbose" }, + "LevelSwitches": { "controlSwitch": "Verbose" }, + "FilterSwitches": { "$filterSwitch": "Application = 'Sample'" }, "MinimumLevel": { "Default": "Debug", "Override": { @@ -35,14 +36,45 @@ { "Name": "File", "Args": { - "path": "%TEMP%\\Logs\\serilog-configuration-sample.txt", - "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}" + "path": "%TEMP%/Logs/serilog-configuration-sample.txt", + "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}/{ThreadName}) {Message}{NewLine}{Exception}" } } ] } }, - "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], + "WriteTo:ConditionalSink": { + "Name": "Conditional", + "Args": { + "expression": "@Level in ['Error', 'Fatal']", + "configureSink": [ + { + "Name": "File", + "Args": { + "path": "%TEMP%/Logs/serilog-configuration-sample-errors.txt" + } + } + ] + } + }, + "Enrich": [ + "FromLogContext", + "WithThreadId", + { + "Name": "AtLevel", + "Args": { + "enrichFromLevel": "Error", + "configureEnricher": [ "WithThreadName" ] + } + }, + { + "Name": "When", + "Args": { + "expression": "Application = 'Sample'", + "configureEnricher": [ "WithMachineName" ] + } + } + ], "Properties": { "Application": "Sample" }, @@ -66,9 +98,9 @@ ], "Filter": [ { - "Name": "ByIncludingOnly", + "Name": "ControlledBy", "Args": { - "expression": "Application = 'Sample'" + "switch": "$filterSwitch" } }, { diff --git a/serilog-settings-configuration.sln b/serilog-settings-configuration.sln index 82c4512..ec9c9b2 100644 --- a/serilog-settings-configuration.sln +++ b/serilog-settings-configuration.sln @@ -1,16 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E41FD57-5FAB-4E3C-B16E-463DE98338BC}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{62D0B904-1D11-4962-A4A8-DE28672AA28B}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .gitignore = .gitignore appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 CHANGES.md = CHANGES.md + Directory.Build.props = Directory.Build.props + assets\icon.png = assets\icon.png LICENSE = LICENSE README.md = README.md serilog-settings-configuration.sln.DotSettings = serilog-settings-configuration.sln.DotSettings diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index e62947a..e54b5a5 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -1,8 +1,9 @@ - + Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - 3.1.0 + 3.2.0 + latest Serilog Contributors netstandard2.0;net451;net461 true @@ -13,12 +14,14 @@ true Serilog.Settings.Configuration serilog;json - https://serilog.net/images/serilog-configuration-nuget.png - https://github.com/serilog/serilog-settings-configuration + icon.png + https://github.com/serilog/serilog-settings-configuration/ Apache-2.0 - https://github.com/serilog/serilog-settings-configuration - git Serilog + true + true + true + snupkg @@ -26,10 +29,12 @@ - - + + + + - + diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs index 4d356a8..cc16a55 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs @@ -16,26 +16,30 @@ protected static bool IsCaseInsensitiveMatch(string text, string textToFind) public static AssemblyFinder Auto() { - // Need to check `Assembly.GetEntryAssembly()` first because - // `DependencyContext.Default` throws an exception when `Assembly.GetEntryAssembly()` returns null - if (Assembly.GetEntryAssembly() != null && DependencyContext.Default != null) + try { - return new DependencyContextAssemblyFinder(DependencyContext.Default); + // Need to check `Assembly.GetEntryAssembly()` first because + // `DependencyContext.Default` throws an exception when `Assembly.GetEntryAssembly()` returns null + if (Assembly.GetEntryAssembly() != null && DependencyContext.Default != null) + { + return new DependencyContextAssemblyFinder(DependencyContext.Default); + } } + catch (NotSupportedException) when (typeof(object).Assembly.Location is "") // bundled mode detection + { + } + return new DllScanningAssemblyFinder(); } public static AssemblyFinder ForSource(ConfigurationAssemblySource configurationAssemblySource) { - switch (configurationAssemblySource) + return configurationAssemblySource switch { - case ConfigurationAssemblySource.UseLoadedAssemblies: - return Auto(); - case ConfigurationAssemblySource.AlwaysScanDllFiles: - return new DllScanningAssemblyFinder(); - default: - throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null); - } + ConfigurationAssemblySource.UseLoadedAssemblies => Auto(), + ConfigurationAssemblySource.AlwaysScanDllFiles => new DllScanningAssemblyFinder(), + _ => throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null), + }; } public static AssemblyFinder ForDependencyContext(DependencyContext dependencyContext) diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs index d7faa77..7a6feaa 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs @@ -18,11 +18,20 @@ public DependencyContextAssemblyFinder(DependencyContext dependencyContext) public override IReadOnlyList FindAssembliesContainingName(string nameToFind) { var query = from library in _dependencyContext.RuntimeLibraries + where IsReferencingSerilog(library) from assemblyName in library.GetDefaultAssemblyNames(_dependencyContext) where IsCaseInsensitiveMatch(assemblyName.Name, nameToFind) select assemblyName; return query.ToList().AsReadOnly(); + + static bool IsReferencingSerilog(Library library) + { + const string Serilog = "serilog"; + return library.Dependencies.Any(dependency => + dependency.Name.StartsWith(Serilog, StringComparison.OrdinalIgnoreCase) && + (dependency.Name.Length == Serilog.Length || dependency.Name[Serilog.Length] == '.')); + } } } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs index 95584e2..102b492 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs @@ -50,7 +50,7 @@ where IsCaseInsensitiveMatch(assemblyFileName, nameToFind) return query.ToList().AsReadOnly(); - AssemblyName TryGetAssemblyNameFrom(string path) + static AssemblyName TryGetAssemblyNameFrom(string path) { try { diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs index 3bf16df..720d56d 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs @@ -3,9 +3,10 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; -using System.Text.RegularExpressions; using Serilog.Configuration; using Serilog.Core; @@ -17,7 +18,7 @@ namespace Serilog.Settings.Configuration { class ConfigurationReader : IConfigurationReader { - const string LevelSwitchNameRegex = @"^\$[A-Za-z]+[A-Za-z0-9]*$"; + const string LevelSwitchNameRegex = @"^\${0,1}[A-Za-z]+[A-Za-z0-9]*$"; readonly IConfigurationSection _section; readonly IReadOnlyCollection _configurationAssemblies; @@ -41,6 +42,7 @@ internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyColle public void Configure(LoggerConfiguration loggerConfiguration) { ProcessLevelSwitchDeclarations(); + ProcessFilterSwitchDeclarations(); ApplyMinimumLevel(loggerConfiguration); ApplyEnrichment(loggerConfiguration); @@ -50,6 +52,63 @@ public void Configure(LoggerConfiguration loggerConfiguration) ApplyAuditSinks(loggerConfiguration); } + void ProcessFilterSwitchDeclarations() + { + var filterSwitchesDirective = _section.GetSection("FilterSwitches"); + + foreach (var filterSwitchDeclaration in filterSwitchesDirective.GetChildren()) + { + var filterSwitch = LoggingFilterSwitchProxy.Create(); + if (filterSwitch == null) + { + SelfLog.WriteLine($"FilterSwitches section found, but neither Serilog.Expressions nor Serilog.Filters.Expressions is referenced."); + break; + } + + var switchName = filterSwitchDeclaration.Key; + // switchName must be something like $switch to avoid ambiguities + if (!IsValidSwitchName(switchName)) + { + throw new FormatException($"\"{switchName}\" is not a valid name for a Filter Switch declaration. The first character of the name must be a letter or '$' sign, like \"FilterSwitches\" : {{\"$switchName\" : \"{{FilterExpression}}\"}}"); + } + + SetFilterSwitch(throwOnError: true); + SubscribeToFilterExpressionChanges(); + + _resolutionContext.AddFilterSwitch(switchName, filterSwitch); + + void SubscribeToFilterExpressionChanges() + { + ChangeToken.OnChange(filterSwitchDeclaration.GetReloadToken, () => SetFilterSwitch(throwOnError: false)); + } + + void SetFilterSwitch(bool throwOnError) + { + var filterExpr = filterSwitchDeclaration.Value; + if (string.IsNullOrWhiteSpace(filterExpr)) + { + filterSwitch.Expression = null; + return; + } + + try + { + filterSwitch.Expression = filterExpr; + } + catch (Exception e) + { + var errMsg = $"The expression '{filterExpr}' is invalid filter expression: {e.Message}."; + if (throwOnError) + { + throw new InvalidOperationException(errMsg, e); + } + + SelfLog.WriteLine(errMsg); + } + } + } + } + void ProcessLevelSwitchDeclarations() { var levelSwitchesDirective = _section.GetSection("LevelSwitches"); @@ -60,7 +119,7 @@ void ProcessLevelSwitchDeclarations() // switchName must be something like $switch to avoid ambiguities if (!IsValidSwitchName(switchName)) { - throw new FormatException($"\"{switchName}\" is not a valid name for a Level Switch declaration. Level switch must be declared with a '$' sign, like \"LevelSwitches\" : {{\"$switchName\" : \"InitialLevel\"}}"); + throw new FormatException($"\"{switchName}\" is not a valid name for a Level Switch declaration. The first character of the name must be a letter or '$' sign, like \"LevelSwitches\" : {{\"$switchName\" : \"InitialLevel\"}}"); } LoggingLevelSwitch newSwitch; @@ -94,7 +153,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration) var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy"); if (minLevelControlledByDirective.Value != null) { - var globalMinimumLevelSwitch = _resolutionContext.LookUpSwitchByName(minLevelControlledByDirective.Value); + var globalMinimumLevelSwitch = _resolutionContext.LookUpLevelSwitchByName(minLevelControlledByDirective.Value); // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already loggerConfiguration.MinimumLevel.ControlledBy(globalMinimumLevelSwitch); } @@ -109,7 +168,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration) } else { - var overrideSwitch = _resolutionContext.LookUpSwitchByName(overridenLevelOrSwitch); + var overrideSwitch = _resolutionContext.LookUpLevelSwitchByName(overridenLevelOrSwitch); // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already loggerConfiguration.MinimumLevel.Override(overridePrefix, overrideSwitch); } @@ -185,6 +244,12 @@ void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfigura CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerSinkConfiguration); } + void IConfigurationReader.ApplyEnrichment(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) + { + var methodCalls = GetMethodCalls(_section); + CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerEnrichmentConfiguration); + } + void ApplyEnrichment(LoggerConfiguration loggerConfiguration) { var enrichDirective = _section.GetSection("Enrich"); @@ -220,39 +285,14 @@ void ApplyEnrichment(LoggerConfiguration loggerConfiguration) select new { Name = argument.Key, - Value = GetArgumentValue(argument) + Value = GetArgumentValue(argument, _configurationAssemblies) }).ToDictionary(p => p.Name, p => p.Value) select new { Name = name, Args = callArgs })) .ToLookup(p => p.Name, p => p.Args); return result; - IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection) - { - IConfigurationArgumentValue argumentValue; - - // Reject configurations where an element has both scalar and complex - // values as a result of reading multiple configuration sources. - if (argumentSection.Value != null && argumentSection.GetChildren().Any()) - throw new InvalidOperationException( - $"The value for the argument '{argumentSection.Path}' is assigned different value " + - "types in more than one configuration source. Ensure all configurations consistently " + - "use either a scalar (int, string, boolean) or a complex (array, section, list, " + - "POCO, etc.) type for this argument value."); - - if (argumentSection.Value != null) - { - argumentValue = new StringArgumentValue(argumentSection.Value); - } - else - { - argumentValue = new ObjectArgumentValue(argumentSection, _configurationAssemblies); - } - - return argumentValue; - } - - string GetSectionName(IConfigurationSection s) + static string GetSectionName(IConfigurationSection s) { var name = s.GetSection("Name"); if (name.Value == null) @@ -262,9 +302,35 @@ string GetSectionName(IConfigurationSection s) } } + internal static IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection, IReadOnlyCollection configurationAssemblies) + { + IConfigurationArgumentValue argumentValue; + + // Reject configurations where an element has both scalar and complex + // values as a result of reading multiple configuration sources. + if (argumentSection.Value != null && argumentSection.GetChildren().Any()) + throw new InvalidOperationException( + $"The value for the argument '{argumentSection.Path}' is assigned different value " + + "types in more than one configuration source. Ensure all configurations consistently " + + "use either a scalar (int, string, boolean) or a complex (array, section, list, " + + "POCO, etc.) type for this argument value."); + + if (argumentSection.Value != null) + { + argumentValue = new StringArgumentValue(argumentSection.Value); + } + else + { + argumentValue = new ObjectArgumentValue(argumentSection, configurationAssemblies); + } + + return argumentValue; + } + static IReadOnlyCollection LoadConfigurationAssemblies(IConfigurationSection section, AssemblyFinder assemblyFinder) { - var assemblies = new Dictionary(); + var serilogAssembly = typeof(ILogger).Assembly; + var assemblies = new Dictionary { [serilogAssembly.FullName] = serilogAssembly }; var usingSection = section.GetSection("Using"); if (usingSection.GetChildren().Any()) diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs index f19f142..af815af 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs @@ -1,9 +1,10 @@ -using Serilog.Configuration; +using Serilog.Configuration; namespace Serilog.Settings.Configuration { interface IConfigurationReader : ILoggerSettings { void ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration); + void ApplyEnrichment(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration); } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs new file mode 100644 index 0000000..4736ee2 --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs @@ -0,0 +1,49 @@ +using System; + +namespace Serilog.Settings.Configuration +{ + class LoggingFilterSwitchProxy + { + readonly Action _setProxy; + readonly Func _getProxy; + + LoggingFilterSwitchProxy(object realSwitch) + { + RealSwitch = realSwitch ?? throw new ArgumentNullException(nameof(realSwitch)); + + var expressionProperty = realSwitch.GetType().GetProperty("Expression"); + + _setProxy = (Action)Delegate.CreateDelegate( + typeof(Action), + realSwitch, + expressionProperty.GetSetMethod()); + + _getProxy = (Func)Delegate.CreateDelegate( + typeof(Func), + realSwitch, + expressionProperty.GetGetMethod()); + } + + public object RealSwitch { get; } + + public string Expression + { + get => _getProxy(); + set => _setProxy(value); + } + + public static LoggingFilterSwitchProxy Create(string expression = null) + { + var filterSwitchType = + Type.GetType("Serilog.Expressions.LoggingFilterSwitch, Serilog.Expressions") ?? + Type.GetType("Serilog.Filters.Expressions.LoggingFilterSwitch, Serilog.Filters.Expressions"); + + if (filterSwitchType is null) + { + return null; + } + + return new LoggingFilterSwitchProxy(Activator.CreateInstance(filterSwitchType, expression)); + } + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs index 6b54c34..83f7443 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs @@ -1,8 +1,10 @@ -using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; +using Microsoft.Extensions.Configuration; + using Serilog.Configuration; namespace Serilog.Settings.Configuration @@ -31,24 +33,83 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext) typeInfo.GetGenericTypeDefinition() is Type genericType && genericType == typeof(Action<>)) { var configType = typeInfo.GenericTypeArguments[0]; - if (configType != typeof(LoggerConfiguration) && configType != typeof(LoggerSinkConfiguration)) - throw new ArgumentException($"Configuration for Action<{configType}> is not implemented."); - IConfigurationReader configReader = new ConfigurationReader(_section, _configurationAssemblies, resolutionContext); - if (configType == typeof(LoggerConfiguration)) + return configType switch { - return new Action(configReader.Configure); + _ when configType == typeof(LoggerConfiguration) => new Action(configReader.Configure), + _ when configType == typeof(LoggerSinkConfiguration) => new Action(configReader.ApplySinks), + _ when configType == typeof(LoggerEnrichmentConfiguration) => new Action(configReader.ApplyEnrichment), + _ => throw new ArgumentException($"Configuration resolution for Action<{configType.Name}> parameter type at the path {_section.Path} is not implemented.") + }; + } + + if (toType.IsArray) + return CreateArray(); + + if (IsContainer(toType, out var elementType) && TryCreateContainer(out var result)) + return result; + + // MS Config binding can work with a limited set of primitive types and collections + return _section.Get(toType); + + object CreateArray() + { + var elementType = toType.GetElementType(); + var configurationElements = _section.GetChildren().ToArray(); + var result = Array.CreateInstance(elementType, configurationElements.Length); + for (int i = 0; i < configurationElements.Length; ++i) + { + var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); + var value = argumentValue.ConvertTo(elementType, resolutionContext); + result.SetValue(value, i); } - if (configType == typeof(LoggerSinkConfiguration)) + return result; + } + + bool TryCreateContainer(out object result) + { + result = null; + + if (toType.GetConstructor(Type.EmptyTypes) == null) + return false; + + // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#collection-initializers + var addMethod = toType.GetMethods().FirstOrDefault(m => !m.IsStatic && m.Name == "Add" && m.GetParameters()?.Length == 1 && m.GetParameters()[0].ParameterType == elementType); + if (addMethod == null) + return false; + + var configurationElements = _section.GetChildren().ToArray(); + result = Activator.CreateInstance(toType); + + for (int i = 0; i < configurationElements.Length; ++i) { - return new Action(loggerSinkConfig => configReader.ApplySinks(loggerSinkConfig)); + var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); + var value = argumentValue.ConvertTo(elementType, resolutionContext); + addMethod.Invoke(result, new object[] { value }); } + + return true; } + } - // MS Config binding - return _section.Get(toType); + static bool IsContainer(Type type, out Type elementType) + { + elementType = null; + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType) + { + if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + elementType = iface.GetGenericArguments()[0]; + return true; + } + } + } + + return false; } } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs index 90ab06d..2963a64 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs @@ -12,11 +12,13 @@ namespace Serilog.Settings.Configuration sealed class ResolutionContext { readonly IDictionary _declaredLevelSwitches; + readonly IDictionary _declaredFilterSwitches; readonly IConfiguration _appConfiguration; public ResolutionContext(IConfiguration appConfiguration = null) { _declaredLevelSwitches = new Dictionary(); + _declaredFilterSwitches = new Dictionary(); _appConfiguration = appConfiguration; } @@ -26,7 +28,7 @@ public ResolutionContext(IConfiguration appConfiguration = null) /// the name of a switch to look up /// the LoggingLevelSwitch registered with the name /// if no switch has been registered with - public LoggingLevelSwitch LookUpSwitchByName(string switchName) + public LoggingLevelSwitch LookUpLevelSwitchByName(string switchName) { if (_declaredLevelSwitches.TryGetValue(switchName, out var levelSwitch)) { @@ -36,6 +38,16 @@ public LoggingLevelSwitch LookUpSwitchByName(string switchName) throw new InvalidOperationException($"No LoggingLevelSwitch has been declared with name \"{switchName}\". You might be missing a section \"LevelSwitches\":{{\"{switchName}\":\"InitialLevel\"}}"); } + public LoggingFilterSwitchProxy LookUpFilterSwitchByName(string switchName) + { + if (_declaredFilterSwitches.TryGetValue(switchName, out var filterSwitch)) + { + return filterSwitch; + } + + throw new InvalidOperationException($"No LoggingFilterSwitch has been declared with name \"{switchName}\". You might be missing a section \"FilterSwitches\":{{\"{switchName}\":\"{{FilterExpression}}\"}}"); + } + public bool HasAppConfiguration => _appConfiguration != null; public IConfiguration AppConfiguration @@ -55,7 +67,19 @@ public void AddLevelSwitch(string levelSwitchName, LoggingLevelSwitch levelSwitc { if (levelSwitchName == null) throw new ArgumentNullException(nameof(levelSwitchName)); if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch)); - _declaredLevelSwitches[levelSwitchName] = levelSwitch; + _declaredLevelSwitches[ToSwitchReference(levelSwitchName)] = levelSwitch; + } + + public void AddFilterSwitch(string filterSwitchName, LoggingFilterSwitchProxy filterSwitch) + { + if (filterSwitchName == null) throw new ArgumentNullException(nameof(filterSwitchName)); + if (filterSwitch == null) throw new ArgumentNullException(nameof(filterSwitch)); + _declaredFilterSwitches[ToSwitchReference(filterSwitchName)] = filterSwitch; + } + + string ToSwitchReference(string switchName) + { + return switchName.StartsWith("$") ? switchName : $"${switchName}"; } } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs index 0a7c09b..b73ae2c 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs @@ -32,7 +32,13 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext) if (toType == typeof(LoggingLevelSwitch)) { - return resolutionContext.LookUpSwitchByName(argumentValue); + return resolutionContext.LookUpLevelSwitchByName(argumentValue); + } + + if (toType.FullName == "Serilog.Expressions.LoggingFilterSwitch" || + toType.FullName == "Serilog.Filters.Expressions.LoggingFilterSwitch") + { + return resolutionContext.LookUpFilterSwitchByName(argumentValue).RealSwitch; } var toTypeInfo = toType.GetTypeInfo(); @@ -93,20 +99,22 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext) // maybe it's the assembly-qualified type name of a concrete implementation // with a default constructor var type = FindType(argumentValue.Trim()); - if (type != null) + if (type == null) { - var ctor = type.GetTypeInfo().DeclaredConstructors.FirstOrDefault(ci => - { - var parameters = ci.GetParameters(); - return parameters.Length == 0 || parameters.All(pi => pi.HasDefaultValue); - }); + throw new InvalidOperationException($"Type {argumentValue} was not found."); + } + + var ctor = type.GetTypeInfo().DeclaredConstructors.Where(ci => !ci.IsStatic).FirstOrDefault(ci => + { + var parameters = ci.GetParameters(); + return parameters.Length == 0 || parameters.All(pi => pi.HasDefaultValue); + }); - if (ctor == null) - throw new InvalidOperationException($"A default constructor was not found on {type.FullName}."); + if (ctor == null) + throw new InvalidOperationException($"A default constructor was not found on {type.FullName}."); - var call = ctor.GetParameters().Select(pi => pi.DefaultValue).ToArray(); - return ctor.Invoke(call); - } + var call = ctor.GetParameters().Select(pi => pi.DefaultValue).ToArray(); + return ctor.Invoke(call); } return Convert.ChangeType(argumentValue, toType); diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs index bbde10d..4b460b3 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs @@ -67,6 +67,13 @@ static class SurrogateConfigurationMethods LoggingLevelSwitch levelSwitch = null) => auditSinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); + static LoggerConfiguration Logger( + LoggerAuditSinkConfiguration auditSinkConfiguration, + Action configureLogger, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null) + => auditSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch); + // .Filter... // ======= // TODO: add overload for array argument (ILogEventEnricher[]) @@ -101,6 +108,14 @@ static LoggerConfiguration AsScalar(LoggerDestructuringConfiguration loggerDestr ILogEventEnricher enricher) => loggerEnrichmentConfiguration.With(enricher); + static LoggerConfiguration AtLevel( + LoggerEnrichmentConfiguration loggerEnrichmentConfiguration, + Action configureEnricher, + LogEventLevel enrichFromLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null) + => levelSwitch != null ? loggerEnrichmentConfiguration.AtLevel(levelSwitch, configureEnricher) + : loggerEnrichmentConfiguration.AtLevel(enrichFromLevel, configureEnricher); + static LoggerConfiguration FromLogContext(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) => loggerEnrichmentConfiguration.FromLogContext(); diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs index eed3803..472b0f9 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs @@ -162,6 +162,37 @@ public void AuditSinksAreConfigured() Assert.Equal(1, DummyRollingFileAuditSink.Emitted.Count); } + [Fact] + public void AuditToSubLoggersAreConfigured() + { + var json = @"{ + ""Serilog"": { + ""Using"": [""TestDummies""], + ""AuditTo"": [{ + ""Name"": ""Logger"", + ""Args"": { + ""configureLogger"" : { + ""AuditTo"": [{ + ""Name"": ""DummyRollingFile"", + ""Args"": {""pathFormat"" : ""C:\\""} + }]} + } + }] + } + }"; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + DummyRollingFileAuditSink.Reset(); + + log.Write(Some.InformationEvent()); + + Assert.Equal(0, DummyRollingFileSink.Emitted.Count); + Assert.Equal(1, DummyRollingFileAuditSink.Emitted.Count); + } + [Fact] public void TestMinimumLevelOverrides() { @@ -274,8 +305,10 @@ public void SinksAreConfiguredWithStaticMember() [Theory] [InlineData("$switchName", true)] [InlineData("$SwitchName", true)] + [InlineData("SwitchName", true)] [InlineData("$switch1", true)] [InlineData("$sw1tch0", true)] + [InlineData("sw1tch0", true)] [InlineData("$SWITCHNAME", true)] [InlineData("$$switchname", false)] [InlineData("$switchname$", false)] @@ -295,29 +328,60 @@ public void LoggingLevelSwitchNameValidityScenarios(string switchName, bool expe public void LoggingLevelSwitchWithInvalidNameThrowsFormatException() { var json = @"{ - ""Serilog"": { - ""LevelSwitches"": {""switchNameNotStartingWithDollar"" : ""Warning"" } + ""Serilog"": { + ""LevelSwitches"": {""1InvalidSwitchName"" : ""Warning"" } } }"; var ex = Assert.Throws(() => ConfigFromJson(json)); - Assert.Contains("\"switchNameNotStartingWithDollar\"", ex.Message); + Assert.Contains("\"1InvalidSwitchName\"", ex.Message); Assert.Contains("'$' sign", ex.Message); Assert.Contains("\"LevelSwitches\" : {\"$switchName\" :", ex.Message); } - [Fact] - public void LoggingLevelSwitchIsConfigured() + [Theory] + [InlineData("$mySwitch")] + [InlineData("mySwitch")] + public void LoggingFilterSwitchIsConfigured(string switchName) { - var json = @"{ - ""Serilog"": { - ""LevelSwitches"": {""$switch1"" : ""Warning"" }, - ""MinimumLevel"" : { - ""ControlledBy"" : ""$switch1"" - } - } - }"; + var json = $@"{{ + 'Serilog': {{ + 'FilterSwitches': {{ '{switchName}': 'Prop = 42' }}, + 'Filter:BySwitch': {{ + 'Name': 'ControlledBy', + 'Args': {{ + 'switch': '$mySwitch' + }} + }} + }} + }}"; + LogEvent evt = null; + + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Write(Some.InformationEvent()); + Assert.Null(evt); + + log.ForContext("Prop", 42).Write(Some.InformationEvent()); + Assert.NotNull(evt); + } + + [Theory] + [InlineData("$switch1")] + [InlineData("switch1")] + public void LoggingLevelSwitchIsConfigured(string switchName) + { + var json = $@"{{ + 'Serilog': {{ + 'LevelSwitches': {{ '{switchName}' : 'Warning' }}, + 'MinimumLevel' : {{ + 'ControlledBy' : '$switch1' + }} + }} + }}"; LogEvent evt = null; var log = ConfigFromJson(json) @@ -584,6 +648,46 @@ public void SinkWithStringArrayArgument() Assert.Equal(1, DummyRollingFileSink.Emitted.Count); } + [Fact] + public void DestructureWithCollectionsOfTypeArgument() + { + var json = @"{ + ""Serilog"": { + ""Using"": [ ""TestDummies"" ], + ""Destructure"": [{ + ""Name"": ""DummyArrayOfType"", + ""Args"": { + ""list"": [ + ""System.Byte"", + ""System.Int16"" + ], + ""array"" : [ + ""System.Int32"", + ""System.String"" + ], + ""type"" : ""System.TimeSpan"", + ""custom"" : [ + ""System.Int64"" + ], + ""customString"" : [ + ""System.UInt32"" + ] + } + }] + } + }"; + + DummyPolicy.Current = null; + + ConfigFromJson(json); + + Assert.Equal(typeof(TimeSpan), DummyPolicy.Current.Type); + Assert.Equal(new[] { typeof(int), typeof(string) }, DummyPolicy.Current.Array); + Assert.Equal(new[] { typeof(byte), typeof(short) }, DummyPolicy.Current.List); + Assert.Equal(typeof(long), DummyPolicy.Current.Custom.First); + Assert.Equal("System.UInt32", DummyPolicy.Current.CustomStrings.First); + } + [Fact] public void SinkWithIntArrayArgument() { diff --git a/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs b/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs index d8ab19a..be937a2 100644 --- a/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs @@ -65,7 +65,7 @@ public void ShouldProbePrivateBinPath() AppDomain.Unload(ad); } - void DoTestInner() + static void DoTestInner() { var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("customSink"); Assert.Equal(2, assemblyNames.Count); diff --git a/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs b/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs index b1b1f43..6b439f6 100644 --- a/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs @@ -21,6 +21,13 @@ public class DynamicLevelChangeTests } }, 'LevelSwitches': { '$mySwitch': 'Information' }, + 'FilterSwitches': { '$myFilter': null }, + 'Filter:Dummy': { + 'Name': 'ControlledBy', + 'Args': { + 'switch': '$myFilter' + } + }, 'WriteTo:Dummy': { 'Name': 'DummyConsole', 'Args': { @@ -64,10 +71,22 @@ public void ShouldRespectDynamicLevelChanges() UpdateConfig(overrideLevel: LogEventLevel.Debug); logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent()); Assert.Single(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(filterExpression: "Prop = 'Val_1'"); + logger.Write(Some.DebugEvent()); + logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent()); + Assert.Single(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(filterExpression: "Prop = 'Val_2'"); + logger.Write(Some.DebugEvent()); + logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent()); + Assert.Empty(DummyConsoleSink.Emitted); } } - void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null) + void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null, string filterExpression = null) { if (minimumLevel.HasValue) { @@ -84,6 +103,11 @@ void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel _configSource.Set("Serilog:MinimumLevel:Override:Root.Test", overrideLevel.Value.ToString()); } + if (filterExpression != null) + { + _configSource.Set("Serilog:FilterSwitches:$myFilter", filterExpression); + } + _configSource.Reload(); } } diff --git a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj index 69ceb38..9a02189 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -1,18 +1,8 @@  - - - net452;netcoreapp2.0 + net50;netcoreapp3.1;netcoreapp2.1;net452 + latest Serilog.Settings.Configuration.Tests ../../assets/Serilog.snk true @@ -29,23 +19,25 @@ - - + - - - + + - - - - + + - - + + + + + + + + diff --git a/test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs b/test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs index 710d0c8..07a848b 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs @@ -10,8 +10,7 @@ public class DelegatingSink : ILogEventSink public DelegatingSink(Action write) { - if (write == null) throw new ArgumentNullException(nameof(write)); - _write = write; + _write = write ?? throw new ArgumentNullException(nameof(write)); } public void Emit(LogEvent logEvent) diff --git a/test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs b/test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs index 02e3487..6ef5d96 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs @@ -8,5 +8,11 @@ public static object LiteralValue(this LogEventPropertyValue @this) { return ((ScalarValue)@this).Value; } + + public static string ToValidJson(this string str) + { + str = str.Replace('\'', '"'); + return str; + } } } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs b/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs index 31cf8ac..1175ee4 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs @@ -45,7 +45,7 @@ public JsonStringConfigProvider(string json) : base(new JsonConfigurationSource public override void Load() { - Load(StringToStream(_json)); + Load(StringToStream(_json.ToValidJson())); } static Stream StringToStream(string str) diff --git a/test/TestDummies/DummyLoggerConfigurationExtensions.cs b/test/TestDummies/DummyLoggerConfigurationExtensions.cs index ecae9cd..7c1d109 100644 --- a/test/TestDummies/DummyLoggerConfigurationExtensions.cs +++ b/test/TestDummies/DummyLoggerConfigurationExtensions.cs @@ -1,13 +1,13 @@ -using System; +using Microsoft.Extensions.Configuration; using Serilog; -using Serilog.Events; -using Serilog.Formatting; using Serilog.Configuration; using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using System; +using System.Collections.Generic; using TestDummies.Console; using TestDummies.Console.Themes; -using Microsoft.Extensions.Configuration; -using System.Collections.Generic; namespace TestDummies { @@ -63,13 +63,20 @@ public static LoggerConfiguration WithDummyThreadId(this LoggerEnrichmentConfigu public static LoggerConfiguration DummyRollingFile( this LoggerSinkConfiguration loggerSinkConfiguration, - List objectBinding, + List objectBinding, string pathFormat, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) { return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); } + public class Binding + { + public string Foo { get; set; } + + public string Abc { get; set; } + } + public static LoggerConfiguration DummyRollingFile( this LoggerSinkConfiguration loggerSinkConfiguration, string[] stringArrayBinding, @@ -133,5 +140,21 @@ string hardCodedString return loggerDestructuringConfiguration.With(new DummyHardCodedStringDestructuringPolicy(hardCodedString)); } + public static LoggerConfiguration DummyArrayOfType(this LoggerDestructuringConfiguration loggerSinkConfiguration, + List list, + Type[] array = null, + Type type = null, + CustomCollection custom = null, + CustomCollection customString = null) + { + return loggerSinkConfiguration.With(DummyPolicy.Current = new DummyPolicy + { + List = list, + Array = array, + Type = type, + Custom = custom, + CustomStrings = customString, + }); + } } } diff --git a/test/TestDummies/DummyPolicy.cs b/test/TestDummies/DummyPolicy.cs new file mode 100644 index 0000000..ada4c2f --- /dev/null +++ b/test/TestDummies/DummyPolicy.cs @@ -0,0 +1,48 @@ +using Serilog.Core; +using Serilog.Events; +using System; +using System.Collections; +using System.Collections.Generic; + +namespace TestDummies +{ + public class DummyPolicy : IDestructuringPolicy + { + public static DummyPolicy Current { get; set; } + + public Type[] Array { get; set; } + + public List List { get; set; } + + public CustomCollection Custom { get; set; } + + public CustomCollection CustomStrings { get; set; } + + public Type Type { get; set; } + + public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result) + { + result = null; + return false; + } + } + + public class CustomCollection : IEnumerable + { + private readonly List inner = new List(); + + public void Add(T item) => inner.Add(item); + + // wrong signature for collection initializer + public int Add() => 0; + + // wrong signature for collection initializer + public void Add(string a, byte b) { } + + public T First => inner.Count > 0 ? inner[0] : default; + + public IEnumerator GetEnumerator() => inner.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => inner.GetEnumerator(); + } +} diff --git a/test/TestDummies/DummyThreadIdEnricher.cs b/test/TestDummies/DummyThreadIdEnricher.cs index a640d55..3460d14 100644 --- a/test/TestDummies/DummyThreadIdEnricher.cs +++ b/test/TestDummies/DummyThreadIdEnricher.cs @@ -5,6 +5,10 @@ namespace TestDummies { public class DummyThreadIdEnricher : ILogEventEnricher { + static DummyThreadIdEnricher() + { + } + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { logEvent.AddPropertyIfAbsent(propertyFactory diff --git a/test/TestDummies/Properties/AssemblyInfo.cs b/test/TestDummies/Properties/AssemblyInfo.cs deleted file mode 100644 index 8903297..0000000 --- a/test/TestDummies/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("TestDummies")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("2bb12ce5-c867-43bd-ae5d-253fe3248c7f")] diff --git a/test/TestDummies/TestDummies.csproj b/test/TestDummies/TestDummies.csproj index 5c212f5..5806810 100644 --- a/test/TestDummies/TestDummies.csproj +++ b/test/TestDummies/TestDummies.csproj @@ -1,15 +1,11 @@  - net452;netstandard2.0 + netstandard2.0;net452 TestDummies ../../assets/Serilog.snk true true - TestDummies - false - false - false