├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── on-push-do-doco.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── icon.png └── src ├── Directory.Build.props ├── Directory.Build.targets ├── NuGetReadme.md ├── ZeroLog.Analyzers.Tests ├── DiscardedLogMessageAnalyzerTests.cs ├── LegacyStringInterpolationAnalyzerTests.cs ├── PrefixPatternAnalyzerTests.cs ├── UseStringInterpolationAnalyzerTests.cs ├── UseStringInterpolationCodeFixProviderTests.cs ├── ZeroLog.Analyzers.Tests.csproj └── ZeroLogAnalyzerTest.cs ├── ZeroLog.Analyzers ├── DiagnosticIds.cs ├── DiscardedLogMessageAnalyzer.cs ├── LegacyStringInterpolationAnalyzer.cs ├── PrefixPatternAnalyzer.cs ├── Properties │ └── AssemblyInfo.cs ├── Support │ ├── CompilerServices.cs │ └── Index.cs ├── UseStringInterpolationAnalyzer.cs ├── UseStringInterpolationCodeFixProvider.cs ├── ZeroLog.Analyzers.csproj └── ZeroLogFacts.cs ├── ZeroLog.Benchmarks ├── EnumTests │ └── EnumBenchmarks.cs ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── LatencyTests │ ├── LatencyBenchmarks.cs │ ├── Log4NetMultiProducer.cs │ ├── NLogAsyncMultiProducer.cs │ ├── NLogSyncMultiProducer.cs │ ├── SerilogMultiProducer.cs │ └── ZeroLogMultiProducer.cs ├── Logging │ ├── AppendingBenchmarks.cs │ └── BenchmarkLogMessageProvider.cs ├── Program.cs ├── ThroughputTests │ ├── ThroughputBenchmarks.cs │ └── ThroughputToFileBench.cs ├── Tools │ ├── Log4NetTestAppender.cs │ ├── NLogTestTarget.cs │ ├── SerilogTestSink.cs │ └── SimpleLatencyBenchmark.cs ├── ZeroLog.Benchmarks.csproj └── ZeroLog.Benchmarks.v3.ncrunchproject ├── ZeroLog.Impl.Base ├── ArgumentType.cs ├── Log.Generated.cs ├── Log.Generated.tt ├── Log.cs ├── LogLevel.cs ├── LogManager.cs ├── LogMessage.Append.cs ├── LogMessage.Generated.cs ├── LogMessage.Generated.tt ├── LogMessage.KeyValue.cs ├── LogMessage.Unmanaged.cs ├── LogMessage.cs ├── LogMetadata.ttinclude ├── Support │ └── Attributes.cs └── ZeroLog.Impl.Base.csproj ├── ZeroLog.Impl.Full ├── Appenders │ ├── Appender.cs │ ├── ConsoleAppender.cs │ ├── DateAndSizeRollingFileAppender.cs │ ├── NoopAppender.cs │ ├── StreamAppender.cs │ └── TextWriterAppender.cs ├── BufferSegmentProvider.cs ├── Configuration │ ├── AppenderConfiguration.cs │ ├── Enums.cs │ ├── LoggerConfiguration.cs │ ├── ResolvedLoggerConfiguration.cs │ └── ZeroLogConfiguration.cs ├── EnumArg.cs ├── EnumCache.cs ├── Formatting │ ├── CharBufferBuilder.cs │ ├── DefaultFormatter.cs │ ├── Formatter.cs │ ├── HexUtils.cs │ ├── JsonWriter.cs │ ├── KeyValueList.cs │ ├── LoggedKeyValue.cs │ ├── LoggedMessage.cs │ └── PrefixWriter.cs ├── GlobalUsings.cs ├── ILogMessageProvider.cs ├── Log.Impl.cs ├── LogManager.Impl.cs ├── LogMessage.Append.Impl.cs ├── LogMessage.Impl.cs ├── LogMessage.KeyValue.Impl.cs ├── LogMessage.Output.cs ├── LogMessage.Unmanaged.Impl.cs ├── ObjectPool.cs ├── Runner.cs ├── Support │ ├── ConcurrentQueueCapacityInitializer.cs │ └── TypeUtil.cs ├── UnmanagedArgHeader.cs ├── UnmanagedCache.cs └── ZeroLog.Impl.Full.csproj ├── ZeroLog.Tests.NetStandard ├── Initializer.cs ├── LogManagerTests.cs ├── LogMessageTests.cs ├── LogTests.cs ├── SanityChecks.cs ├── SanityChecks.should_export_expected_namespaces.verified.txt ├── SanityChecks.should_export_expected_types.verified.txt ├── SanityChecks.should_have_expected_public_api.verified.txt └── ZeroLog.Tests.NetStandard.csproj ├── ZeroLog.Tests ├── AllocationTests.cs ├── Appenders │ ├── AppenderTests.cs │ ├── DateAndSizeRollingFileAppenderTests.cs │ ├── StreamAppenderTests.cs │ └── TextWriterAppenderTests.cs ├── BufferSegmentProviderTests.cs ├── Configuration │ ├── LoggerConfigurationTests.cs │ ├── ResolvedLoggerConfigurationTests.cs │ └── ZeroLogConfigurationTests.cs ├── DocumentationTests.cs ├── EnumCacheTests.cs ├── Formatting │ ├── DefaultFormatterTests.cs │ ├── FormatterTests.cs │ ├── LoggedMessageTests.cs │ └── PrefixWriterTests.cs ├── IntegrationTests.cs ├── LogManagerTests.Config.cs ├── LogManagerTests.Enums.cs ├── LogManagerTests.cs ├── LogMessageTests.EnumTests.cs ├── LogMessageTests.MiscTests.cs ├── LogMessageTests.StringTests.cs ├── LogMessageTests.UnmanagedTests.cs ├── LogMessageTests.ValueTypeTests.cs ├── LogMessageTests.cs ├── LogTests.Messages.cs ├── LogTests.Messages.tt ├── LogTests.cs ├── ModuleInitializer.cs ├── ObjectPoolTests.cs ├── PerformanceAppender.cs ├── PerformanceTests.cs ├── RunnerTests.Async.cs ├── RunnerTests.Sync.cs ├── SanityChecks.cs ├── SanityChecks.should_export_expected_namespaces.verified.txt ├── SanityChecks.should_export_expected_types.verified.txt ├── SanityChecks.should_have_expected_public_api.DotNet6_0.verified.txt ├── SanityChecks.should_have_expected_public_api.DotNet7_0.verified.txt ├── SanityChecks.should_have_expected_public_api.DotNet8_0.verified.txt ├── SanityChecks.should_have_expected_public_api.DotNet9_0.verified.txt ├── Snippets.Init.cs ├── Snippets.cs ├── Support │ ├── AssertExtensions.cs │ ├── GcTester.cs │ ├── GcTesterTests.cs │ ├── HexUtilsTests.cs │ ├── TestTimeProvider.cs │ └── TypeUtilTests.cs ├── TestAppender.cs ├── TestLogMessageProvider.cs ├── UninitializedLogManagerTests.cs ├── Wait.cs └── ZeroLog.Tests.csproj ├── ZeroLog.sln ├── ZeroLog.sln.DotSettings ├── ZeroLog.snk ├── ZeroLog.v3.ncrunchsolution ├── ZeroLog ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── Properties │ ├── AssemblyData.cs │ └── AssemblyInfo.cs ├── ZeroLog.csproj └── ZeroLog.targets └── mdsnippets.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | 4 | [*] 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | end_of_line = crlf 8 | 9 | [*.{cs,csx,xaml,cake}] 10 | indent_size = 4 11 | indent_style = space 12 | 13 | [*.{config,nuspec,resx}] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [*.{csproj,vcxproj,props,targets}] 18 | indent_size = 2 19 | indent_style = space 20 | insert_final_newline = false 21 | 22 | [*.sln] 23 | indent_style = tab 24 | 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | 28 | [*.sh] 29 | end_of_line = lf 30 | 31 | [*.cs] 32 | resharper_indent_raw_literal_string = indent 33 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.verified.txt text eol=lf 2 | *.verified.xml text eol=lf 3 | *.verified.json text eol=lf 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [ push, pull_request ] 3 | 4 | env: 5 | DOTNET_NOLOGO: 1 6 | NUGET_CERT_REVOCATION_MODE: offline 7 | BUILD_DOTNET_VERSION: | 8 | 6.0.x 9 | 7.0.x 10 | 8.0.x 11 | 9.0.x 12 | 13 | jobs: 14 | windows: 15 | name: Windows 16 | runs-on: windows-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: ${{ env.BUILD_DOTNET_VERSION }} 25 | 26 | - name: Restore 27 | run: dotnet restore src/ZeroLog.sln 28 | 29 | - name: Build 30 | run: dotnet build --configuration Release --no-restore src/ZeroLog.sln 31 | 32 | - name: Pack 33 | run: dotnet pack --configuration Release --no-build src/ZeroLog.sln 34 | 35 | - name: Test 36 | run: dotnet test --configuration Release --no-build src/ZeroLog.sln 37 | 38 | - name: Upload NuGet 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: NuGet 42 | path: output/*.nupkg 43 | 44 | linux: 45 | name: Linux 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | 51 | - name: Setup .NET 52 | uses: actions/setup-dotnet@v4 53 | with: 54 | dotnet-version: ${{ env.BUILD_DOTNET_VERSION }} 55 | 56 | - name: Restore 57 | run: dotnet restore src/ZeroLog.sln 58 | 59 | - name: Build 60 | run: dotnet build --configuration Release --no-restore src/ZeroLog.sln 61 | 62 | - name: Test ZeroLog 63 | run: dotnet test --configuration Release --no-build src/ZeroLog.Tests/ZeroLog.Tests.csproj 64 | 65 | - name: Test Analyzers 66 | run: dotnet test --configuration Release --no-build src/ZeroLog.Analyzers.Tests/ZeroLog.Analyzers.Tests.csproj 67 | -------------------------------------------------------------------------------- /.github/workflows/on-push-do-doco.yml: -------------------------------------------------------------------------------- 1 | name: on-push-do-doco 2 | on: 3 | push: 4 | jobs: 5 | release: 6 | runs-on: windows-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Run MarkdownSnippets 10 | run: | 11 | dotnet tool install --global MarkdownSnippets.Tool 12 | mdsnippets ${GITHUB_WORKSPACE} 13 | shell: bash 14 | - name: Push changes 15 | run: | 16 | git config --local user.email "action@github.com" 17 | git config --local user.name "GitHub Action" 18 | git commit -m "Doco changes" -a || echo "nothing to commit" 19 | remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" 20 | branch="${GITHUB_REF:11}" 21 | git push "${remote}" ${branch} || echo "nothing to push" 22 | shell: bash 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | local-* 3 | 4 | .vs/ 5 | .vscode/ 6 | .idea/ 7 | output/ 8 | bin/ 9 | obj/ 10 | 11 | Thumbs.db 12 | 13 | *.user 14 | *.log 15 | *.binlog 16 | *.orig 17 | 18 | BenchmarkDotNet.Artifacts/ 19 | *.received.* 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ABC arbitrage 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abc-Arbitrage/ZeroLog/3d621607d0faa4b23d11cb27ed7722a22e39ae0b/icon.png -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13.0 4 | 9.0 5 | 4 6 | true 7 | true 8 | false 9 | $(DefaultItemExcludes);*.DotSettings;*.ncrunchproject;BenchmarkDotNet.Artifacts/** 10 | embedded 11 | false 12 | true 13 | direct 14 | true 15 | $(MSBuildThisFileDirectory)ZeroLog.snk 16 | true 17 | 18 | 19 | 20 | 2.2.0 21 | A high-performance, zero-allocation logging library. 22 | Reda Bouallou;Mendel Monteiro-Beckerman;Romain Verdier;Lucas Trzesniewski;Serge Farny 23 | https://github.com/Abc-Arbitrage/ZeroLog 24 | MIT 25 | ABC arbitrage 26 | Copyright © ABC arbitrage 2017-$([System.DateTime]::Now.ToString('yyyy')) 27 | log;logging;zero-allocation 28 | true 29 | true 30 | $(MSBuildThisFileDirectory)..\output 31 | 32 | 33 | 34 | false 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/NuGetReadme.md: -------------------------------------------------------------------------------- 1 | # ZeroLog 2 | 3 | **ZeroLog is a high-performance, zero-allocation .NET logging library**. 4 | 5 | It provides logging capabilities to be used in latency-sensitive applications, where garbage collections are undesirable. ZeroLog can be used in a complete zero-allocation manner, meaning that after the initialization phase, it will not allocate any managed object on the heap, thus preventing any GC from being triggered. 6 | 7 | .NET 6 and C# 10 or later are required to use this library. 8 | 9 | See the [GitHub repository](https://github.com/Abc-Arbitrage/ZeroLog) for more information. 10 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers.Tests/DiscardedLogMessageAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.CodeAnalysis.Testing; 3 | using NUnit.Framework; 4 | 5 | namespace ZeroLog.Analyzers.Tests; 6 | 7 | [TestFixture] 8 | public class DiscardedLogMessageAnalyzerTests 9 | { 10 | [Test] 11 | public Task should_not_report_usual_usage() 12 | { 13 | var test = new Test 14 | { 15 | TestCode = """ 16 | class C 17 | { 18 | void M(ZeroLog.Log log) 19 | => log.Info().Append(42).Log(); 20 | } 21 | """ 22 | }; 23 | 24 | return test.RunAsync(); 25 | } 26 | 27 | [Test] 28 | public Task should_report_missing_log() 29 | { 30 | var test = new Test 31 | { 32 | TestCode = """ 33 | class C 34 | { 35 | void M(ZeroLog.Log log) 36 | => log.Info().{|#0:Append|}(42); 37 | } 38 | """, 39 | ExpectedDiagnostics = 40 | { 41 | new DiagnosticResult(DiscardedLogMessageAnalyzer.DiscardedLogMessageDiagnostic).WithLocation(0) 42 | } 43 | }; 44 | 45 | return test.RunAsync(); 46 | } 47 | 48 | [Test] 49 | public Task should_not_report_explicit_discard() 50 | { 51 | var test = new Test 52 | { 53 | TestCode = """ 54 | class C 55 | { 56 | void M(ZeroLog.Log log) 57 | => _ = log.Info().Append(42); 58 | } 59 | """ 60 | }; 61 | 62 | return test.RunAsync(); 63 | } 64 | 65 | [Test] 66 | public Task should_report_discard_on_any_method_that_returns_log_message() 67 | { 68 | var test = new Test 69 | { 70 | TestCode = """ 71 | class C 72 | { 73 | void M() 74 | => {|#0:N|}(); 75 | 76 | ZeroLog.LogMessage N() 77 | => throw null; 78 | } 79 | """, 80 | ExpectedDiagnostics = 81 | { 82 | new DiagnosticResult(DiscardedLogMessageAnalyzer.DiscardedLogMessageDiagnostic).WithLocation(0) 83 | } 84 | }; 85 | 86 | return test.RunAsync(); 87 | } 88 | 89 | [Test] 90 | public Task should_not_report_returned_log_message() 91 | { 92 | var test = new Test 93 | { 94 | TestCode = """ 95 | class C 96 | { 97 | ZeroLog.LogMessage M(ZeroLog.Log log) 98 | => log.Info().Append(42); 99 | } 100 | """ 101 | }; 102 | 103 | return test.RunAsync(); 104 | } 105 | 106 | private class Test : ZeroLogAnalyzerTest; 107 | } 108 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers.Tests/LegacyStringInterpolationAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.Testing; 4 | using NUnit.Framework; 5 | 6 | namespace ZeroLog.Analyzers.Tests; 7 | 8 | [TestFixture] 9 | public class LegacyStringInterpolationAnalyzerTests 10 | { 11 | [Test] 12 | public Task should_not_report_interpolation_with_handler() 13 | { 14 | var test = new Test 15 | { 16 | LanguageVersion = LanguageVersion.CSharp10, 17 | TestCode = """ 18 | class C 19 | { 20 | void M(ZeroLog.Log log) 21 | => log.Info($"foo {42}"); 22 | } 23 | """ 24 | }; 25 | 26 | return test.RunAsync(); 27 | } 28 | 29 | [Test] 30 | public Task should_report_allocating_interpolation_on_log() 31 | { 32 | var test = new Test 33 | { 34 | LanguageVersion = LanguageVersion.CSharp9, 35 | TestCode = """ 36 | class C 37 | { 38 | void M(ZeroLog.Log log) 39 | => log.Info({|#0:$"foo {42}"|}); 40 | } 41 | """, 42 | ExpectedDiagnostics = 43 | { 44 | new DiagnosticResult(LegacyStringInterpolationAnalyzer.AllocatingStringInterpolationDiagnostic).WithLocation(0) 45 | } 46 | }; 47 | 48 | return test.RunAsync(); 49 | } 50 | 51 | [Test] 52 | public Task should_report_allocating_interpolation_on_log_message() 53 | { 54 | var test = new Test 55 | { 56 | LanguageVersion = LanguageVersion.CSharp9, 57 | TestCode = """ 58 | class C 59 | { 60 | void M(ZeroLog.LogMessage message) 61 | => message.Append({|#0:$"foo {42}"|}); 62 | } 63 | """, 64 | ExpectedDiagnostics = 65 | { 66 | new DiagnosticResult(LegacyStringInterpolationAnalyzer.AllocatingStringInterpolationDiagnostic).WithLocation(0) 67 | } 68 | }; 69 | 70 | return test.RunAsync(); 71 | } 72 | 73 | [Test] 74 | public Task should_not_report_unrelated_allocating_interpolation() 75 | { 76 | var test = new Test 77 | { 78 | TestCode = """ 79 | class C 80 | { 81 | void M() 82 | => N($"foo {42}"); 83 | 84 | void N(string value) { } 85 | } 86 | """ 87 | }; 88 | 89 | return test.RunAsync(); 90 | } 91 | 92 | private class Test : ZeroLogAnalyzerTest; 93 | } 94 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers.Tests/PrefixPatternAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.CodeAnalysis.Testing; 3 | using NUnit.Framework; 4 | 5 | namespace ZeroLog.Analyzers.Tests; 6 | 7 | [TestFixture] 8 | public class PrefixPatternAnalyzerTests 9 | { 10 | [Test] 11 | public Task should_report_invalid_pattern() 12 | { 13 | var test = new Test 14 | { 15 | TestCode = """ 16 | using ZeroLog.Formatting; 17 | 18 | class C 19 | { 20 | DefaultFormatter M() 21 | => new DefaultFormatter { PrefixPattern = {|#0:"%{level:-20}"|} }; 22 | } 23 | """, 24 | ExpectedDiagnostics = 25 | { 26 | new DiagnosticResult(PrefixPatternAnalyzer.InvalidPrefixPatternDiagnostic).WithLocation(0).WithArguments("%{level:-20}") 27 | } 28 | }; 29 | 30 | return test.RunAsync(); 31 | } 32 | 33 | [Test] 34 | public Task should_not_report_valid_pattern() 35 | { 36 | var test = new Test 37 | { 38 | TestCode = """ 39 | using ZeroLog.Formatting; 40 | 41 | class C 42 | { 43 | DefaultFormatter M() 44 | => new DefaultFormatter { PrefixPattern = "%{level:20}" }; 45 | } 46 | """ 47 | }; 48 | 49 | return test.RunAsync(); 50 | } 51 | 52 | [Test] 53 | public Task should_not_report_assignment_to_different_symbol() 54 | { 55 | var test = new Test 56 | { 57 | TestCode = """ 58 | using ZeroLog.Formatting; 59 | 60 | class C 61 | { 62 | C M() 63 | => new C { PrefixPattern = "%{level:-20}" }; 64 | 65 | string PrefixPattern { get; init; } 66 | } 67 | """ 68 | }; 69 | 70 | return test.RunAsync(); 71 | } 72 | 73 | private class Test : ZeroLogAnalyzerTest; 74 | } 75 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers.Tests/UseStringInterpolationAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.CodeAnalysis.Testing; 3 | using NUnit.Framework; 4 | 5 | namespace ZeroLog.Analyzers.Tests; 6 | 7 | [TestFixture] 8 | public class UseStringInterpolationAnalyzerTests 9 | { 10 | [Test] 11 | public Task should_report_direct_log_opportunity() 12 | { 13 | var test = new Test 14 | { 15 | TestCode = """ 16 | class C 17 | { 18 | void M(ZeroLog.Log log) 19 | => log.{|#0:Info|}().Append("Foo").Log(); 20 | } 21 | """, 22 | ExpectedDiagnostics = 23 | { 24 | new DiagnosticResult(UseStringInterpolationAnalyzer.UseStringInterpolationDiagnostic).WithLocation(0) 25 | } 26 | }; 27 | 28 | return test.RunAsync(); 29 | } 30 | 31 | [Test] 32 | public Task should_report_interpolation_opportunity() 33 | { 34 | var test = new Test 35 | { 36 | TestCode = """ 37 | class C 38 | { 39 | void M(ZeroLog.Log log) 40 | => log.{|#0:Info|}().Append(42).AppendEnum(System.DayOfWeek.Friday).Append(true).Log(); 41 | } 42 | """, 43 | ExpectedDiagnostics = 44 | { 45 | new DiagnosticResult(UseStringInterpolationAnalyzer.UseStringInterpolationDiagnostic).WithLocation(0) 46 | } 47 | }; 48 | 49 | return test.RunAsync(); 50 | } 51 | 52 | [Test] 53 | public Task should_not_report_interpolation_opportunity_when_key_value_pairs_are_used() 54 | { 55 | var test = new Test 56 | { 57 | TestCode = """ 58 | class C 59 | { 60 | void M(ZeroLog.Log log) 61 | => log.Info().AppendKeyValue("Key", "Value").Log(); 62 | } 63 | """ 64 | }; 65 | 66 | return test.RunAsync(); 67 | } 68 | 69 | [Test] 70 | public Task should_report_interpolation_opportunity_when_format_string_is_a_literal() 71 | { 72 | var test = new Test 73 | { 74 | TestCode = """ 75 | class C 76 | { 77 | void M1(ZeroLog.Log log) 78 | => log.{|#0:Info|}().Append(42, "X").Log(); 79 | 80 | void M2(ZeroLog.Log log) 81 | => log.{|#1:Info|}().Append(format: "X", value: 40 + 2).Log(); 82 | } 83 | """, 84 | ExpectedDiagnostics = 85 | { 86 | new DiagnosticResult(UseStringInterpolationAnalyzer.UseStringInterpolationDiagnostic).WithLocation(0), 87 | new DiagnosticResult(UseStringInterpolationAnalyzer.UseStringInterpolationDiagnostic).WithLocation(1) 88 | } 89 | }; 90 | 91 | return test.RunAsync(); 92 | } 93 | 94 | [Test] 95 | public Task should_not_report_interpolation_opportunity_when_format_string_is_not_a_literal() 96 | { 97 | var test = new Test 98 | { 99 | TestCode = """ 100 | class C 101 | { 102 | const string format = "X"; 103 | 104 | void M1(ZeroLog.Log log) 105 | => log.Info().Append(42, format).Log(); 106 | 107 | void M2(ZeroLog.Log log) 108 | => log.Info().Append(format: format, value: 42).Log(); 109 | } 110 | """ 111 | }; 112 | 113 | return test.RunAsync(); 114 | } 115 | 116 | [Test] 117 | public Task should_not_report_interpolation_opportunity_when_verbatim_inner_interpolations_are_used() 118 | { 119 | var test = new Test 120 | { 121 | TestCode = """ 122 | class C 123 | { 124 | const string format = "X"; 125 | 126 | void M(ZeroLog.Log log) 127 | => log.Info().Append($@"").Log(); 128 | } 129 | """ 130 | }; 131 | 132 | return test.RunAsync(); 133 | } 134 | 135 | private class Test : ZeroLogAnalyzerTest; 136 | } 137 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers.Tests/ZeroLog.Analyzers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | enable 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers.Tests/ZeroLogAnalyzerTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.IO; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeFixes; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.CSharp.Testing; 7 | using Microsoft.CodeAnalysis.Diagnostics; 8 | using Microsoft.CodeAnalysis.Testing; 9 | 10 | namespace ZeroLog.Analyzers.Tests; 11 | 12 | internal static class ZeroLogAnalyzerTest 13 | { 14 | private static readonly ReferenceAssemblies _netReferenceAssemblies = new( 15 | "net9.0", 16 | new PackageIdentity("Microsoft.NETCore.App.Ref", "9.0.0"), 17 | Path.Combine("ref", "net9.0") 18 | ); 19 | 20 | public static void ConfigureTest(AnalyzerTest test) 21 | { 22 | test.ReferenceAssemblies = _netReferenceAssemblies; 23 | test.TestState.AdditionalReferences.Add(typeof(LogManager).Assembly); 24 | } 25 | } 26 | 27 | internal abstract class ZeroLogAnalyzerTest : CSharpAnalyzerTest 28 | where TAnalyzer : DiagnosticAnalyzer, new() 29 | { 30 | [StringSyntax("csharp")] 31 | public new string TestCode 32 | { 33 | set => base.TestCode = value; 34 | } 35 | 36 | public LanguageVersion LanguageVersion { get; init; } = LanguageVersion.Default; 37 | 38 | protected ZeroLogAnalyzerTest() 39 | { 40 | ZeroLogAnalyzerTest.ConfigureTest(this); 41 | } 42 | 43 | protected override ParseOptions CreateParseOptions() 44 | => ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); 45 | } 46 | 47 | internal abstract class ZeroLogCodeFixTest : CSharpCodeFixTest 48 | where TAnalyzer : DiagnosticAnalyzer, new() 49 | where TCodeFix : CodeFixProvider, new() 50 | { 51 | [StringSyntax("csharp")] 52 | public new string TestCode 53 | { 54 | set => base.TestCode = value; 55 | } 56 | 57 | [StringSyntax("csharp")] 58 | public new string FixedCode 59 | { 60 | set => base.FixedCode = value; 61 | } 62 | 63 | protected ZeroLogCodeFixTest() 64 | { 65 | ZeroLogAnalyzerTest.ConfigureTest(this); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/DiagnosticIds.cs: -------------------------------------------------------------------------------- 1 | namespace ZeroLog.Analyzers; 2 | 3 | internal static class DiagnosticIds 4 | { 5 | public const string Category = "ZeroLog"; 6 | 7 | public const string DiscardedLogMessage = "ZL0001"; 8 | public const string AllocatingStringInterpolation = "ZL0002"; 9 | public const string UseStringInterpolation = "ZL0003"; 10 | public const string InvalidPrefixPattern = "ZL0004"; 11 | } 12 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/DiscardedLogMessageAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | using Microsoft.CodeAnalysis.Operations; 6 | 7 | namespace ZeroLog.Analyzers; 8 | 9 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 10 | public class DiscardedLogMessageAnalyzer : DiagnosticAnalyzer 11 | { 12 | public static readonly DiagnosticDescriptor DiscardedLogMessageDiagnostic = new( 13 | DiagnosticIds.DiscardedLogMessage, 14 | "Discarded LogMessage", 15 | "The returned LogMessage cannot be implicitly discarded. This is most often caused by a missing call to Log(). If needed, discard the return value explicitly.", 16 | DiagnosticIds.Category, 17 | DiagnosticSeverity.Error, 18 | true 19 | ); 20 | 21 | public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( 22 | DiscardedLogMessageDiagnostic 23 | ); 24 | 25 | public override void Initialize(AnalysisContext context) 26 | { 27 | context.EnableConcurrentExecution(); 28 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 29 | 30 | context.RegisterCompilationStartAction(AnalyzeCompilationStart); 31 | } 32 | 33 | private static void AnalyzeCompilationStart(CompilationStartAnalysisContext compilationStartContext) 34 | { 35 | var logMessageType = compilationStartContext.Compilation.GetTypeByMetadataName(ZeroLogFacts.TypeNames.LogMessage); 36 | if (logMessageType is null) 37 | return; 38 | 39 | compilationStartContext.RegisterOperationAction( 40 | operationContext => 41 | { 42 | var operation = (IExpressionStatementOperation)operationContext.Operation; 43 | 44 | if (operation.Operation.Kind == OperationKind.Invocation 45 | && SymbolEqualityComparer.Default.Equals(operation.Operation.Type, logMessageType)) 46 | { 47 | operationContext.ReportDiagnostic(Diagnostic.Create(DiscardedLogMessageDiagnostic, GetDiagnosticLocation(operation))); 48 | } 49 | }, 50 | OperationKind.ExpressionStatement 51 | ); 52 | } 53 | 54 | private static Location GetDiagnosticLocation(IExpressionStatementOperation operation) 55 | { 56 | return operation.Operation.Syntax switch 57 | { 58 | InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax expressionSyntax } => expressionSyntax.Name.GetLocation(), 59 | InvocationExpressionSyntax { Expression: IdentifierNameSyntax identifierNameSyntax } => identifierNameSyntax.GetLocation(), 60 | _ => operation.Syntax.GetLocation() 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/LegacyStringInterpolationAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Microsoft.CodeAnalysis.Operations; 7 | 8 | namespace ZeroLog.Analyzers; 9 | 10 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 11 | public class LegacyStringInterpolationAnalyzer : DiagnosticAnalyzer 12 | { 13 | public static readonly DiagnosticDescriptor AllocatingStringInterpolationDiagnostic = new( 14 | DiagnosticIds.AllocatingStringInterpolation, 15 | "Allocating string interpolation", 16 | "This string interpolation will allocate. Set the language version to C# 10 or greater to fix this.", 17 | DiagnosticIds.Category, 18 | DiagnosticSeverity.Warning, 19 | true 20 | ); 21 | 22 | public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( 23 | AllocatingStringInterpolationDiagnostic 24 | ); 25 | 26 | public override void Initialize(AnalysisContext context) 27 | { 28 | context.EnableConcurrentExecution(); 29 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 30 | 31 | context.RegisterCompilationStartAction(AnalyzeCompilationStart); 32 | } 33 | 34 | private static void AnalyzeCompilationStart(CompilationStartAnalysisContext compilationStartContext) 35 | { 36 | var compilation = (CSharpCompilation)compilationStartContext.Compilation; 37 | 38 | if (compilation.LanguageVersion >= LanguageVersion.CSharp10) 39 | return; 40 | 41 | var logType = compilation.GetTypeByMetadataName(ZeroLogFacts.TypeNames.Log); 42 | var logMessageType = compilation.GetTypeByMetadataName(ZeroLogFacts.TypeNames.LogMessage); 43 | 44 | if (logType is null || logMessageType is null) 45 | return; 46 | 47 | var stringParameters = logType.GetMembers() 48 | .Where(m => m.Kind == SymbolKind.Method && ZeroLogFacts.IsLogLevelName(m.Name)) 49 | .Concat( 50 | logMessageType.GetMembers(ZeroLogFacts.MethodNames.Append) 51 | .Where(m => m.Kind == SymbolKind.Method) 52 | ) 53 | .Cast() 54 | .Where(m => m.Parameters.Length > 0 && m.Parameters[0].Type.SpecialType == SpecialType.System_String) 55 | .Select(m => m.Parameters[0]) 56 | .ToImmutableHashSet(SymbolEqualityComparer.Default); 57 | 58 | compilationStartContext.RegisterOperationAction( 59 | operationContext => 60 | { 61 | if (operationContext.Operation.Parent?.Kind != OperationKind.Argument) 62 | return; 63 | 64 | var argumentOperation = (IArgumentOperation)operationContext.Operation.Parent; 65 | if (argumentOperation.Parameter is null) 66 | return; 67 | 68 | if (stringParameters.Contains(argumentOperation.Parameter)) 69 | operationContext.ReportDiagnostic(Diagnostic.Create(AllocatingStringInterpolationDiagnostic, operationContext.Operation.Syntax.GetLocation())); 70 | }, 71 | OperationKind.InterpolatedString 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/PrefixPatternAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | using Microsoft.CodeAnalysis.Operations; 6 | using ZeroLog.Formatting; 7 | 8 | namespace ZeroLog.Analyzers; 9 | 10 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 11 | public class PrefixPatternAnalyzer : DiagnosticAnalyzer 12 | { 13 | public static readonly DiagnosticDescriptor InvalidPrefixPatternDiagnostic = new( 14 | DiagnosticIds.InvalidPrefixPattern, 15 | "Invalid prefix pattern", 16 | "Invalid prefix pattern: {0}", 17 | DiagnosticIds.Category, 18 | DiagnosticSeverity.Error, 19 | true 20 | ); 21 | 22 | public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( 23 | InvalidPrefixPatternDiagnostic 24 | ); 25 | 26 | public override void Initialize(AnalysisContext context) 27 | { 28 | context.EnableConcurrentExecution(); 29 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 30 | 31 | context.RegisterCompilationStartAction(AnalyzeCompilationStart); 32 | } 33 | 34 | private static void AnalyzeCompilationStart(CompilationStartAnalysisContext compilationStartContext) 35 | { 36 | var compilation = (CSharpCompilation)compilationStartContext.Compilation; 37 | 38 | var defaultFormatterType = compilation.GetTypeByMetadataName(ZeroLogFacts.TypeNames.DefaultFormatter); 39 | var prefixPatternProperties = defaultFormatterType?.GetMembers(ZeroLogFacts.PropertyNames.PrefixPattern); 40 | if (prefixPatternProperties is not [IPropertySymbol prefixPatternProperty]) 41 | return; 42 | 43 | compilationStartContext.RegisterOperationAction( 44 | operationContext => 45 | { 46 | if (operationContext.Operation is IAssignmentOperation { Value.ConstantValue: { HasValue: true, Value: var pattern }, Target: IPropertyReferenceOperation { Property: var assignedProperty } } assignmentOperation 47 | && SymbolEqualityComparer.Default.Equals(assignedProperty, prefixPatternProperty) 48 | && !PrefixWriter.IsValidPattern(pattern as string)) 49 | { 50 | operationContext.ReportDiagnostic(Diagnostic.Create(InvalidPrefixPatternDiagnostic, assignmentOperation.Value.Syntax.GetLocation(), pattern ?? "null")); 51 | } 52 | }, 53 | OperationKind.SimpleAssignment 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using ZeroLog; 3 | 4 | [assembly: InternalsVisibleTo($"ZeroLog.Analyzers.Tests, PublicKey={AssemblyData.PublicKey}")] 5 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/Support/CompilerServices.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | 3 | namespace System.Runtime.CompilerServices; 4 | 5 | internal static class IsExternalInit; 6 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/Support/Index.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Globalization; 3 | using System.Runtime.CompilerServices; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace System; 7 | 8 | internal readonly struct Index : IEquatable 9 | { 10 | private readonly int _value; 11 | 12 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 13 | public Index(int value, bool fromEnd = false) 14 | { 15 | if (value < 0) 16 | ThrowValueArgumentOutOfRange_NeedNonNegNumException(); 17 | 18 | _value = fromEnd ? ~value : value; 19 | } 20 | 21 | private Index(int value) 22 | { 23 | _value = value; 24 | } 25 | 26 | public static Index Start => new(0); 27 | public static Index End => new(~0); 28 | 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public static Index FromStart(int value) 31 | { 32 | if (value < 0) 33 | ThrowValueArgumentOutOfRange_NeedNonNegNumException(); 34 | 35 | return new Index(value); 36 | } 37 | 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | public static Index FromEnd(int value) 40 | { 41 | if (value < 0) 42 | ThrowValueArgumentOutOfRange_NeedNonNegNumException(); 43 | 44 | return new Index(~value); 45 | } 46 | 47 | public int Value => _value < 0 ? ~_value : _value; 48 | public bool IsFromEnd => _value < 0; 49 | 50 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 51 | public int GetOffset(int length) 52 | { 53 | var offset = _value; 54 | if (IsFromEnd) 55 | offset += length + 1; 56 | return offset; 57 | } 58 | 59 | public override bool Equals(object? value) 60 | => value is Index index && _value == index._value; 61 | 62 | public bool Equals(Index other) 63 | => _value == other._value; 64 | 65 | public override int GetHashCode() 66 | => _value; 67 | 68 | public static implicit operator Index(int value) 69 | => FromStart(value); 70 | 71 | public override string ToString() 72 | => IsFromEnd 73 | ? '^' + Value.ToString(CultureInfo.InvariantCulture) 74 | : ((uint)Value).ToString(CultureInfo.InvariantCulture); 75 | 76 | [SuppressMessage("ReSharper", "NotResolvedInText")] 77 | private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() 78 | => throw new ArgumentOutOfRangeException("value", "Non-negative number required."); 79 | } 80 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/ZeroLog.Analyzers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | enable 5 | $(NoWarn);RS2008 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ZeroLog.Analyzers/ZeroLogFacts.cs: -------------------------------------------------------------------------------- 1 | namespace ZeroLog.Analyzers; 2 | 3 | internal static class ZeroLogFacts 4 | { 5 | public static bool IsLogLevelName(string? value) 6 | => value is "Trace" or "Debug" or "Info" or "Warn" or "Error" or "Fatal"; 7 | 8 | public static class TypeNames 9 | { 10 | public const string Log = "ZeroLog.Log"; 11 | public const string LogMessage = "ZeroLog.LogMessage"; 12 | public const string DefaultFormatter = "ZeroLog.Formatting.DefaultFormatter"; 13 | } 14 | 15 | public static class MethodNames 16 | { 17 | public const string Append = "Append"; 18 | public const string AppendEnum = "AppendEnum"; 19 | public const string Log = "Log"; 20 | } 21 | 22 | public static class ParameterNames 23 | { 24 | public const string FormatString = "format"; 25 | } 26 | 27 | public static class PropertyNames 28 | { 29 | public const string PrefixPattern = "PrefixPattern"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/EnumTests/EnumBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Jobs; 5 | using BenchmarkDotNet.Running; 6 | using InlineIL; 7 | using static InlineIL.IL.Emit; 8 | 9 | namespace ZeroLog.Benchmarks.EnumTests; 10 | 11 | public static class EnumBenchmarksRunner 12 | { 13 | public static void Run() 14 | { 15 | Validate(); 16 | BenchmarkRunner.Run(); 17 | } 18 | 19 | private static void Validate() 20 | { 21 | var benchmarks = new EnumBenchmarks(); 22 | var expected = benchmarks.Typeof(); 23 | 24 | if (benchmarks.TypeofCached() != expected) 25 | throw new InvalidOperationException(); 26 | 27 | if (benchmarks.TypedRef() != expected) 28 | throw new InvalidOperationException(); 29 | 30 | if (benchmarks.TypeHandleIl() != expected) 31 | throw new InvalidOperationException(); 32 | } 33 | } 34 | 35 | [MemoryDiagnoser] 36 | [SimpleJob(RuntimeMoniker.Net80)] 37 | public unsafe class EnumBenchmarks 38 | { 39 | [Benchmark(Baseline = true)] 40 | public IntPtr Typeof() => TypeofImpl(); 41 | 42 | [MethodImpl(MethodImplOptions.NoInlining)] 43 | private static IntPtr TypeofImpl() 44 | where T : struct 45 | { 46 | return typeof(T).TypeHandle.Value; 47 | } 48 | 49 | [Benchmark] 50 | public IntPtr TypeofCached() => TypeofCachedImpl(); 51 | 52 | [MethodImpl(MethodImplOptions.NoInlining)] 53 | private static IntPtr TypeofCachedImpl() 54 | where T : struct 55 | { 56 | return Cache.TypeHandle; 57 | } 58 | 59 | private struct Cache 60 | { 61 | public static readonly IntPtr TypeHandle = typeof(T).TypeHandle.Value; 62 | } 63 | 64 | [Benchmark] 65 | public IntPtr TypedRef() => TypedRefImpl(); 66 | 67 | [MethodImpl(MethodImplOptions.NoInlining)] 68 | private static IntPtr TypedRefImpl() 69 | where T : struct 70 | { 71 | #pragma warning disable CS8500 72 | var value = default(T); 73 | var typedRef = __makeref(value); 74 | return ((IntPtr*)&typedRef)[1]; 75 | #pragma warning restore CS8500 76 | } 77 | 78 | [Benchmark] 79 | public IntPtr TypeHandleIl() => TypeHandleIlImpl(); 80 | 81 | [MethodImpl(MethodImplOptions.NoInlining)] 82 | private static IntPtr TypeHandleIlImpl() 83 | where T : struct 84 | { 85 | IL.DeclareLocals( 86 | false, 87 | new LocalVar(typeof(RuntimeTypeHandle)) 88 | ); 89 | 90 | Ldtoken(typeof(T)); 91 | Stloc_0(); 92 | Ldloca_S(0); 93 | Call(new MethodRef(typeof(RuntimeTypeHandle), "get_" + nameof(RuntimeTypeHandle.Value))); 94 | return IL.Return(); 95 | } 96 | } 97 | 98 | public enum SomeEnum 99 | { 100 | Foo, 101 | Bar, 102 | Baz 103 | } 104 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Defines if sequence points should be generated for each emitted IL instruction. Default value: Debug 12 | 13 | 14 | 15 | 16 | 17 | Insert sequence points in Debug builds only (this is the default). 18 | 19 | 20 | 21 | 22 | Insert sequence points in Release builds only. 23 | 24 | 25 | 26 | 27 | Always insert sequence points. 28 | 29 | 30 | 31 | 32 | Never insert sequence points. 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Defines how warnings should be handled. Default value: Warnings 41 | 42 | 43 | 44 | 45 | 46 | Emit build warnings (this is the default). 47 | 48 | 49 | 50 | 51 | Do not emit warnings. 52 | 53 | 54 | 55 | 56 | Treat warnings as errors. 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 68 | 69 | 70 | 71 | 72 | A comma-separated list of error codes that can be safely ignored in assembly verification. 73 | 74 | 75 | 76 | 77 | 'false' to turn off automatic generation of the XML Schema file. 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/LatencyTests/Log4NetMultiProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using HdrHistogram; 4 | using log4net.Config; 5 | using log4net.Layout; 6 | using ZeroLog.Benchmarks.Tools; 7 | 8 | namespace ZeroLog.Benchmarks.LatencyTests; 9 | 10 | public class Log4NetMultiProducer 11 | { 12 | public SimpleLatencyBenchmarkResult Bench(int warmingMessageCount, int totalMessageCount, int producingThreadCount) 13 | { 14 | var repository = log4net.LogManager.GetRepository(Assembly.GetExecutingAssembly()); 15 | 16 | var layout = new PatternLayout("%-4timestamp [%thread] %-5level %logger %ndc - %message%newline"); 17 | layout.ActivateOptions(); 18 | var appender = new Log4NetTestAppender(false); 19 | appender.ActivateOptions(); 20 | BasicConfigurator.Configure(repository, appender); 21 | 22 | var logger = log4net.LogManager.GetLogger(repository.Name, nameof(appender)); 23 | var signal = appender.SetMessageCountTarget(totalMessageCount + warmingMessageCount); 24 | 25 | var produce = new Func(() => 26 | { 27 | var warmingMessageByProducer = warmingMessageCount / producingThreadCount; 28 | int[] counter = { 0 }; 29 | var warmingResult = SimpleLatencyBenchmark.Bench(() => logger.InfoFormat("Hi {0} ! It's {1:HH:mm:ss}, and the message is #{2}", "dude", DateTime.UtcNow, counter[0]++), warmingMessageByProducer); 30 | 31 | var messageByProducer = totalMessageCount / producingThreadCount; 32 | counter[0] = 0; 33 | return SimpleLatencyBenchmark.Bench(() => logger.InfoFormat("Hi {0} ! It's {1:HH:mm:ss}, and the message is #{2}", "dude", DateTime.UtcNow, counter[0]++), messageByProducer); 34 | }); 35 | 36 | return SimpleLatencyBenchmark.RunBench(producingThreadCount, produce, signal); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/LatencyTests/NLogAsyncMultiProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using HdrHistogram; 4 | using NLog.Config; 5 | using NLog.Targets.Wrappers; 6 | using ZeroLog.Benchmarks.Tools; 7 | 8 | namespace ZeroLog.Benchmarks.LatencyTests; 9 | 10 | public class NLogAsyncMultiProducer 11 | { 12 | public SimpleLatencyBenchmarkResult Bench(int queueSize, int warmingMessageCount, int totalMessageCount, int producingThreadCount) 13 | { 14 | var appender = new NLogTestTarget(false); 15 | var asyncTarget = new AsyncTargetWrapper(appender, queueSize, overflowAction: AsyncTargetWrapperOverflowAction.Block); 16 | 17 | var config = new LoggingConfiguration(); 18 | config.AddTarget(nameof(asyncTarget), asyncTarget); 19 | config.LoggingRules.Add(new LoggingRule(nameof(asyncTarget), NLog.LogLevel.Debug, asyncTarget)); 20 | NLog.LogManager.Configuration = config; 21 | NLog.LogManager.ReconfigExistingLoggers(); 22 | 23 | var logger = NLog.LogManager.GetLogger(nameof(asyncTarget)); 24 | 25 | 26 | var signal = appender.SetMessageCountTarget(warmingMessageCount + totalMessageCount); 27 | 28 | var produce = new Func(() => 29 | { 30 | var warmingMessageByProducer = warmingMessageCount / producingThreadCount; 31 | int[] counter = { 0 }; 32 | var warmingResult = SimpleLatencyBenchmark.Bench(() => logger.Info("Hi {0} ! It's {1:HH:mm:ss}, and the message is #{2}", "dude", DateTime.UtcNow, counter[0]++), warmingMessageByProducer); 33 | 34 | var messageByProducer = totalMessageCount / producingThreadCount; 35 | counter[0] = 0; 36 | return SimpleLatencyBenchmark.Bench(() => logger.Info("Hi {0} ! It's {1:HH:mm:ss}, and the message is #{2}", "dude", DateTime.UtcNow, counter[0]++), messageByProducer); 37 | }); 38 | 39 | var flusher = new Action(() => 40 | { 41 | while (!signal.IsSet) 42 | NLog.LogManager.Flush(); 43 | }); 44 | 45 | Task.Factory.StartNew(flusher, TaskCreationOptions.LongRunning); 46 | 47 | var result = SimpleLatencyBenchmark.RunBench(producingThreadCount, produce, signal); 48 | LogManager.Shutdown(); 49 | 50 | return result; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/LatencyTests/NLogSyncMultiProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HdrHistogram; 3 | using NLog.Config; 4 | using ZeroLog.Benchmarks.Tools; 5 | 6 | namespace ZeroLog.Benchmarks.LatencyTests; 7 | 8 | public class NLogSyncMultiProducer 9 | { 10 | public SimpleLatencyBenchmarkResult Bench(int warmingMessageCount, int totalMessageCount, int producingThreadCount) 11 | { 12 | var appender = new NLogTestTarget(false); 13 | 14 | var config = new LoggingConfiguration(); 15 | config.AddTarget(nameof(appender), appender); 16 | config.LoggingRules.Add(new LoggingRule(nameof(appender), NLog.LogLevel.Debug, appender)); 17 | NLog.LogManager.Configuration = config; 18 | NLog.LogManager.ReconfigExistingLoggers(); 19 | 20 | var logger = NLog.LogManager.GetLogger(nameof(appender)); 21 | 22 | 23 | var signal = appender.SetMessageCountTarget(totalMessageCount); 24 | 25 | var produce = new Func(() => 26 | { 27 | var warmingMessageByProducer = warmingMessageCount / producingThreadCount; 28 | int[] counter = { 0 }; 29 | var warmingResult = SimpleLatencyBenchmark.Bench(() => logger.Info("Hi {0} ! It's {1:HH:mm:ss}, and the message is #{2}", "dude", DateTime.UtcNow, counter[0]++), warmingMessageByProducer); 30 | 31 | var messageByProducer = totalMessageCount / producingThreadCount; 32 | counter[0] = 0; 33 | return SimpleLatencyBenchmark.Bench(() => logger.Info("Hi {0} ! It's {1:HH:mm:ss}, and the message is #{2}", "dude", DateTime.UtcNow, counter[0]++), messageByProducer); 34 | }); 35 | 36 | var result = SimpleLatencyBenchmark.RunBench(producingThreadCount, produce, signal); 37 | LogManager.Shutdown(); 38 | 39 | return result; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/LatencyTests/SerilogMultiProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HdrHistogram; 3 | using ZeroLog.Benchmarks.Tools; 4 | using ZeroLog.Configuration; 5 | using ZeroLog.Tests; 6 | 7 | namespace ZeroLog.Benchmarks.LatencyTests; 8 | 9 | public class SerilogMultiProducer 10 | { 11 | public SimpleLatencyBenchmarkResult Bench(int warmingMessageCount, int totalMessageCount, int producingThreadCount) 12 | { 13 | var sink = new SerilogTestSink(false); 14 | 15 | var logger = new Serilog.LoggerConfiguration() 16 | .WriteTo.Sink(sink) 17 | .CreateLogger(); 18 | 19 | var signal = sink.SetMessageCountTarget(warmingMessageCount + totalMessageCount); 20 | 21 | var produce = new Func(() => 22 | { 23 | var warmingMessageByProducer = warmingMessageCount / producingThreadCount; 24 | int[] counter = { 0 }; 25 | var warmingResult = SimpleLatencyBenchmark.Bench(() => logger.Information("Hi {name} ! It's {date:HH:mm:ss}, and the message is #{number}", "dude", DateTime.UtcNow, counter[0]++), warmingMessageByProducer); 26 | 27 | var messageByProducer = totalMessageCount / producingThreadCount; 28 | counter[0] = 0; 29 | return SimpleLatencyBenchmark.Bench(() => logger.Information("Hi {name} ! It's {date:HH:mm:ss}, and the message is #{number}", "dude", DateTime.UtcNow, counter[0]++), messageByProducer); 30 | }); 31 | 32 | var result = SimpleLatencyBenchmark.RunBench(producingThreadCount, produce, signal); 33 | 34 | return result; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/LatencyTests/ZeroLogMultiProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HdrHistogram; 3 | using ZeroLog.Benchmarks.Tools; 4 | using ZeroLog.Configuration; 5 | using ZeroLog.Tests; 6 | 7 | namespace ZeroLog.Benchmarks.LatencyTests; 8 | 9 | public class ZeroLogMultiProducer 10 | { 11 | public SimpleLatencyBenchmarkResult Bench(int queueSize, int warmingMessageCount, int totalMessageCount, int producingThreadCount) 12 | { 13 | var appender = new TestAppender(false); 14 | LogManager.Initialize(new ZeroLogConfiguration 15 | { 16 | RootLogger = 17 | { 18 | LogMessagePoolExhaustionStrategy = LogMessagePoolExhaustionStrategy.WaitUntilAvailable, 19 | Appenders = {appender} 20 | }, 21 | LogMessagePoolSize = queueSize, 22 | }); 23 | var logger = LogManager.GetLogger(nameof(ZeroLog)); 24 | 25 | var signal = appender.SetMessageCountTarget(warmingMessageCount + totalMessageCount); 26 | 27 | var produce = new Func(() => 28 | { 29 | var warmingMessageByProducer = warmingMessageCount / producingThreadCount; 30 | int[] counter = { 0 }; 31 | const string text = "dude"; 32 | var warmingResult = SimpleLatencyBenchmark.Bench(() => logger.Info($"Hi {text} ! It's {DateTime.UtcNow:HH:mm:ss}, and the message is #{counter[0]++}"), warmingMessageByProducer); 33 | 34 | var messageByProducer = totalMessageCount / producingThreadCount; 35 | counter[0] = 0; 36 | return SimpleLatencyBenchmark.Bench(() => logger.Info($"Hi {text} ! It's {DateTime.UtcNow:HH:mm:ss}, and the message is #{counter[0]++}"), messageByProducer); 37 | }); 38 | 39 | var result = SimpleLatencyBenchmark.RunBench(producingThreadCount, produce, signal); 40 | LogManager.Shutdown(); 41 | 42 | return result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/Logging/AppendingBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Attributes; 3 | using ZeroLog.Configuration; 4 | 5 | namespace ZeroLog.Benchmarks.Logging; 6 | 7 | public class AppendingBenchmarks 8 | { 9 | private BenchmarkLogMessageProvider _provider; 10 | private Log _log; 11 | 12 | private readonly int _intField = 42; 13 | private readonly string _stringField = "string"; 14 | private readonly double _doubleField = 42.42; 15 | private readonly DateTime _dateTimeField = new(2022, 02, 22); 16 | 17 | [GlobalSetup] 18 | public void GlobalSetup() 19 | { 20 | _provider = new BenchmarkLogMessageProvider(); 21 | _log = new Log("BenchmarkV2"); 22 | _log.UpdateConfiguration(_provider, ResolvedLoggerConfiguration.SingleAppender(LogLevel.Trace)); 23 | } 24 | 25 | [GlobalCleanup] 26 | public void GlobalCleanup() 27 | { 28 | LogManager.Shutdown(); 29 | } 30 | 31 | [Benchmark] 32 | public void SimpleString() 33 | { 34 | _log.Debug("Lorem ipsum dolor sit amet"); 35 | } 36 | 37 | [Benchmark] 38 | public void InterpolatedString2() 39 | { 40 | _log.Debug($"Lorem ipsum {_stringField} dolor sit amet {_stringField} dolor sit amet {_stringField} dolor sit amet {_stringField}"); 41 | } 42 | 43 | [Benchmark] 44 | public void InterpolatedString() 45 | { 46 | _log.Debug($"Lorem ipsum {_intField} dolor sit amet {_doubleField} dolor sit amet {_dateTimeField} dolor sit amet {_stringField}"); 47 | } 48 | 49 | [Benchmark] 50 | public void AppendedString() 51 | { 52 | _log.Debug() 53 | .Append("Lorem ipsum ").Append(_intField) 54 | .Append(" dolor sit amet ").Append(_doubleField) 55 | .Append(" dolor sit amet ").Append(_dateTimeField) 56 | .Append(" dolor sit amet ").Append(_stringField) 57 | .Log(); 58 | } 59 | 60 | [Benchmark] 61 | public void AppendedString2() 62 | { 63 | _log.Debug() 64 | .Append("Lorem ipsum ").Append(_stringField) 65 | .Append(" dolor sit amet ").Append(_stringField) 66 | .Append(" dolor sit amet ").Append(_stringField) 67 | .Append(" dolor sit amet ").Append(_stringField) 68 | .Log(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/Logging/BenchmarkLogMessageProvider.cs: -------------------------------------------------------------------------------- 1 | using ZeroLog.Configuration; 2 | 3 | namespace ZeroLog.Benchmarks.Logging; 4 | 5 | internal class BenchmarkLogMessageProvider : ILogMessageProvider 6 | { 7 | private readonly LogMessage _logMessage; 8 | 9 | public BenchmarkLogMessageProvider(int logMessageBufferSize = 128, int logMessageStringCapacity = 32) 10 | { 11 | _logMessage = LogMessage.CreateTestMessage(LogLevel.Trace, logMessageBufferSize, logMessageStringCapacity); 12 | } 13 | 14 | public LogMessage AcquireLogMessage(LogMessagePoolExhaustionStrategy poolExhaustionStrategy) 15 | => _logMessage; 16 | 17 | public void Submit(LogMessage message) 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Columns; 3 | using BenchmarkDotNet.Configs; 4 | using BenchmarkDotNet.Running; 5 | using ZeroLog.Benchmarks.LatencyTests; 6 | using ZeroLog.Benchmarks.ThroughputTests; 7 | using ZeroLog.Benchmarks.Tools; 8 | 9 | namespace ZeroLog.Benchmarks; 10 | 11 | public class Program 12 | { 13 | private static void Throughput() 14 | { 15 | var config = ManualConfig.Create(DefaultConfig.Instance); 16 | config.AddColumn(StatisticColumn.P90); 17 | config.AddColumn(StatisticColumn.P95); 18 | 19 | var benchs = BenchmarkConverter.TypeToBenchmarks(typeof(ThroughputBenchmarks), config); 20 | 21 | BenchmarkRunner.Run(benchs); 22 | } 23 | 24 | private static void LatencyMultiProducer(int threadCount, int warmupMessageCount, int messageCount, int queueSize) 25 | { 26 | var zeroLog = new ZeroLogMultiProducer().Bench(queueSize, warmupMessageCount, messageCount, threadCount); 27 | var nlogSync = new NLogSyncMultiProducer().Bench(warmupMessageCount, messageCount, threadCount); 28 | var nlogAsync = new NLogAsyncMultiProducer().Bench(queueSize, warmupMessageCount, messageCount, threadCount); 29 | var log4net = new Log4NetMultiProducer().Bench(warmupMessageCount, messageCount, threadCount); 30 | var serilog = new SerilogMultiProducer().Bench(warmupMessageCount, messageCount, threadCount); 31 | 32 | SimpleLatencyBenchmark.PrintSummary( 33 | $"{threadCount} producers, {messageCount:N0} total log events (queue size={queueSize:N0}) - unit is *us*", 34 | ("ZeroLog", zeroLog), 35 | ("NLogSync", nlogSync), 36 | ("NLogAsync", nlogAsync), 37 | ("Log4net", log4net), 38 | ("Serilog", serilog) 39 | ); 40 | } 41 | 42 | public static void Main() 43 | { 44 | //Throughput(); 45 | 46 | // LatencyMultiProducer(4, 4 * 25_000, 4 * 250_000, 64); 47 | //LatencyMultiProducer(8, 8 * 25_000, 8 * 250_000, 64); 48 | // LatencyMultiProducer(4, 4 * 25_000, 4 * 250_000, 1024); 49 | //LatencyMultiProducer(8, 8 * 25_000, 8 * 250_000, 1024); 50 | 51 | //EnumBenchmarksRunner.Run(); 52 | //ThroughputToFileBench.Run(); 53 | 54 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(); 55 | 56 | while (Console.KeyAvailable) 57 | Console.ReadKey(true); 58 | 59 | Console.WriteLine("Press enter to exit"); 60 | Console.ReadLine(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/ThroughputTests/ThroughputToFileBench.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using ZeroLog.Appenders; 5 | using ZeroLog.Configuration; 6 | 7 | namespace ZeroLog.Benchmarks.ThroughputTests; 8 | 9 | public class ThroughputToFileBench 10 | { 11 | public static void Run() 12 | { 13 | var dir = Path.GetFullPath(Guid.NewGuid().ToString()); 14 | Directory.CreateDirectory(dir); 15 | 16 | try 17 | { 18 | Console.WriteLine("Initializing..."); 19 | 20 | LogManager.Initialize(new ZeroLogConfiguration 21 | { 22 | LogMessagePoolSize = 1000 * 4096 * 4, 23 | RootLogger = 24 | { 25 | LogMessagePoolExhaustionStrategy = LogMessagePoolExhaustionStrategy.WaitUntilAvailable, 26 | Appenders = { new DateAndSizeRollingFileAppender(dir) { FileNamePrefix = "Output" } } 27 | } 28 | }); 29 | 30 | var log = LogManager.GetLogger(typeof(ThroughputToFileBench)); 31 | var duration = TimeSpan.FromSeconds(10); 32 | 33 | Console.WriteLine("Starting..."); 34 | 35 | GC.Collect(); 36 | GC.WaitForPendingFinalizers(); 37 | GC.Collect(); 38 | 39 | var sw = Stopwatch.StartNew(); 40 | long counter = 0; 41 | while (sw.Elapsed < duration) 42 | { 43 | counter++; 44 | log.Debug().Append("Counter is: ").Append(counter).Log(); 45 | } 46 | 47 | Console.WriteLine($"Log events: {counter:N0}, Time to append: {sw.Elapsed}"); 48 | Console.WriteLine("Flushing..."); 49 | LogManager.Shutdown(); 50 | Console.WriteLine($"Time to flush: {sw.Elapsed}"); 51 | } 52 | catch (Exception ex) 53 | { 54 | Console.WriteLine(ex); 55 | } 56 | finally 57 | { 58 | Directory.Delete(dir, true); 59 | } 60 | 61 | Console.WriteLine("Done"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/Tools/Log4NetTestAppender.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using log4net.Appender; 4 | using log4net.Core; 5 | 6 | namespace ZeroLog.Benchmarks; 7 | 8 | internal class Log4NetTestAppender : AppenderSkeleton 9 | { 10 | private readonly bool _captureLoggedMessages; 11 | private int _messageCount; 12 | private ManualResetEventSlim _signal; 13 | private int _messageCountTarget; 14 | 15 | public List LoggedMessages { get; } = new(); 16 | 17 | public Log4NetTestAppender(bool captureLoggedMessages) 18 | { 19 | _captureLoggedMessages = captureLoggedMessages; 20 | } 21 | 22 | public ManualResetEventSlim SetMessageCountTarget(int expectedMessageCount) 23 | { 24 | _signal = new ManualResetEventSlim(false); 25 | _messageCount = 0; 26 | _messageCountTarget = expectedMessageCount; 27 | return _signal; 28 | } 29 | 30 | protected override void Append(LoggingEvent loggingEvent) 31 | { 32 | var formatted = loggingEvent.RenderedMessage; 33 | 34 | if (_captureLoggedMessages) 35 | LoggedMessages.Add(loggingEvent.ToString()); 36 | 37 | if (++_messageCount == _messageCountTarget) 38 | _signal.Set(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/Tools/NLogTestTarget.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using NLog; 4 | using NLog.Targets; 5 | 6 | namespace ZeroLog.Benchmarks; 7 | 8 | [Target("NLogTestTarget")] 9 | internal class NLogTestTarget : NLog.Targets.TargetWithLayout 10 | { 11 | private readonly bool _captureLoggedMessages; 12 | private int _messageCount; 13 | private ManualResetEventSlim _signal; 14 | private int _messageCountTarget; 15 | 16 | public NLogTestTarget(bool captureLoggedMessages) 17 | { 18 | _captureLoggedMessages = captureLoggedMessages; 19 | } 20 | 21 | public List LoggedMessages { get; } = new(); 22 | 23 | public ManualResetEventSlim SetMessageCountTarget(int expectedMessageCount) 24 | { 25 | _signal = new ManualResetEventSlim(false); 26 | _messageCount = 0; 27 | _messageCountTarget = expectedMessageCount; 28 | return _signal; 29 | } 30 | 31 | protected override void Write(LogEventInfo logEvent) 32 | { 33 | var logMessage = this.Layout.Render(logEvent); 34 | 35 | if (_captureLoggedMessages) 36 | LoggedMessages.Add(logMessage); 37 | 38 | if (++_messageCount == _messageCountTarget) 39 | _signal.Set(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/Tools/SerilogTestSink.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using Serilog.Core; 4 | using Serilog.Events; 5 | 6 | namespace ZeroLog.Benchmarks.Tools; 7 | 8 | public class SerilogTestSink : ILogEventSink 9 | { 10 | private readonly bool _captureLoggedMessages; 11 | private readonly Lock _lock = new(); 12 | private int _messageCount; 13 | private ManualResetEventSlim _signal; 14 | private int _messageCountTarget; 15 | 16 | public List LoggedMessages { get; } = new(); 17 | 18 | public SerilogTestSink(bool captureLoggedMessages) 19 | { 20 | _captureLoggedMessages = captureLoggedMessages; 21 | } 22 | 23 | public ManualResetEventSlim SetMessageCountTarget(int expectedMessageCount) 24 | { 25 | _signal = new ManualResetEventSlim(false); 26 | _messageCount = 0; 27 | _messageCountTarget = expectedMessageCount; 28 | return _signal; 29 | } 30 | 31 | public void Emit(LogEvent logEvent) 32 | { 33 | var formatted = logEvent.RenderMessage(); 34 | 35 | lock (_lock) 36 | { 37 | if (_captureLoggedMessages) 38 | LoggedMessages.Add(formatted); 39 | 40 | if (++_messageCount == _messageCountTarget) 41 | _signal.Set(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/Tools/SimpleLatencyBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using HdrHistogram; 7 | 8 | namespace ZeroLog.Benchmarks.Tools; 9 | 10 | public class SimpleLatencyBenchmark 11 | { 12 | public static LongHistogram Bench(Action action, int count) 13 | { 14 | var histogram = new LongHistogram(TimeStamp.Minutes(1), 5); 15 | 16 | for (var i = 0; i < count; i++) 17 | histogram.Record(() => action()); 18 | 19 | return histogram; 20 | } 21 | 22 | public static void PrintSummary(string title, params (string name, SimpleLatencyBenchmarkResult result)[] results) 23 | { 24 | Console.WriteLine(title); 25 | Console.WriteLine(String.Join("", Enumerable.Range(0, title.Length).Select(_ => "="))); 26 | Console.WriteLine(); 27 | 28 | Console.WriteLine("+------------+--------+--------+--------+------------+------------+------------+------------+------------+------------+------------+"); 29 | Console.WriteLine("| Test | Mean | Median | P90 | P95 | P99 | P99.9 | P99.99 | P99.999 | Max | GC Count |"); 30 | Console.WriteLine("+------------+--------+--------+--------+------------+------------+------------+------------+------------+------------+------------+"); 31 | 32 | foreach (var (name, result) in results) 33 | { 34 | var histo = Concatenate(result.ExecutionTimes); 35 | 36 | Console.WriteLine($"| {name,-10} | {histo.GetMean(),6:N0} | {histo.GetValueAtPercentile(50),6:N0} | {histo.GetValueAtPercentile(90),6:N0} | {histo.GetValueAtPercentile(95),10:N0} | {histo.GetValueAtPercentile(99),10:N0} | {histo.GetValueAtPercentile(99.9),10:N0} | {histo.GetValueAtPercentile(99.99),10:N0} | {histo.GetValueAtPercentile(99.999),10:N0} | {histo.GetMaxValue(),10:N0} | {result.CollectionCount,10:N0} |"); 37 | } 38 | 39 | Console.WriteLine("+------------+--------+--------+--------+------------+------------+------------+------------+------------+------------+------------+"); 40 | } 41 | 42 | private static HistogramBase Concatenate(List seq) 43 | { 44 | var result = seq.First().Copy(); 45 | foreach (var h in seq.Skip(1)) 46 | result.Add(h); 47 | return result; 48 | } 49 | 50 | public static SimpleLatencyBenchmarkResult RunBench(int producingThreadCount, Func produce, ManualResetEventSlim signal) 51 | { 52 | var tasks = Enumerable.Range(0, producingThreadCount).Select(_ => new Task(produce, TaskCreationOptions.LongRunning)).ToList(); 53 | 54 | GC.Collect(2); 55 | GC.WaitForPendingFinalizers(); 56 | GC.Collect(2); 57 | var collectionsBefore = GC.CollectionCount(0); 58 | 59 | foreach (var task in tasks) 60 | { 61 | task.Start(); 62 | } 63 | 64 | signal.Wait(TimeSpan.FromSeconds(30)); 65 | 66 | var collectionsAfter = GC.CollectionCount(0); 67 | var result = new SimpleLatencyBenchmarkResult { ExecutionTimes = tasks.Select(x => x.Result).ToList(), CollectionCount = collectionsAfter - collectionsBefore }; 68 | return result; 69 | } 70 | } 71 | 72 | public class SimpleLatencyBenchmarkResult 73 | { 74 | public List ExecutionTimes { get; set; } 75 | public int CollectionCount { get; set; } 76 | } 77 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/ZeroLog.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | Exe 5 | $(NoWarn);CS8002 6 | false 7 | false 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/ZeroLog.Benchmarks/ZeroLog.Benchmarks.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/ArgumentType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ZeroLog; 4 | 5 | [Flags] 6 | internal enum ArgumentType : byte 7 | { 8 | None, 9 | 10 | String, 11 | Null, 12 | Char, 13 | Boolean, 14 | 15 | Byte, 16 | SByte, 17 | Int16, 18 | UInt16, 19 | Int32, 20 | UInt32, 21 | Int64, 22 | UInt64, 23 | IntPtr, 24 | UIntPtr, 25 | Single, 26 | Double, 27 | Decimal, 28 | 29 | Guid, 30 | DateTime, 31 | TimeSpan, 32 | DateOnly, 33 | TimeOnly, 34 | DateTimeOffset, 35 | StringSpan, 36 | Utf8StringSpan, 37 | Enum, 38 | Unmanaged, 39 | 40 | KeyString, 41 | EndOfTruncatedMessage, 42 | 43 | FormatFlag = 1 << 7 44 | } 45 | 46 | internal static class ArgumentTypeExtensions 47 | { 48 | public static bool IsNumeric(this ArgumentType argType) 49 | { 50 | switch (argType) 51 | { 52 | case ArgumentType.Byte: 53 | case ArgumentType.SByte: 54 | case ArgumentType.Int16: 55 | case ArgumentType.UInt16: 56 | case ArgumentType.Int32: 57 | case ArgumentType.UInt32: 58 | case ArgumentType.Int64: 59 | case ArgumentType.UInt64: 60 | case ArgumentType.IntPtr: 61 | case ArgumentType.UIntPtr: 62 | case ArgumentType.Single: 63 | case ArgumentType.Double: 64 | case ArgumentType.Decimal: 65 | return true; 66 | 67 | default: 68 | return false; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/Log.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | using System.Text; 4 | 5 | namespace ZeroLog; 6 | 7 | /// 8 | /// Represents a named logger used by applications to log messages. 9 | /// 10 | public sealed partial class Log 11 | { 12 | [SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")] 13 | private LogLevel _logLevel = LogLevel.None; 14 | 15 | internal string Name { get; } 16 | internal string CompactName { get; } 17 | 18 | internal Log(string name) 19 | { 20 | Name = name; 21 | CompactName = GetCompactName(name); 22 | } 23 | 24 | /// 25 | /// Indicates whether logs of the given level are enabled for this logger. 26 | /// 27 | /// The log level. 28 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 29 | public partial bool IsEnabled(LogLevel level); 30 | 31 | /// 32 | /// Returns a log message builder for the given level. 33 | /// 34 | /// The log level. 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public LogMessage ForLevel(LogLevel level) 37 | => IsEnabled(level) 38 | ? InternalAcquireLogMessage(level) 39 | : LogMessage.Empty; 40 | 41 | private partial LogMessage InternalAcquireLogMessage(LogLevel level); 42 | 43 | /// 44 | /// Returns the logger name. 45 | /// 46 | public override string ToString() 47 | => Name; 48 | 49 | internal static string GetCompactName(string? name) 50 | { 51 | name = name?.Trim('.'); 52 | 53 | if (name is null or "") 54 | return string.Empty; 55 | 56 | var lastDotIndex = name.LastIndexOf('.'); 57 | if (lastDotIndex < 0) 58 | return name; 59 | 60 | var sb = new StringBuilder(); 61 | var nextChar = 0; 62 | 63 | while (nextChar < lastDotIndex) 64 | { 65 | var c = name[nextChar]; 66 | 67 | if (c == '.') 68 | { 69 | ++nextChar; 70 | continue; 71 | } 72 | 73 | sb.Append(c); 74 | 75 | var nextDot = name.IndexOf('.', nextChar + 1); 76 | if (nextDot < 0) 77 | break; 78 | 79 | nextChar = nextDot + 1; 80 | } 81 | 82 | sb.Append(name, lastDotIndex, name.Length - lastDotIndex); 83 | return sb.ToString(); 84 | } 85 | 86 | #if NETSTANDARD 87 | 88 | public partial bool IsEnabled(LogLevel level) 89 | => false; 90 | 91 | private partial LogMessage InternalAcquireLogMessage(LogLevel level) 92 | => LogMessage.Empty; 93 | 94 | #endif 95 | } 96 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/LogLevel.cs: -------------------------------------------------------------------------------- 1 | namespace ZeroLog; 2 | 3 | /// 4 | /// Represents a log severity level. 5 | /// 6 | public enum LogLevel 7 | { 8 | // Same level values as in Microsoft.Extensions.Logging.LogLevel, but with different names 9 | 10 | /// 11 | /// The most detailed log level. 12 | /// 13 | Trace, 14 | 15 | /// 16 | /// Debugging information. 17 | /// 18 | Debug, 19 | 20 | /// 21 | /// Informational message. 22 | /// 23 | Info, 24 | 25 | /// 26 | /// Warning message. 27 | /// 28 | Warn, 29 | 30 | /// 31 | /// Error message. 32 | /// 33 | Error, 34 | 35 | /// 36 | /// Critical error message. 37 | /// 38 | Fatal, 39 | 40 | /// 41 | /// Represents a disabled log level. 42 | /// 43 | None 44 | } 45 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/LogManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace ZeroLog; 6 | 7 | /// 8 | /// The entry point of ZeroLog. 9 | /// 10 | [SuppressMessage("ReSharper", "PartialTypeWithSinglePart")] 11 | public sealed partial class LogManager 12 | { 13 | private static readonly ConcurrentDictionary _loggers = new(); 14 | 15 | /// 16 | /// Returns a logger for the given type. 17 | /// 18 | /// 19 | /// The logger name will be the full name of the type. 20 | /// 21 | /// The type. 22 | public static Log GetLogger() 23 | #if NET9_0_OR_GREATER 24 | where T : allows ref struct 25 | #endif 26 | => GetLogger(typeof(T)); 27 | 28 | /// 29 | /// Returns a logger for the given type. 30 | /// 31 | /// 32 | /// The logger name will be the full name of the type. 33 | /// 34 | /// The type. 35 | public static Log GetLogger(Type type) 36 | => GetLogger(type.FullName!); 37 | 38 | /// 39 | /// Returns a logger for the given name. 40 | /// 41 | /// The logger name. 42 | public static partial Log GetLogger(string name); 43 | 44 | #if NETSTANDARD 45 | 46 | public static partial Log GetLogger(string name) 47 | => _loggers.GetOrAdd(name, static n => new Log(n)); 48 | 49 | #endif 50 | } 51 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/LogMessage.KeyValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace ZeroLog; 6 | 7 | [SuppressMessage("ReSharper", "UnusedParameterInPartialMethod")] 8 | partial class LogMessage 9 | { 10 | /// 11 | /// Appends a value of type string to the message metadata. 12 | /// 13 | /// The key. 14 | /// The value. 15 | public LogMessage AppendKeyValue(string key, string? value) 16 | { 17 | InternalAppendKeyValue(key, value); 18 | return this; 19 | } 20 | 21 | /// 22 | /// Appends a value of enum type to the message metadata. 23 | /// 24 | /// The key. 25 | /// The value. 26 | public LogMessage AppendKeyValue(string key, T value) 27 | where T : struct, Enum 28 | { 29 | InternalAppendKeyValue(key, value); 30 | return this; 31 | } 32 | 33 | /// 34 | /// Appends a value of nullable enum type to the message metadata. 35 | /// 36 | /// The key. 37 | /// The value. 38 | public LogMessage AppendKeyValue(string key, T? value) 39 | where T : struct, Enum 40 | { 41 | InternalAppendKeyValue(key, value); 42 | return this; 43 | } 44 | 45 | /// 46 | /// Appends a value of type string span to the message metadata. This will copy the span and use buffer space. 47 | /// 48 | /// The key. 49 | /// The value. 50 | public LogMessage AppendKeyValue(string key, ReadOnlySpan value) 51 | { 52 | InternalAppendKeyValue(key, value); 53 | return this; 54 | } 55 | 56 | /// 57 | /// Appends a UTF-8 string to the message metadata. This will copy the span and use buffer space. 58 | /// 59 | /// The key. 60 | /// The value. 61 | public LogMessage AppendKeyValue(string key, ReadOnlySpan value) 62 | { 63 | InternalAppendKeyValue(key, value); 64 | return this; 65 | } 66 | 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | private partial void InternalAppendKeyValue(string key, string? value); 69 | 70 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 71 | private partial void InternalAppendKeyValue(string key, T value, ArgumentType argType) 72 | where T : unmanaged; 73 | 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | private partial void InternalAppendKeyValue(string key, T? value, ArgumentType argType) 76 | where T : unmanaged; 77 | 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | private partial void InternalAppendKeyValue(string key, T value) 80 | where T : struct, Enum; 81 | 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | private partial void InternalAppendKeyValue(string key, T? value) 84 | where T : struct, Enum; 85 | 86 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 87 | private partial void InternalAppendKeyValue(string key, ReadOnlySpan value); 88 | 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | private partial void InternalAppendKeyValue(string key, ReadOnlySpan value); 91 | 92 | #if NETSTANDARD 93 | 94 | private partial void InternalAppendKeyValue(string key, string? value) 95 | { 96 | } 97 | 98 | private partial void InternalAppendKeyValue(string key, T value, ArgumentType argType) 99 | where T : unmanaged 100 | { 101 | } 102 | 103 | private partial void InternalAppendKeyValue(string key, T? value, ArgumentType argType) 104 | where T : unmanaged 105 | { 106 | } 107 | 108 | private partial void InternalAppendKeyValue(string key, T value) 109 | where T : struct, Enum 110 | { 111 | } 112 | 113 | private partial void InternalAppendKeyValue(string key, T? value) 114 | where T : struct, Enum 115 | { 116 | } 117 | 118 | private partial void InternalAppendKeyValue(string key, ReadOnlySpan value) 119 | { 120 | } 121 | 122 | private partial void InternalAppendKeyValue(string key, ReadOnlySpan value) 123 | { 124 | } 125 | 126 | #endif 127 | } 128 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/LogMessage.Unmanaged.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace ZeroLog; 5 | 6 | [SuppressMessage("ReSharper", "UnusedParameterInPartialMethod")] 7 | partial class LogMessage 8 | { 9 | /// 10 | /// Appends an unmanaged value to the message. 11 | /// 12 | /// The value to append. 13 | /// The format string. 14 | /// The value type. 15 | public LogMessage AppendUnmanaged(T value, string? format = null) 16 | where T : unmanaged 17 | { 18 | InternalAppendUnmanaged(ref value, format); 19 | return this; 20 | } 21 | 22 | /// 23 | /// Appends a nullable unmanaged value to the message. 24 | /// 25 | /// The value to append. 26 | /// The format string. 27 | /// The value type. 28 | public LogMessage AppendUnmanaged(T? value, string? format = null) 29 | where T : unmanaged 30 | { 31 | InternalAppendUnmanaged(ref value, format); 32 | return this; 33 | } 34 | 35 | /// 36 | /// Appends an unmanaged value to the message. 37 | /// 38 | /// The value to append. 39 | /// The format string. 40 | /// The value type. 41 | public LogMessage AppendUnmanaged(ref T value, string? format = null) 42 | where T : unmanaged 43 | { 44 | InternalAppendUnmanaged(ref value, format); 45 | return this; 46 | } 47 | 48 | /// 49 | /// Appends a nullable unmanaged value to the message. 50 | /// 51 | /// The value to append. 52 | /// The format string. 53 | /// The value type. 54 | public LogMessage AppendUnmanaged(ref T? value, string? format = null) 55 | where T : unmanaged 56 | { 57 | InternalAppendUnmanaged(ref value, format); 58 | return this; 59 | } 60 | 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | private partial void InternalAppendUnmanaged(ref T value, string? format) 63 | where T : unmanaged; 64 | 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | private partial void InternalAppendUnmanaged(ref T? value, string? format) 67 | where T : unmanaged; 68 | 69 | #if NETSTANDARD 70 | 71 | private partial void InternalAppendUnmanaged(ref T value, string? format) 72 | where T : unmanaged 73 | { 74 | } 75 | 76 | private partial void InternalAppendUnmanaged(ref T? value, string? format) 77 | where T : unmanaged 78 | { 79 | } 80 | 81 | #endif 82 | } 83 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/LogMetadata.ttinclude: -------------------------------------------------------------------------------- 1 | <#+ 2 | private static readonly (string name, string argType, bool isFormattable, bool isNetCoreOnly)[] _valueTypes = 3 | { 4 | ("bool", "Boolean", false, false), 5 | ("byte", "Byte", true, false), 6 | ("sbyte", "SByte", true, false), 7 | ("char", "Char", false, false), 8 | ("short", "Int16", true, false), 9 | ("ushort", "UInt16", true, false), 10 | ("int", "Int32", true, false), 11 | ("uint", "UInt32", true, false), 12 | ("long", "Int64", true, false), 13 | ("ulong", "UInt64", true, false), 14 | ("nint", "IntPtr", true, false), 15 | ("nuint", "UIntPtr", true, false), 16 | ("float", "Single", true, false), 17 | ("double", "Double", true, false), 18 | ("decimal", "Decimal", true, false), 19 | ("Guid", "Guid", true, false), 20 | ("DateTime", "DateTime", true, false), 21 | ("TimeSpan", "TimeSpan", true, false), 22 | ("DateOnly", "DateOnly", true, true), 23 | ("TimeOnly", "TimeOnly", true, true), 24 | ("DateTimeOffset", "DateTimeOffset", true, false), 25 | }; 26 | 27 | private static readonly string[] _logLevels = 28 | { 29 | "Trace", 30 | "Debug", 31 | "Info", 32 | "Warn", 33 | "Error", 34 | "Fatal" 35 | }; 36 | #> 37 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/Support/Attributes.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD 2 | 3 | namespace System.Diagnostics.CodeAnalysis 4 | { 5 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] 6 | internal sealed class NotNullAttribute : Attribute; 7 | } 8 | 9 | namespace System.Runtime.CompilerServices 10 | { 11 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] 12 | internal sealed class InterpolatedStringHandlerAttribute : Attribute; 13 | 14 | [AttributeUsage(AttributeTargets.Parameter)] 15 | internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute 16 | { 17 | public InterpolatedStringHandlerArgumentAttribute(string argument) 18 | => Arguments = [argument]; 19 | 20 | public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) 21 | => Arguments = arguments; 22 | 23 | public string[] Arguments { get; } 24 | } 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Base/ZeroLog.Impl.Base.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | ZeroLog 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | TextTemplatingFileGenerator 15 | Log.Generated.cs 16 | 17 | 18 | True 19 | True 20 | Log.Generated.tt 21 | 22 | 23 | 24 | TextTemplatingFileGenerator 25 | LogMessage.Generated.cs 26 | 27 | 28 | True 29 | True 30 | LogMessage.Generated.tt 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Appenders/Appender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using ZeroLog.Configuration; 4 | using ZeroLog.Formatting; 5 | 6 | namespace ZeroLog.Appenders; 7 | 8 | /// 9 | /// An appender which handles logged messages. 10 | /// 11 | /// 12 | /// Messages are handled by appenders on a dedicated thread. 13 | /// 14 | public abstract class Appender : IDisposable 15 | { 16 | private readonly Stopwatch _quarantineStopwatch = new(); 17 | private bool _needsFlush; 18 | 19 | /// 20 | /// The minimum log level of messages this appender should handle. 21 | /// 22 | public LogLevel Level { get; init; } 23 | 24 | /// 25 | /// Handles a logged message. 26 | /// 27 | /// The logged message. 28 | public abstract void WriteMessage(LoggedMessage message); 29 | 30 | /// 31 | /// Flushes the appender. 32 | /// 33 | /// 34 | /// This is called each time the message queue is empty after a message has been handled. 35 | /// 36 | public virtual void Flush() 37 | { 38 | } 39 | 40 | /// 41 | public virtual void Dispose() 42 | { 43 | } 44 | 45 | internal void InternalWriteMessage(LoggedMessage message, ZeroLogConfiguration config) 46 | { 47 | if (_quarantineStopwatch.IsRunning && _quarantineStopwatch.Elapsed < config.AppenderQuarantineDelay) 48 | return; 49 | 50 | try 51 | { 52 | _needsFlush = true; 53 | WriteMessage(message); 54 | _quarantineStopwatch.Stop(); 55 | } 56 | catch 57 | { 58 | _quarantineStopwatch.Restart(); 59 | } 60 | } 61 | 62 | internal void InternalFlush() 63 | { 64 | if (!_needsFlush) 65 | return; 66 | 67 | try 68 | { 69 | _needsFlush = false; 70 | Flush(); 71 | } 72 | catch 73 | { 74 | _quarantineStopwatch.Restart(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Appenders/ConsoleAppender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ZeroLog.Formatting; 3 | 4 | namespace ZeroLog.Appenders; 5 | 6 | /// 7 | /// An appender which logs to the standard output. 8 | /// 9 | public class ConsoleAppender : StreamAppender 10 | { 11 | private LogLevel _lastLoggedLevel = LogLevel.None; 12 | 13 | /// 14 | /// Defines whether messages should be colored. 15 | /// 16 | /// 17 | /// True by default when the standard output is not redirected. 18 | /// 19 | public bool ColorOutput { get; init; } 20 | 21 | /// 22 | /// Initializes a new instance of the console appender. 23 | /// 24 | public ConsoleAppender() 25 | { 26 | Stream = Console.OpenStandardOutput(); 27 | Encoding = Console.OutputEncoding; 28 | ColorOutput = !Console.IsOutputRedirected; 29 | } 30 | 31 | /// 32 | public override void WriteMessage(LoggedMessage message) 33 | { 34 | if (ColorOutput) 35 | UpdateConsoleColor(message); 36 | 37 | base.WriteMessage(message); 38 | } 39 | 40 | /// 41 | public override void Flush() 42 | { 43 | base.Flush(); 44 | 45 | if (ColorOutput) 46 | Console.ResetColor(); 47 | 48 | _lastLoggedLevel = LogLevel.None; 49 | } 50 | 51 | private void UpdateConsoleColor(LoggedMessage message) 52 | { 53 | if (message.Level == _lastLoggedLevel) 54 | return; 55 | 56 | if (_lastLoggedLevel != LogLevel.None) 57 | base.Flush(); 58 | 59 | _lastLoggedLevel = message.Level; 60 | 61 | switch (message.Level) 62 | { 63 | case LogLevel.Fatal: 64 | Console.ForegroundColor = ConsoleColor.Black; 65 | Console.BackgroundColor = ConsoleColor.White; 66 | break; 67 | 68 | case LogLevel.Error: 69 | Console.BackgroundColor = ConsoleColor.Black; 70 | Console.ForegroundColor = ConsoleColor.Red; 71 | break; 72 | 73 | case LogLevel.Warn: 74 | Console.BackgroundColor = ConsoleColor.Black; 75 | Console.ForegroundColor = ConsoleColor.Yellow; 76 | break; 77 | 78 | case LogLevel.Info: 79 | Console.BackgroundColor = ConsoleColor.Black; 80 | Console.ForegroundColor = ConsoleColor.White; 81 | break; 82 | 83 | case LogLevel.Debug: 84 | case LogLevel.Trace: 85 | Console.BackgroundColor = ConsoleColor.Black; 86 | Console.ForegroundColor = ConsoleColor.Gray; 87 | break; 88 | 89 | default: 90 | goto case LogLevel.Info; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Appenders/NoopAppender.cs: -------------------------------------------------------------------------------- 1 | using ZeroLog.Formatting; 2 | 3 | namespace ZeroLog.Appenders; 4 | 5 | /// 6 | /// An appender which does nothing. 7 | /// 8 | public sealed class NoopAppender : Appender 9 | { 10 | /// 11 | /// Does nothing. 12 | /// 13 | /// The message to ignore. 14 | public override void WriteMessage(LoggedMessage message) 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Appenders/StreamAppender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Text; 5 | using ZeroLog.Formatting; 6 | 7 | namespace ZeroLog.Appenders; 8 | 9 | /// 10 | /// Base class for appenders which write to a . 11 | /// 12 | public abstract class StreamAppender : Appender 13 | { 14 | private byte[] _byteBuffer = []; 15 | 16 | private Encoding _encoding = Encoding.UTF8; 17 | private bool _useSpanGetBytes; 18 | private Formatter? _formatter; 19 | 20 | /// 21 | /// The stream to write to. 22 | /// 23 | protected internal Stream? Stream { get; set; } 24 | 25 | /// 26 | /// The encoding to use when writing to the stream. 27 | /// 28 | protected internal Encoding Encoding 29 | { 30 | get => _encoding; 31 | set 32 | { 33 | _encoding = value; 34 | UpdateEncodingSpecificData(); 35 | } 36 | } 37 | 38 | /// 39 | /// The formatter to use to convert log messages to text. 40 | /// 41 | public Formatter Formatter 42 | { 43 | get => _formatter ??= new DefaultFormatter(); 44 | init => _formatter = value; 45 | } 46 | 47 | /// 48 | /// Initializes a new instance of the stream appender. 49 | /// 50 | protected StreamAppender() 51 | { 52 | UpdateEncodingSpecificData(); 53 | } 54 | 55 | /// 56 | public override void Dispose() 57 | { 58 | Stream?.Dispose(); 59 | Stream = null; 60 | 61 | base.Dispose(); 62 | } 63 | 64 | /// 65 | public override void WriteMessage(LoggedMessage message) 66 | { 67 | if (Stream is null) 68 | return; 69 | 70 | if (_useSpanGetBytes) 71 | { 72 | var chars = Formatter.FormatMessage(message); 73 | var byteCount = _encoding.GetBytes(chars, _byteBuffer); 74 | Stream.Write(_byteBuffer, 0, byteCount); 75 | } 76 | else 77 | { 78 | Formatter.FormatMessage(message); 79 | var charBuffer = Formatter.GetBuffer(out var charCount); 80 | var byteCount = _encoding.GetBytes(charBuffer, 0, charCount, _byteBuffer, 0); 81 | Stream.Write(_byteBuffer, 0, byteCount); 82 | } 83 | } 84 | 85 | /// 86 | public override void Flush() 87 | { 88 | Stream?.Flush(); 89 | base.Flush(); 90 | } 91 | 92 | private void UpdateEncodingSpecificData() 93 | { 94 | var maxBytes = _encoding.GetMaxByteCount(LogManager.OutputBufferSize); 95 | 96 | // The base Encoding class allocates buffers in all non-abstract GetBytes overloads in order to call the abstract 97 | // GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) in the end. 98 | // If an encoding overrides the Span version of GetBytes, we assume it avoids this allocation 99 | // and it skips safety checks as those are guaranteed by the Span struct. In that case, we can call this overload directly. 100 | _useSpanGetBytes = OverridesSpanGetBytes(_encoding.GetType()); 101 | 102 | if (_byteBuffer.Length < maxBytes) 103 | _byteBuffer = GC.AllocateUninitializedArray(maxBytes); 104 | } 105 | 106 | internal static bool OverridesSpanGetBytes(Type encodingType) 107 | => encodingType.GetMethod(nameof(System.Text.Encoding.GetBytes), BindingFlags.Public | BindingFlags.Instance, [typeof(ReadOnlySpan), typeof(Span)])?.DeclaringType == encodingType; 108 | } 109 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Appenders/TextWriterAppender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using ZeroLog.Formatting; 5 | 6 | namespace ZeroLog.Appenders; 7 | 8 | /// 9 | /// Appenders which writes to a . 10 | /// 11 | public class TextWriterAppender : Appender 12 | { 13 | private TextWriter? _textWriter; 14 | private Formatter? _formatter; 15 | private bool _useSpanWrite; 16 | 17 | /// 18 | /// The target . 19 | /// 20 | public TextWriter? TextWriter 21 | { 22 | get => _textWriter; 23 | set => SetTextWriter(value); 24 | } 25 | 26 | /// 27 | /// The formatter to use to convert log messages to text. 28 | /// 29 | public Formatter Formatter 30 | { 31 | get => _formatter ??= new DefaultFormatter(); 32 | init => _formatter = value; 33 | } 34 | 35 | /// 36 | /// Initializes a new instance of the appender. 37 | /// 38 | public TextWriterAppender() 39 | { 40 | } 41 | 42 | /// 43 | /// Initializes a new instance of the appender. 44 | /// 45 | /// The target . 46 | public TextWriterAppender(TextWriter? textWriter) 47 | { 48 | TextWriter = textWriter; 49 | } 50 | 51 | /// 52 | public override void Dispose() 53 | { 54 | base.Dispose(); 55 | TextWriter?.Dispose(); 56 | } 57 | 58 | /// 59 | public override void WriteMessage(LoggedMessage message) 60 | { 61 | if (TextWriter is not { } writer) 62 | return; 63 | 64 | // Same logic as in StreamAppender: use the ROS overload if it's overridden, otherwise use the older overload. 65 | if (_useSpanWrite) 66 | { 67 | var chars = Formatter.FormatMessage(message); 68 | writer.Write(chars); 69 | } 70 | else 71 | { 72 | Formatter.FormatMessage(message); 73 | var buffer = Formatter.GetBuffer(out var length); 74 | writer.Write(buffer, 0, length); 75 | } 76 | } 77 | 78 | /// 79 | public override void Flush() 80 | { 81 | TextWriter?.Flush(); 82 | base.Flush(); 83 | } 84 | 85 | private void SetTextWriter(TextWriter? newTextWriter) 86 | { 87 | Flush(); 88 | 89 | var isSameType = _textWriter?.GetType() == newTextWriter?.GetType(); 90 | _textWriter = newTextWriter; 91 | 92 | if (!isSameType) 93 | _useSpanWrite = _textWriter is { } textWriter && OverridesSpanWrite(textWriter.GetType()); 94 | } 95 | 96 | internal static bool OverridesSpanWrite(Type textWriterType) 97 | => textWriterType.GetMethod(nameof(System.IO.TextWriter.Write), BindingFlags.Public | BindingFlags.Instance, [typeof(ReadOnlySpan)])?.DeclaringType == textWriterType; 98 | } 99 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/BufferSegmentProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ZeroLog; 4 | 5 | internal unsafe class BufferSegmentProvider 6 | { 7 | private readonly Lock _lock = new(); 8 | private readonly int _segmentCount; 9 | private readonly int _segmentSize; 10 | 11 | private byte[]? _currentBuffer; 12 | private int _currentSegment; 13 | 14 | internal int BufferSize => _segmentCount * _segmentSize; 15 | 16 | public BufferSegmentProvider(int segmentCount, int segmentSize) 17 | { 18 | if (segmentCount <= 0) 19 | throw new ArgumentOutOfRangeException(nameof(segmentCount), "Invalid pool size"); 20 | 21 | if (segmentSize <= 0) 22 | throw new ArgumentOutOfRangeException(nameof(segmentSize), "Invalid buffer size"); 23 | 24 | const int maxBufferSize = 1024 * 1024 * 1024; 25 | segmentSize = Math.Min(segmentSize, maxBufferSize); 26 | 27 | while ((long)segmentSize * segmentCount > maxBufferSize) 28 | segmentCount >>= 1; 29 | 30 | _segmentCount = segmentCount; 31 | _segmentSize = segmentSize; 32 | } 33 | 34 | public BufferSegment GetSegment() 35 | { 36 | lock (_lock) 37 | { 38 | if (_currentSegment >= _segmentCount || _currentBuffer is null) 39 | { 40 | _currentBuffer = GC.AllocateUninitializedArray(BufferSize, pinned: true); 41 | _currentSegment = 0; 42 | } 43 | 44 | fixed (byte* data = &_currentBuffer[_segmentSize * _currentSegment++]) 45 | { 46 | return new BufferSegment(data, _segmentSize, _currentBuffer); 47 | } 48 | } 49 | } 50 | 51 | public static BufferSegment CreateStandaloneSegment(int bufferSize) 52 | { 53 | var buffer = GC.AllocateUninitializedArray(bufferSize, pinned: true); 54 | 55 | fixed (byte* data = buffer) 56 | { 57 | return new BufferSegment(data, bufferSize, buffer); 58 | } 59 | } 60 | } 61 | 62 | internal unsafe struct BufferSegment(byte* data, int length, byte[]? underlyingBuffer) 63 | { 64 | public readonly byte* Data = data; 65 | public readonly int Length = length; 66 | public readonly byte[]? UnderlyingBuffer = underlyingBuffer; 67 | } 68 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Configuration/AppenderConfiguration.cs: -------------------------------------------------------------------------------- 1 | using ZeroLog.Appenders; 2 | 3 | namespace ZeroLog.Configuration; 4 | 5 | /// 6 | /// An appender configuration. Appenders can be implicitly cast to this class. 7 | /// 8 | public sealed class AppenderConfiguration 9 | { 10 | /// 11 | /// The appender to use. 12 | /// 13 | public Appender Appender { get; } 14 | 15 | /// 16 | /// The minimum log level of messages this appender should handle. 17 | /// This can be higher than the level defined on the appender. 18 | /// 19 | public LogLevel Level { get; set; } 20 | 21 | /// 22 | /// Creates a configuration for an appender. 23 | /// 24 | /// The appender to configure. 25 | public AppenderConfiguration(Appender appender) 26 | { 27 | Appender = appender; 28 | Level = appender.Level; 29 | } 30 | 31 | /// 32 | /// Creates a configuration for an appender. 33 | /// 34 | /// The appender to configure. 35 | public static implicit operator AppenderConfiguration(Appender appender) 36 | => new(appender); 37 | 38 | internal AppenderConfiguration Clone() 39 | => (AppenderConfiguration)MemberwiseClone(); 40 | } 41 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Configuration/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace ZeroLog.Configuration; 2 | 3 | /// 4 | /// The strategy to apply upon log message pool exhaustion. 5 | /// 6 | public enum LogMessagePoolExhaustionStrategy 7 | { 8 | /// 9 | /// Drop the log message and log an error instead. 10 | /// 11 | DropLogMessageAndNotifyAppenders = 0, 12 | 13 | /// 14 | /// Forget about the message. 15 | /// 16 | DropLogMessage = 1, 17 | 18 | /// 19 | /// Block until it's possible to log. This can potentially lock the application if log messages are never released back to the pool. 20 | /// 21 | WaitUntilAvailable = 2, 22 | 23 | /// 24 | /// Allocates a new log message. 25 | /// 26 | Allocate = 3, 27 | 28 | /// 29 | /// The default value is . 30 | /// 31 | Default = DropLogMessageAndNotifyAppenders 32 | } 33 | 34 | /// 35 | /// Specifies the way log messages are formatted and passed to appenders. 36 | /// 37 | public enum AppendingStrategy 38 | { 39 | /// 40 | /// Use a dedicated thread to format log messages and write them to appenders. 41 | /// 42 | /// 43 | /// Intended for production. Ensures minimal overhead on the thread logging a message. 44 | /// 45 | Asynchronous, 46 | 47 | /// 48 | /// Use the current thread to format log messages and write them to appenders. 49 | /// 50 | /// 51 | /// Intended for unit testing. Can cause contention if several threads try to log messages simultaneously. 52 | /// 53 | Synchronous 54 | } 55 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/EnumArg.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Globalization; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | using ZeroLog.Configuration; 7 | using ZeroLog.Support; 8 | 9 | namespace ZeroLog; 10 | 11 | [StructLayout(LayoutKind.Sequential)] 12 | [SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")] 13 | [SuppressMessage("ReSharper", "ReplaceWithPrimaryConstructorParameter")] 14 | internal readonly struct EnumArg(IntPtr typeHandle, ulong value) 15 | { 16 | private readonly IntPtr _typeHandle = typeHandle; 17 | private readonly ulong _value = value; 18 | 19 | public Type? Type => TypeUtil.GetTypeFromHandle(_typeHandle); 20 | 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | public bool TryFormat(Span destination, out int charsWritten, ZeroLogConfiguration config) 23 | { 24 | var enumString = GetString(config); 25 | 26 | if (enumString != null) 27 | { 28 | if (enumString.Length <= destination.Length) 29 | { 30 | enumString.CopyTo(destination); 31 | charsWritten = enumString.Length; 32 | return true; 33 | } 34 | 35 | charsWritten = 0; 36 | return false; 37 | } 38 | 39 | return TryAppendNumericValue(destination, out charsWritten); 40 | } 41 | 42 | [MethodImpl(MethodImplOptions.NoInlining)] 43 | private bool TryAppendNumericValue(Span destination, out int charsWritten) 44 | { 45 | if (_value <= long.MaxValue || !EnumCache.IsEnumSigned(_typeHandle)) 46 | return _value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); 47 | 48 | return unchecked((long)_value).TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); 49 | } 50 | 51 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 52 | public string? GetString(ZeroLogConfiguration config) 53 | => EnumCache.GetString(_typeHandle, _value, out var enumRegistered) 54 | ?? GetStringSlow(enumRegistered, config); 55 | 56 | [MethodImpl(MethodImplOptions.NoInlining)] 57 | private string? GetStringSlow(bool enumRegistered, ZeroLogConfiguration config) 58 | { 59 | if (enumRegistered || !config.AutoRegisterEnums) 60 | return null; 61 | 62 | if (Type is not { } type) 63 | return null; 64 | 65 | LogManager.RegisterEnum(type); 66 | return EnumCache.GetString(_typeHandle, _value, out _); 67 | } 68 | 69 | public bool TryGetValue(out T result) 70 | where T : unmanaged 71 | { 72 | if (Type == typeof(T)) 73 | { 74 | result = EnumCache.FromUInt64(_value); 75 | return true; 76 | } 77 | 78 | result = default; 79 | return false; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Formatting/CharBufferBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Globalization; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace ZeroLog.Formatting; 7 | 8 | [SuppressMessage("ReSharper", "ReplaceSliceWithRangeIndexer")] 9 | [SuppressMessage("ReSharper", "ReplaceWithPrimaryConstructorParameter")] 10 | internal ref struct CharBufferBuilder(Span buffer) 11 | { 12 | private readonly Span _buffer = buffer; 13 | private int _pos = 0; 14 | 15 | public int Length => _pos; 16 | 17 | public ReadOnlySpan GetOutput() 18 | => _buffer.Slice(0, _pos); 19 | 20 | public Span GetRemainingBuffer() 21 | => _buffer.Slice(_pos); 22 | 23 | public void IncrementPos(int chars) 24 | => _pos += chars; 25 | 26 | /// 27 | /// Appends a character, but does nothing if there is no more room for it. 28 | /// 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public void Append(char value) 31 | { 32 | if (_pos < _buffer.Length) 33 | _buffer[_pos++] = value; 34 | } 35 | 36 | public bool TryAppendWhole(ReadOnlySpan value) 37 | { 38 | if (value.Length <= _buffer.Length - _pos) 39 | { 40 | value.CopyTo(_buffer.Slice(_pos)); 41 | _pos += value.Length; 42 | return true; 43 | } 44 | 45 | return false; 46 | } 47 | 48 | public bool TryAppendPartial(ReadOnlySpan value) 49 | { 50 | if (value.Length <= _buffer.Length - _pos) 51 | { 52 | value.CopyTo(_buffer.Slice(_pos)); 53 | _pos += value.Length; 54 | return true; 55 | } 56 | 57 | var length = _buffer.Length - _pos; 58 | value.Slice(0, length).CopyTo(_buffer.Slice(_pos)); 59 | _pos += length; 60 | return false; 61 | } 62 | 63 | public void TryAppendPartial(char value, int count) 64 | { 65 | if (count > 0) 66 | { 67 | count = Math.Min(count, _buffer.Length - _pos); 68 | _buffer.Slice(_pos, count).Fill(value); 69 | _pos += count; 70 | } 71 | } 72 | 73 | public bool TryAppend(int value, string? format = null) 74 | { 75 | if (!value.TryFormat(_buffer.Slice(_pos), out var charsWritten, format, CultureInfo.InvariantCulture)) 76 | return false; 77 | 78 | _pos += charsWritten; 79 | return true; 80 | } 81 | 82 | public bool TryAppend(char value) 83 | { 84 | if (_pos < _buffer.Length) 85 | { 86 | _buffer[_pos] = value; 87 | ++_pos; 88 | return true; 89 | } 90 | 91 | return false; 92 | } 93 | 94 | public bool TryAppend(DateTime value, string? format) 95 | { 96 | if (!value.TryFormat(_buffer.Slice(_pos), out var charsWritten, format, CultureInfo.InvariantCulture)) 97 | return false; 98 | 99 | _pos += charsWritten; 100 | return true; 101 | } 102 | 103 | public bool TryAppend(TimeSpan value, string? format) 104 | { 105 | if (!value.TryFormat(_buffer.Slice(_pos), out var charsWritten, format, CultureInfo.InvariantCulture)) 106 | return false; 107 | 108 | _pos += charsWritten; 109 | return true; 110 | } 111 | 112 | public bool TryAppend(T value, string? format = null) 113 | where T : struct, ISpanFormattable 114 | { 115 | if (!value.TryFormat(_buffer.Slice(_pos), out var charsWritten, format, CultureInfo.InvariantCulture)) 116 | return false; 117 | 118 | _pos += charsWritten; 119 | return true; 120 | } 121 | 122 | public override string ToString() 123 | => GetOutput().ToString(); 124 | } 125 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Formatting/Formatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ZeroLog.Formatting; 4 | 5 | /// 6 | /// A formatter which converts a logged message to text. 7 | /// 8 | public abstract class Formatter 9 | { 10 | private readonly char[] _buffer = GC.AllocateUninitializedArray(LogManager.OutputBufferSize); 11 | private int _position; 12 | 13 | /// 14 | /// Formats the given message to text. 15 | /// 16 | /// The message to format. 17 | /// A span representing the text to log. 18 | public ReadOnlySpan FormatMessage(LoggedMessage message) 19 | { 20 | _position = 0; 21 | WriteMessage(message); 22 | return GetOutput(); 23 | } 24 | 25 | /// 26 | /// Formats the given message to text. 27 | /// 28 | /// 29 | /// Call to append text to the output. 30 | /// 31 | /// The message to format. 32 | protected abstract void WriteMessage(LoggedMessage message); 33 | 34 | /// 35 | /// Appends text to the output. 36 | /// 37 | /// The value to write. 38 | protected internal void Write(ReadOnlySpan value) 39 | { 40 | var charCount = Math.Min(value.Length, _buffer.Length - _position); 41 | value.Slice(0, charCount).CopyTo(_buffer.AsSpan(_position)); 42 | _position += charCount; 43 | } 44 | 45 | /// 46 | /// Appends text followed by a newline to the output. 47 | /// 48 | /// The value to write. 49 | protected internal void WriteLine(ReadOnlySpan value) 50 | { 51 | Write(value); 52 | WriteLine(); 53 | } 54 | 55 | /// 56 | /// Appends a newline to the output. 57 | /// 58 | /// 59 | /// If the buffer is full, the newline will be inserted by overwriting the last characters. 60 | /// 61 | protected internal void WriteLine() 62 | { 63 | if (_position <= _buffer.Length - Environment.NewLine.Length) 64 | { 65 | Environment.NewLine.AsSpan().CopyTo(_buffer.AsSpan(_position)); 66 | _position += Environment.NewLine.Length; 67 | } 68 | else 69 | { 70 | // Make sure to end the string with a newline 71 | Environment.NewLine.AsSpan().CopyTo(_buffer.AsSpan(_buffer.Length - Environment.NewLine.Length)); 72 | } 73 | } 74 | 75 | /// 76 | /// Returns a span of the current output. 77 | /// 78 | protected internal Span GetOutput() 79 | => _buffer.AsSpan(0, _position); 80 | 81 | /// 82 | /// Returns a span of the remaining buffer. Call after modifying it. 83 | /// 84 | protected Span GetRemainingBuffer() 85 | => _buffer.AsSpan(_position); 86 | 87 | /// 88 | /// Advances the position on the buffer returned by by . 89 | /// 90 | /// The character count to advance the position by. 91 | protected void AdvanceBy(int charCount) 92 | => _position += charCount; 93 | 94 | internal char[] GetBuffer(out int length) 95 | { 96 | length = _position; 97 | return _buffer; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Formatting/HexUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ZeroLog.Formatting; 4 | 5 | internal static class HexUtils 6 | { 7 | private static readonly char[] _hexTable = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; 8 | 9 | public static unsafe void AppendValueAsHex(byte* valuePtr, int size, Span destination) 10 | { 11 | for (var index = 0; index < size; ++index) 12 | { 13 | var char0Index = valuePtr[index] & 0xf; 14 | var char1Index = (valuePtr[index] & 0xf0) >> 4; 15 | 16 | destination[2 * index] = _hexTable[char1Index]; 17 | destination[2 * index + 1] = _hexTable[char0Index]; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Formatting/JsonWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace ZeroLog.Formatting; 5 | 6 | internal static unsafe class JsonWriter 7 | { 8 | public static void WriteJsonToStringBuffer(KeyValueList keyValueList, Span destination, out int charsWritten) 9 | { 10 | var builder = new CharBufferBuilder(destination); 11 | 12 | builder.Append('{'); 13 | builder.Append(' '); 14 | 15 | var first = true; 16 | 17 | foreach (var keyValue in keyValueList) 18 | { 19 | if (!first) 20 | { 21 | builder.Append(','); 22 | builder.Append(' '); 23 | } 24 | 25 | AppendString(ref builder, keyValue.Key); 26 | 27 | builder.Append(':'); 28 | builder.Append(' '); 29 | 30 | AppendJsonValue(ref builder, keyValue); 31 | 32 | first = false; 33 | } 34 | 35 | builder.Append(' '); 36 | builder.Append('}'); 37 | 38 | charsWritten = builder.Length; 39 | } 40 | 41 | private static void AppendJsonValue(ref CharBufferBuilder builder, in LoggedKeyValue keyValue) 42 | { 43 | if (keyValue.IsBoolean) 44 | builder.TryAppendWhole(keyValue.Value.SequenceEqual(bool.TrueString) ? "true" : "false"); 45 | else if (keyValue.IsNumeric) 46 | builder.TryAppendWhole(keyValue.Value); 47 | else if (keyValue.IsNull) 48 | builder.TryAppendWhole("null"); 49 | else 50 | AppendString(ref builder, keyValue.Value); 51 | } 52 | 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | private static void AppendString(ref CharBufferBuilder builder, ReadOnlySpan value) 55 | { 56 | builder.Append('"'); 57 | 58 | foreach (var c in value) 59 | AppendEscapedChar(c, ref builder); 60 | 61 | builder.Append('"'); 62 | } 63 | 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | private static void AppendEscapedChar(char c, ref CharBufferBuilder builder) 66 | { 67 | // Escape characters based on https://tools.ietf.org/html/rfc7159 68 | 69 | if (c is '\\' or '"' or <= '\u001F') 70 | AppendControlChar(c, ref builder); 71 | else 72 | builder.Append(c); 73 | } 74 | 75 | [MethodImpl(MethodImplOptions.NoInlining)] 76 | private static void AppendControlChar(char c, ref CharBufferBuilder builder) 77 | { 78 | switch (c) 79 | { 80 | case '"': 81 | builder.TryAppendWhole(@"\"""); 82 | break; 83 | 84 | case '\\': 85 | builder.TryAppendWhole(@"\\"); 86 | break; 87 | 88 | case '\b': 89 | builder.TryAppendWhole(@"\b"); 90 | break; 91 | 92 | case '\t': 93 | builder.TryAppendWhole(@"\t"); 94 | break; 95 | 96 | case '\n': 97 | builder.TryAppendWhole(@"\n"); 98 | break; 99 | 100 | case '\f': 101 | builder.TryAppendWhole(@"\f"); 102 | break; 103 | 104 | case '\r': 105 | builder.TryAppendWhole(@"\r"); 106 | break; 107 | 108 | default: 109 | { 110 | const string prefix = @"\u00"; 111 | var destination = builder.GetRemainingBuffer(); 112 | 113 | if (destination.Length >= prefix.Length + 2) 114 | { 115 | builder.TryAppendWhole(prefix); 116 | 117 | var byteValue = unchecked((byte)c); 118 | HexUtils.AppendValueAsHex(&byteValue, 1, builder.GetRemainingBuffer()); 119 | builder.IncrementPos(2); 120 | } 121 | 122 | break; 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Formatting/KeyValueList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace ZeroLog.Formatting; 7 | 8 | /// 9 | /// A list of log message metadata as key/value pairs. 10 | /// 11 | public sealed unsafe class KeyValueList 12 | { 13 | private readonly List _items; 14 | 15 | private readonly char[] _buffer; 16 | private readonly byte[]? _rawDataBuffer; 17 | private int _position; 18 | 19 | /// 20 | /// The number of items contained in the list. 21 | /// 22 | public int Count => _items.Count; 23 | 24 | internal KeyValueList(int bufferSize) 25 | { 26 | _items = new(byte.MaxValue); 27 | _buffer = GC.AllocateUninitializedArray(bufferSize); 28 | } 29 | 30 | internal KeyValueList(KeyValueList other) 31 | { 32 | _buffer = GC.AllocateUninitializedArray(other._position); 33 | other._buffer.AsSpan(0, other._position).CopyTo(_buffer); 34 | _position = other._position; 35 | _items = new(other._items); 36 | 37 | if (other._rawDataBuffer is null) 38 | { 39 | _rawDataBuffer = GC.AllocateUninitializedArray(_items.Sum(item => item.RawDataLength)); 40 | var offset = 0; 41 | 42 | foreach (ref var item in CollectionsMarshal.AsSpan(_items)) 43 | { 44 | new Span((void*)item.RawDataPointerOrOffset, item.RawDataLength).CopyTo(_rawDataBuffer.AsSpan(offset, item.RawDataLength)); 45 | item.RawDataPointerOrOffset = (nuint)offset; 46 | offset += item.RawDataLength; 47 | } 48 | } 49 | else 50 | { 51 | _rawDataBuffer = other._rawDataBuffer; 52 | } 53 | } 54 | 55 | /// 56 | /// Gets the item at the specified index. 57 | /// 58 | /// The item index. 59 | public LoggedKeyValue this[int index] 60 | { 61 | get 62 | { 63 | var item = _items[index]; 64 | 65 | return new LoggedKeyValue( 66 | item.Key, 67 | _buffer.AsSpan(item.StringValueOffset, item.StringValueLength), 68 | _rawDataBuffer is null 69 | ? new ReadOnlySpan((void*)item.RawDataPointerOrOffset, item.RawDataLength) 70 | : _rawDataBuffer.AsSpan((int)item.RawDataPointerOrOffset, item.RawDataLength) 71 | ); 72 | } 73 | } 74 | 75 | internal void Clear() 76 | { 77 | _position = 0; 78 | _items.Clear(); 79 | } 80 | 81 | internal Span GetRemainingBuffer() 82 | => _buffer.AsSpan(_position); 83 | 84 | internal void Add(string key, int stringValueLength, byte* rawDataPointer, int rawDataLength) 85 | { 86 | _items.Add(new InternalItem(key, _position, stringValueLength, (nuint)rawDataPointer, rawDataLength)); 87 | _position += stringValueLength; 88 | } 89 | 90 | /// 91 | /// Gets an enumerator over this list. 92 | /// 93 | public Enumerator GetEnumerator() 94 | => new(this); 95 | 96 | private record struct InternalItem( 97 | string Key, 98 | int StringValueOffset, 99 | int StringValueLength, 100 | nuint RawDataPointerOrOffset, 101 | int RawDataLength 102 | ); 103 | 104 | /// 105 | /// An enumerator over a . 106 | /// 107 | public ref struct Enumerator 108 | { 109 | private readonly KeyValueList _keyValueList; 110 | private int _index; 111 | 112 | internal Enumerator(KeyValueList keyValueList) 113 | { 114 | _keyValueList = keyValueList; 115 | _index = -1; 116 | } 117 | 118 | /// 119 | /// Gets the current item. 120 | /// 121 | public LoggedKeyValue Current => _keyValueList[_index]; 122 | 123 | /// 124 | /// Moves to the next item. 125 | /// 126 | public bool MoveNext() 127 | => ++_index < _keyValueList.Count; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | #if NET9_0_OR_GREATER 2 | global using Lock = System.Threading.Lock; 3 | #else 4 | global using Lock = System.Object; 5 | #endif 6 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/ILogMessageProvider.cs: -------------------------------------------------------------------------------- 1 | using ZeroLog.Configuration; 2 | 3 | namespace ZeroLog; 4 | 5 | internal interface ILogMessageProvider 6 | { 7 | LogMessage AcquireLogMessage(LogMessagePoolExhaustionStrategy poolExhaustionStrategy); 8 | void Submit(LogMessage message); 9 | } 10 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Log.Impl.cs: -------------------------------------------------------------------------------- 1 | using ZeroLog.Appenders; 2 | using ZeroLog.Configuration; 3 | 4 | namespace ZeroLog; 5 | 6 | partial class Log 7 | { 8 | private ILogMessageProvider? _logMessageProvider; 9 | 10 | internal ResolvedLoggerConfiguration Config { get; private set; } = ResolvedLoggerConfiguration.Empty; 11 | 12 | internal void UpdateConfiguration(ILogMessageProvider? provider, ZeroLogConfiguration? config) 13 | => UpdateConfiguration(provider, config?.ResolveLoggerConfiguration(Name)); 14 | 15 | internal void UpdateConfiguration(ILogMessageProvider? provider, ResolvedLoggerConfiguration? config) 16 | { 17 | _logMessageProvider = provider; 18 | Config = config ?? ResolvedLoggerConfiguration.Empty; 19 | _logLevel = Config.Level; 20 | } 21 | 22 | internal void DisableLogging() 23 | { 24 | _logMessageProvider = null; 25 | _logLevel = LogLevel.None; 26 | } 27 | 28 | public partial bool IsEnabled(LogLevel level) 29 | => level >= _logLevel; 30 | 31 | internal Appender[] GetAppenders(LogLevel level) 32 | => Config.GetAppenders(level); 33 | 34 | private partial LogMessage InternalAcquireLogMessage(LogLevel level) 35 | { 36 | var provider = _logMessageProvider; 37 | 38 | var logMessage = provider is not null 39 | ? provider.AcquireLogMessage(Config.LogMessagePoolExhaustionStrategy) 40 | : LogMessage.Empty; 41 | 42 | logMessage.Initialize(this, level); 43 | return logMessage; 44 | } 45 | 46 | internal void Submit(LogMessage message) 47 | => _logMessageProvider?.Submit(message); 48 | } 49 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/LogMessage.Impl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Threading; 4 | using ZeroLog.Configuration; 5 | 6 | namespace ZeroLog; 7 | 8 | unsafe partial class LogMessage 9 | { 10 | internal static readonly LogMessage Empty = new(string.Empty) { Level = LogLevel.None }; 11 | 12 | [SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "This field is a GC root for the underlying buffer")] 13 | private readonly byte[]? _underlyingBuffer; 14 | 15 | private readonly byte* _startOfBuffer; 16 | private readonly byte* _endOfBuffer; 17 | private readonly string?[] _strings; 18 | 19 | private byte* _dataPointer; 20 | private byte _stringIndex; 21 | private bool _isTruncated; 22 | 23 | internal Log? Logger { get; private set; } 24 | internal bool IsTruncated => _isTruncated; 25 | 26 | internal string? ConstantMessage { get; } 27 | internal bool ReturnToPool { get; set; } 28 | 29 | internal LogMessage(string message) 30 | { 31 | ConstantMessage = message; 32 | _strings = []; 33 | } 34 | 35 | internal LogMessage(BufferSegment bufferSegment, int stringCapacity) 36 | { 37 | stringCapacity = Math.Min(stringCapacity, byte.MaxValue); 38 | _strings = stringCapacity > 0 ? new string[stringCapacity] : []; 39 | 40 | _startOfBuffer = bufferSegment.Data; 41 | _dataPointer = bufferSegment.Data; 42 | _endOfBuffer = bufferSegment.Data + bufferSegment.Length; 43 | _underlyingBuffer = bufferSegment.UnderlyingBuffer; 44 | } 45 | 46 | internal void Initialize(Log? log, LogLevel level) 47 | { 48 | if (ReferenceEquals(this, Empty)) // Avoid overhead for ignored messages 49 | return; 50 | 51 | #if NET8_0_OR_GREATER 52 | Timestamp = log?.Config.TimeProvider.GetUtcNow().DateTime ?? DateTime.UtcNow; 53 | #else 54 | Timestamp = DateTime.UtcNow; 55 | #endif 56 | 57 | Level = level; 58 | Thread = Thread.CurrentThread; 59 | Exception = null; 60 | Logger = log; 61 | 62 | _dataPointer = _startOfBuffer; 63 | _stringIndex = 0; 64 | _isTruncated = false; 65 | 66 | #if DEBUG 67 | new Span(_startOfBuffer, (int)(_endOfBuffer - _startOfBuffer)).Fill(0); 68 | _strings.AsSpan().Fill(null); 69 | #endif 70 | } 71 | 72 | public partial void Log() 73 | { 74 | if (!ReferenceEquals(this, Empty)) 75 | Logger?.Submit(this); 76 | } 77 | 78 | /// 79 | /// Creates a log message for unit testing purposes. 80 | /// 81 | /// The message log level. 82 | /// The message buffer size. See . 83 | /// The string argument capacity. See . 84 | /// A standalone log message. 85 | public static LogMessage CreateTestMessage(LogLevel level, int bufferSize, int stringCapacity) 86 | { 87 | var message = new LogMessage(BufferSegmentProvider.CreateStandaloneSegment(bufferSize), stringCapacity); 88 | message.Initialize(null, level); 89 | return message; 90 | } 91 | 92 | internal LogMessage CloneMetadata() 93 | { 94 | return new LogMessage(string.Empty) 95 | { 96 | Level = Level, 97 | Timestamp = Timestamp, 98 | Thread = Thread, 99 | Exception = Exception, 100 | Logger = Logger, 101 | _isTruncated = _isTruncated 102 | }; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/LogMessage.Unmanaged.Impl.cs: -------------------------------------------------------------------------------- 1 | using ZeroLog.Support; 2 | 3 | namespace ZeroLog; 4 | 5 | unsafe partial class LogMessage 6 | { 7 | private partial void InternalAppendUnmanaged(ref T value, string? format) 8 | where T : unmanaged 9 | { 10 | if (string.IsNullOrEmpty(format)) 11 | { 12 | if (_dataPointer + sizeof(ArgumentType) + sizeof(UnmanagedArgHeader) + sizeof(T) <= _endOfBuffer) 13 | { 14 | *(ArgumentType*)_dataPointer = ArgumentType.Unmanaged; 15 | _dataPointer += sizeof(ArgumentType); 16 | 17 | *(UnmanagedArgHeader*)_dataPointer = new UnmanagedArgHeader(TypeUtil.TypeHandle, sizeof(T)); 18 | _dataPointer += sizeof(UnmanagedArgHeader); 19 | 20 | *(T*)_dataPointer = value; 21 | _dataPointer += sizeof(T); 22 | } 23 | else 24 | { 25 | TruncateMessage(); 26 | } 27 | } 28 | else 29 | { 30 | if (_dataPointer + sizeof(ArgumentType) + sizeof(byte) + sizeof(UnmanagedArgHeader) + sizeof(T) <= _endOfBuffer && _stringIndex < _strings.Length) 31 | { 32 | *(ArgumentType*)_dataPointer = ArgumentType.Unmanaged | ArgumentType.FormatFlag; 33 | _dataPointer += sizeof(ArgumentType); 34 | 35 | _strings[_stringIndex] = format; 36 | 37 | *_dataPointer = _stringIndex; 38 | ++_dataPointer; 39 | 40 | ++_stringIndex; 41 | 42 | *(UnmanagedArgHeader*)_dataPointer = new UnmanagedArgHeader(TypeUtil.TypeHandle, sizeof(T)); 43 | _dataPointer += sizeof(UnmanagedArgHeader); 44 | 45 | *(T*)_dataPointer = value; 46 | _dataPointer += sizeof(T); 47 | } 48 | else 49 | { 50 | TruncateMessage(); 51 | } 52 | } 53 | } 54 | 55 | private partial void InternalAppendUnmanaged(ref T? value, string? format) 56 | where T : unmanaged 57 | { 58 | if (value != null) 59 | { 60 | var notNullValue = value.GetValueOrDefault(); 61 | InternalAppendUnmanaged(ref notNullValue, format); 62 | } 63 | else 64 | { 65 | InternalAppendNull(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/ObjectPool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Threading; 5 | using ZeroLog.Support; 6 | 7 | #pragma warning disable CS0169 8 | 9 | namespace ZeroLog; 10 | 11 | internal class ObjectPool : IDisposable 12 | where T : class 13 | { 14 | private readonly ConcurrentQueue _queue; 15 | private readonly Func _factory; 16 | 17 | private object? _paddingBefore16; 18 | private object? _paddingBefore24; 19 | private object? _paddingBefore32; 20 | private object? _paddingBefore40; 21 | private object? _paddingBefore48; 22 | private object? _paddingBefore56; 23 | 24 | private T? _cached; // Put this on its own cache line 25 | 26 | private object? _paddingAfter8; 27 | private object? _paddingAfter16; 28 | private object? _paddingAfter24; 29 | private object? _paddingAfter32; 30 | private object? _paddingAfter40; 31 | private object? _paddingAfter48; 32 | private object? _paddingAfter56; 33 | 34 | private int _queueCapacity; 35 | private int _queueCount; // _pool.Count can be expensive 36 | 37 | public int Count => _queueCount + (_cached is not null ? 1 : 0); 38 | 39 | public ObjectPool(int size, Func factory) 40 | { 41 | _queueCapacity = size; 42 | _queueCount = _queueCapacity; 43 | _factory = factory; 44 | 45 | _queue = new ConcurrentQueue(new ConcurrentQueueCapacityInitializer(_queueCapacity)); 46 | 47 | for (var i = 0; i < _queueCapacity; ++i) 48 | _queue.Enqueue(CreateObject()); 49 | } 50 | 51 | public bool TryAcquire([MaybeNullWhen(false)] out T instance) 52 | { 53 | var cached = Interlocked.Exchange(ref _cached, null); 54 | if (cached is not null) 55 | { 56 | instance = cached; 57 | return true; 58 | } 59 | 60 | if (_queue.TryDequeue(out instance)) 61 | { 62 | Interlocked.Decrement(ref _queueCount); 63 | return true; 64 | } 65 | 66 | return false; 67 | } 68 | 69 | public void Release(T instance) 70 | { 71 | var cached = Interlocked.Exchange(ref _cached, instance); 72 | if (cached is null) 73 | return; 74 | 75 | // We need to check for the capacity, as more objects can be released than were acquired 76 | // when the "allocate" pool exhaustion strategy is used. 77 | // There is a race condition between the Enqueue and Increment calls, 78 | // so we may still exceed the capacity, but this is benign. 79 | 80 | if (_queueCount < _queueCapacity) 81 | { 82 | _queue.Enqueue(cached); 83 | Interlocked.Increment(ref _queueCount); 84 | } 85 | } 86 | 87 | public void Dispose() 88 | { 89 | _queue.Clear(); 90 | _queueCapacity = 0; 91 | _queueCount = 0; 92 | _cached = null; 93 | } 94 | 95 | public T CreateObject() 96 | => _factory(); 97 | } 98 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Support/ConcurrentQueueCapacityInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ZeroLog.Support; 7 | 8 | internal class ConcurrentQueueCapacityInitializer(int size) : ICollection 9 | { 10 | // Fake collection used to initialize the capacity of a ConcurrentQueue: 11 | // - Has a Count property set to the desired initial capacity 12 | // - Has a noop iterator 13 | 14 | public int Count { get; } = size; 15 | public bool IsReadOnly => true; 16 | 17 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 18 | public IEnumerator GetEnumerator() => Enumerable.Empty().GetEnumerator(); 19 | 20 | public void Add(T item) => throw new NotSupportedException(); 21 | public void Clear() => throw new NotSupportedException(); 22 | public bool Contains(T item) => throw new NotSupportedException(); 23 | public void CopyTo(T[] array, int arrayIndex) => throw new NotSupportedException(); 24 | public bool Remove(T item) => throw new NotSupportedException(); 25 | } 26 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/Support/TypeUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | using System.Runtime.InteropServices; 8 | using InlineIL; 9 | using static System.Linq.Expressions.Expression; 10 | 11 | namespace ZeroLog.Support; 12 | 13 | internal static class TypeUtil 14 | { 15 | public static IntPtr GetTypeHandleSlow(Type type) 16 | => type.TypeHandle.Value; 17 | 18 | #if NET7_0_OR_GREATER 19 | public static Type? GetTypeFromHandle(IntPtr typeHandle) 20 | => Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(typeHandle)); 21 | #else 22 | public static Type? GetTypeFromHandle(IntPtr typeHandle) 23 | => _getTypeFromHandleFunc.Invoke(typeHandle); 24 | 25 | private static readonly Func _getTypeFromHandleFunc = BuildGetTypeFromHandleFunc(); 26 | 27 | private static Func BuildGetTypeFromHandleFunc() 28 | { 29 | // The GetTypeFromHandleUnsafe method is the preferred way to get a Type from a handle before .NET 7, as it dates back to the .NET Framework. 30 | var method = typeof(Type).GetMethod("GetTypeFromHandleUnsafe", BindingFlags.Static | BindingFlags.NonPublic, null, [typeof(IntPtr)], null); 31 | if (method is not null) 32 | { 33 | var param = Parameter(typeof(IntPtr)); 34 | return Lambda>(Call(method, param), param).Compile(); 35 | } 36 | 37 | // The GetTypeFromHandleUnsafe method can get trimmed away on .NET 6: ArgIterator is the only type which uses this internal method of the core library, 38 | // and since varargs are only supported on non-ARM Windows, GetTypeFromHandleUnsafe will get removed on other platforms such as Linux. 39 | // To get around this, we use __reftype to convert the handle, but we need to build a TypedReference equivalent manually. 40 | return static handle => 41 | { 42 | IL.Push(new TypedReferenceLayout { Value = default, Type = handle }); 43 | IL.Emit.Refanytype(); 44 | IL.Emit.Call(MethodRef.Method(typeof(Type), nameof(Type.GetTypeFromHandle))); 45 | return IL.Return(); 46 | }; 47 | } 48 | 49 | [StructLayout(LayoutKind.Sequential)] 50 | private struct TypedReferenceLayout 51 | { 52 | public IntPtr Value; 53 | public IntPtr Type; 54 | } 55 | #endif 56 | 57 | public static bool GetIsUnmanagedSlow(Type type) 58 | { 59 | return !(bool)typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.IsReferenceOrContainsReferences), BindingFlags.Static | BindingFlags.Public)! 60 | .MakeGenericMethod(type) 61 | .Invoke(null, null)!; 62 | } 63 | 64 | /// 65 | /// Gets the types defined in the given assembly, except those which could not be loaded. 66 | /// 67 | [DebuggerStepThrough] 68 | public static Type[] GetLoadableTypes(Assembly assembly) 69 | { 70 | try 71 | { 72 | return assembly.GetTypes(); 73 | } 74 | catch (ReflectionTypeLoadException ex) 75 | { 76 | return ex.Types.Where(t => t is not null).ToArray()!; 77 | } 78 | } 79 | } 80 | 81 | internal static class TypeUtil 82 | { 83 | public static readonly IntPtr TypeHandle = TypeUtil.GetTypeHandleSlow(typeof(T)); 84 | } 85 | 86 | [SuppressMessage("ReSharper", "StaticMemberInGenericType")] 87 | internal static class TypeUtilSlow 88 | { 89 | // Initializing this type will allocate 90 | 91 | private static readonly Type? _underlyingType = Nullable.GetUnderlyingType(typeof(T)); 92 | 93 | public static readonly TypeCode UnderlyingTypeCode = Type.GetTypeCode(_underlyingType); 94 | public static readonly TypeCode UnderlyingEnumTypeCode = typeof(T).IsEnum ? Type.GetTypeCode(Enum.GetUnderlyingType(typeof(T))) : TypeCode.Empty; 95 | } 96 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/UnmanagedArgHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.InteropServices; 4 | using ZeroLog.Configuration; 5 | using ZeroLog.Formatting; 6 | using ZeroLog.Support; 7 | 8 | namespace ZeroLog; 9 | 10 | [StructLayout(LayoutKind.Sequential)] 11 | [SuppressMessage("ReSharper", "ReplaceSliceWithRangeIndexer")] 12 | internal readonly unsafe struct UnmanagedArgHeader(IntPtr typeHandle, int typeSize) 13 | { 14 | public Type? Type => TypeUtil.GetTypeFromHandle(typeHandle); 15 | public int Size => typeSize; 16 | 17 | public bool TryAppendTo(byte* valuePtr, Span destination, out int charsWritten, string? format, ZeroLogConfiguration config) 18 | { 19 | if (UnmanagedCache.TryGetFormatter(typeHandle, out var formatter)) 20 | return formatter.Invoke(valuePtr, destination, out charsWritten, format, config); 21 | 22 | return TryAppendUnformattedTo(valuePtr, destination, out charsWritten); 23 | } 24 | 25 | public bool TryAppendUnformattedTo(byte* valuePtr, Span destination, out int charsWritten) 26 | { 27 | const string prefix = "Unmanaged(0x"; 28 | const string suffix = ")"; 29 | 30 | var outputSize = prefix.Length + suffix.Length + 2 * typeSize; 31 | 32 | if (destination.Length < outputSize) 33 | { 34 | charsWritten = 0; 35 | return false; 36 | } 37 | 38 | prefix.CopyTo(destination); 39 | HexUtils.AppendValueAsHex(valuePtr, typeSize, destination.Slice(prefix.Length)); 40 | suffix.CopyTo(destination.Slice(prefix.Length + 2 * typeSize)); 41 | 42 | charsWritten = outputSize; 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ZeroLog.Impl.Full/ZeroLog.Impl.Full.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0;net8.0;net7.0;net6.0 4 | ZeroLog 5 | enable 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests.NetStandard/Initializer.cs: -------------------------------------------------------------------------------- 1 | using DiffEngine; 2 | using NUnit.Framework; 3 | using VerifyTests; 4 | 5 | namespace ZeroLog.Tests.NetStandard; 6 | 7 | [SetUpFixture] 8 | public static class Initializer 9 | { 10 | [OneTimeSetUp] 11 | public static void Initialize() 12 | { 13 | DiffRunner.Disabled = true; 14 | VerifyDiffPlex.Initialize(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests.NetStandard/LogManagerTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ZeroLog.Tests.Support; 3 | 4 | namespace ZeroLog.Tests.NetStandard; 5 | 6 | [TestFixture] 7 | public class LogManagerTests 8 | { 9 | [Test] 10 | public void should_return_cached_log_instance() 11 | { 12 | var fooLog = LogManager.GetLogger("Foo"); 13 | var barLog = LogManager.GetLogger("Bar"); 14 | var fooLog2 = LogManager.GetLogger("Foo"); 15 | 16 | fooLog.ShouldNotBeNull(); 17 | barLog.ShouldNotBeNull(); 18 | 19 | barLog.ShouldNotBeTheSameAs(fooLog); 20 | fooLog2.ShouldBeTheSameAs(fooLog); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests.NetStandard/LogMessageTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ZeroLog.Tests.Support; 3 | 4 | namespace ZeroLog.Tests.NetStandard; 5 | 6 | [TestFixture] 7 | public class LogMessageTests 8 | { 9 | [Test] 10 | public void should_return_empty_string() 11 | { 12 | LogMessage.Empty 13 | .Append(42) 14 | .ToString() 15 | .ShouldEqual(string.Empty); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests.NetStandard/LogTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Tests.Support; 4 | 5 | namespace ZeroLog.Tests.NetStandard; 6 | 7 | [TestFixture] 8 | public class LogTests 9 | { 10 | private Log _log; 11 | 12 | [SetUp] 13 | public void SetUp() 14 | { 15 | _log = new Log("Foo"); 16 | } 17 | 18 | [Test] 19 | public void should_not_throw_fatal() 20 | { 21 | _log.IsFatalEnabled.ShouldBeFalse(); 22 | _log.IsEnabled(LogLevel.Fatal).ShouldBeFalse(); 23 | 24 | _log.Fatal("Message"); 25 | _log.Fatal("Message", new InvalidOperationException()); 26 | 27 | _log.Fatal($"Message {42}"); 28 | _log.Fatal($"Message {42}", new InvalidOperationException()); 29 | 30 | _log.Fatal() 31 | .Append("Message") 32 | .Append($"Other {42}") 33 | .Append(42) 34 | .Log(); 35 | 36 | _log.Fatal().ShouldBeTheSameAs(LogMessage.Empty); 37 | _log.ForLevel(LogLevel.Fatal).ShouldBeTheSameAs(LogMessage.Empty); 38 | } 39 | 40 | [Test] 41 | public void should_not_throw_trace() 42 | { 43 | _log.IsTraceEnabled.ShouldBeFalse(); 44 | _log.IsEnabled(LogLevel.Trace).ShouldBeFalse(); 45 | 46 | _log.Trace("Message"); 47 | _log.Trace("Message", new InvalidOperationException()); 48 | 49 | _log.Trace($"Message {42}"); 50 | _log.Trace($"Message {42}", new InvalidOperationException()); 51 | 52 | _log.Trace() 53 | .Append("Message") 54 | .Append($"Other {42}") 55 | .Append(42) 56 | .Log(); 57 | 58 | _log.Trace().ShouldBeTheSameAs(LogMessage.Empty); 59 | _log.ForLevel(LogLevel.Trace).ShouldBeTheSameAs(LogMessage.Empty); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests.NetStandard/SanityChecks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using PublicApiGenerator; 6 | using VerifyNUnit; 7 | 8 | namespace ZeroLog.Tests.NetStandard; 9 | 10 | [TestFixture] 11 | public class SanityChecks 12 | { 13 | [Test] 14 | public Task should_export_expected_namespaces() 15 | { 16 | return Verifier.Verify( 17 | typeof(LogManager).Assembly 18 | .ExportedTypes 19 | .Select(i => i.Namespace) 20 | .OrderBy(i => i) 21 | .Distinct() 22 | ); 23 | } 24 | 25 | [Test] 26 | public Task should_export_expected_types() 27 | { 28 | return Verifier.Verify( 29 | typeof(LogManager).Assembly 30 | .ExportedTypes 31 | .Select(i => i.FullName) 32 | .OrderBy(i => i) 33 | .Distinct() 34 | ); 35 | } 36 | 37 | [Test] 38 | public Task should_have_expected_public_api() 39 | { 40 | return Verifier.Verify( 41 | typeof(LogManager).Assembly 42 | .GeneratePublicApi(new ApiGeneratorOptions 43 | { 44 | IncludeAssemblyAttributes = false, 45 | ExcludeAttributes = 46 | [ 47 | typeof(ObsoleteAttribute).FullName 48 | ] 49 | }) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests.NetStandard/SanityChecks.should_export_expected_namespaces.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | ZeroLog 3 | ] -------------------------------------------------------------------------------- /src/ZeroLog.Tests.NetStandard/SanityChecks.should_export_expected_types.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | ZeroLog.Log, 3 | ZeroLog.Log+DebugInterpolatedStringHandler, 4 | ZeroLog.Log+ErrorInterpolatedStringHandler, 5 | ZeroLog.Log+FatalInterpolatedStringHandler, 6 | ZeroLog.Log+InfoInterpolatedStringHandler, 7 | ZeroLog.Log+TraceInterpolatedStringHandler, 8 | ZeroLog.Log+WarnInterpolatedStringHandler, 9 | ZeroLog.LogLevel, 10 | ZeroLog.LogManager, 11 | ZeroLog.LogMessage, 12 | ZeroLog.LogMessage+AppendInterpolatedStringHandler, 13 | ZeroLog.LogMessage+AppendOperation`1 14 | ] -------------------------------------------------------------------------------- /src/ZeroLog.Tests.NetStandard/ZeroLog.Tests.NetStandard.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net48 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/BufferSegmentProviderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | using ZeroLog.Tests.Support; 5 | 6 | namespace ZeroLog.Tests; 7 | 8 | [TestFixture] 9 | public unsafe class BufferSegmentProviderTests 10 | { 11 | private BufferSegmentProvider _bufferSegmentProvider; 12 | 13 | private const int _segmentCount = 4; 14 | private const int _segmentSize = 8; 15 | 16 | [SetUp] 17 | public void SetUp() 18 | { 19 | _bufferSegmentProvider = new BufferSegmentProvider(_segmentCount, _segmentSize); 20 | } 21 | 22 | [Test] 23 | public void should_get_buffer_segment_when_there_are_segments_available() 24 | { 25 | var bufferSegment = _bufferSegmentProvider.GetSegment(); 26 | 27 | bufferSegment.Length.ShouldEqual(_segmentSize); 28 | new IntPtr(bufferSegment.Data).ShouldNotEqual(IntPtr.Zero); 29 | bufferSegment.UnderlyingBuffer.ShouldNotBeNull(); 30 | } 31 | 32 | [Test] 33 | public void should_get_all_segments_from_a_large_buffer() 34 | { 35 | var segments = new List(); 36 | 37 | for (var i = 0; i < _segmentCount; ++i) 38 | segments.Add(_bufferSegmentProvider.GetSegment()); 39 | 40 | var lastAddress = (nuint)segments[0].Data; 41 | 42 | for (var i = 1; i < segments.Count; i++) 43 | { 44 | var bufferSegment = segments[i]; 45 | ((nuint)bufferSegment.Data).ShouldEqual(lastAddress + _segmentSize); 46 | bufferSegment.Length.ShouldEqual(_segmentSize); 47 | lastAddress = (nuint)bufferSegment.Data; 48 | } 49 | } 50 | 51 | [Test] 52 | public void should_allocate_a_new_large_buffer_when_needed() 53 | { 54 | var segments = new List(); 55 | 56 | for (var i = 0; i < 2 * _segmentCount; i++) 57 | segments.Add(_bufferSegmentProvider.GetSegment()); 58 | 59 | segments[_segmentCount - 1].UnderlyingBuffer.ShouldBeTheSameAs(segments[0].UnderlyingBuffer); 60 | segments[_segmentCount].UnderlyingBuffer.ShouldNotBeTheSameAs(segments[_segmentCount - 1].UnderlyingBuffer); 61 | segments[_segmentCount + 1].UnderlyingBuffer.ShouldBeTheSameAs(segments[_segmentCount].UnderlyingBuffer); 62 | 63 | ((nuint)segments[_segmentCount + 1].Data).ShouldEqual((nuint)segments[_segmentCount].Data + _segmentSize); 64 | } 65 | 66 | [Test] 67 | public void should_validate_arguments() 68 | { 69 | Assert.Throws(() => _ = new BufferSegmentProvider(0, 1)); 70 | Assert.Throws(() => _ = new BufferSegmentProvider(-1, 1)); 71 | Assert.Throws(() => _ = new BufferSegmentProvider(1, 0)); 72 | Assert.Throws(() => _ = new BufferSegmentProvider(1, -1)); 73 | } 74 | 75 | [Test] 76 | public void should_limit_buffer_size() 77 | { 78 | var provider = new BufferSegmentProvider(4 * 1024, 1024 * 1024); 79 | provider.BufferSize.ShouldEqual(1024 * 1024 * 1024); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Configuration/LoggerConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ZeroLog.Configuration; 3 | using ZeroLog.Tests.Support; 4 | 5 | namespace ZeroLog.Tests.Configuration; 6 | 7 | [TestFixture] 8 | public class LoggerConfigurationTests 9 | { 10 | [Test] 11 | public void should_initialize_config_with_name() 12 | { 13 | var config = new LoggerConfiguration("Foo"); 14 | config.Name.ShouldEqual("Foo"); 15 | config.Level.ShouldBeNull(); 16 | } 17 | 18 | [Test] 19 | public void should_initialize_config_with_name_and_level() 20 | { 21 | var config = new LoggerConfiguration("Foo", LogLevel.Info); 22 | config.Name.ShouldEqual("Foo"); 23 | config.Level.ShouldEqual(LogLevel.Info); 24 | } 25 | 26 | [Test] 27 | public void should_initialize_config_with_type() 28 | { 29 | var config = new LoggerConfiguration(typeof(LoggerConfigurationTests)); 30 | config.Name.ShouldEqual(typeof(LoggerConfigurationTests).FullName); 31 | config.Level.ShouldBeNull(); 32 | } 33 | 34 | [Test] 35 | public void should_initialize_config_with_type_and_level() 36 | { 37 | var config = new LoggerConfiguration(typeof(LoggerConfigurationTests), LogLevel.Info); 38 | config.Name.ShouldEqual(typeof(LoggerConfigurationTests).FullName); 39 | config.Level.ShouldEqual(LogLevel.Info); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Configuration/ZeroLogConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ZeroLog.Configuration; 3 | using ZeroLog.Tests.Support; 4 | 5 | namespace ZeroLog.Tests.Configuration; 6 | 7 | [TestFixture] 8 | public class ZeroLogConfigurationTests 9 | { 10 | [Test] 11 | public void should_set_log_level() 12 | { 13 | var config = new ZeroLogConfiguration(); 14 | config.Loggers.ShouldBeEmpty(); 15 | 16 | config.SetLogLevel("Foo", LogLevel.Info); 17 | 18 | var loggerConfig = config.Loggers.ShouldHaveSingleItem(); 19 | loggerConfig.Name.ShouldEqual("Foo"); 20 | loggerConfig.Level.ShouldEqual(LogLevel.Info); 21 | 22 | config.SetLogLevel("Foo", LogLevel.Warn); 23 | config.Loggers.ShouldHaveSingleItem().ShouldBeTheSameAs(loggerConfig); 24 | loggerConfig.Level.ShouldEqual(LogLevel.Warn); 25 | 26 | config.SetLogLevel("Foo", null); 27 | loggerConfig.Level.ShouldBeNull(); 28 | } 29 | 30 | [Test] 31 | [TestCase(null)] 32 | [TestCase("")] 33 | public void should_set_root_log_level(string name) 34 | { 35 | var config = new ZeroLogConfiguration(); 36 | config.Loggers.ShouldBeEmpty(); 37 | 38 | config.SetLogLevel(name, LogLevel.Warn); 39 | 40 | config.RootLogger.Level.ShouldEqual(LogLevel.Warn); 41 | config.Loggers.ShouldBeEmpty(); 42 | } 43 | 44 | [Test] 45 | public void should_set_log_level_from_initializer() 46 | { 47 | var config = new ZeroLogConfiguration 48 | { 49 | Loggers = 50 | { 51 | { "Foo", LogLevel.Warn } 52 | } 53 | }; 54 | 55 | var logger = config.Loggers.ShouldHaveSingleItem(); 56 | logger.Name.ShouldEqual("Foo"); 57 | logger.Level.ShouldEqual(LogLevel.Warn); 58 | } 59 | 60 | [Test] 61 | public void should_create_test_config() 62 | { 63 | var config = ZeroLogConfiguration.CreateTestConfiguration(); 64 | config.AppendingStrategy.ShouldEqual(AppendingStrategy.Synchronous); // This is the most important property 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/DocumentationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Xml.Linq; 6 | using NUnit.Framework; 7 | using NUnit.Framework.Interfaces; 8 | using ZeroLog.Tests.Support; 9 | 10 | namespace ZeroLog.Tests; 11 | 12 | [TestFixture] 13 | public class DocumentationTests 14 | { 15 | private static Dictionary _members; 16 | 17 | [Test] 18 | [TestCaseSource(nameof(GetDocumentedMembers))] 19 | public void should_have_valid_documentation(XElement member) 20 | { 21 | if (member.Element("inheritdoc") != null) 22 | { 23 | member.Elements("inheritdoc").Count().ShouldEqual(1); 24 | member.Elements().Count(i => i.Name.LocalName is not "inheritdoc").ShouldEqual(0); 25 | } 26 | else 27 | { 28 | member.Elements("summary").Count().ShouldEqual(1); 29 | member.Elements("remarks").Count().ShouldBeLessThanOrEqualTo(1); 30 | } 31 | 32 | foreach (var elem in member.Elements()) 33 | { 34 | var text = Regex.Replace(elem.Value, @"^\s*Default:.*", "", RegexOptions.Multiline).Trim(); 35 | 36 | if (text.Length != 0) 37 | text.ShouldEndWith("."); 38 | } 39 | } 40 | 41 | private static Dictionary GetMembers() 42 | { 43 | if (_members != null) 44 | return _members; 45 | 46 | var xmlFilePath = Path.ChangeExtension(typeof(LogManager).Assembly.Location, ".xml"); 47 | var members = XDocument.Load(xmlFilePath).Root!.Element("members")!.Elements("member"); 48 | var membersDict = members.ToDictionary(i => i.Attribute("name")!.Value, i => i); 49 | 50 | _members ??= membersDict; 51 | return _members; 52 | } 53 | 54 | private static IEnumerable GetDocumentedMembers() 55 | => GetMembers().Select(pair => new TestCaseData(pair.Value).SetCategory("Documentation").SetName(pair.Key)); 56 | } 57 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Formatting/FormatterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Formatting; 4 | using ZeroLog.Tests.Support; 5 | 6 | namespace ZeroLog.Tests.Formatting; 7 | 8 | [TestFixture] 9 | public class FormatterTests 10 | { 11 | private TestFormatter _formatter; 12 | 13 | [SetUp] 14 | public void SetUp() 15 | { 16 | _formatter = new TestFormatter(); 17 | } 18 | 19 | [Test] 20 | public void should_append() 21 | { 22 | _formatter.Write("Foo"); 23 | _formatter.Write("Bar"); 24 | 25 | (_formatter.GetOutput() is "FooBar").ShouldBeTrue(); 26 | } 27 | 28 | [Test] 29 | public void should_append_newline() 30 | { 31 | _formatter.Write("Foo"); 32 | _formatter.WriteLine(); 33 | _formatter.Write("Bar"); 34 | 35 | _formatter.GetOutput().SequenceEqual($"Foo{Environment.NewLine}Bar").ShouldBeTrue(); 36 | } 37 | 38 | [Test] 39 | public void should_append_newline_2() 40 | { 41 | _formatter.WriteLine("Foo"); 42 | _formatter.Write("Bar"); 43 | 44 | _formatter.GetOutput().SequenceEqual($"Foo{Environment.NewLine}Bar").ShouldBeTrue(); 45 | } 46 | 47 | [Test] 48 | public void should_not_overflow() 49 | { 50 | var valueA = new string('a', TestFormatter.BufferLength); 51 | var valueB = new string('b', TestFormatter.BufferLength); 52 | 53 | _formatter.Write(valueA); 54 | _formatter.Write(valueB); 55 | 56 | _formatter.GetOutput().SequenceEqual(valueA).ShouldBeTrue(); 57 | } 58 | 59 | [Test] 60 | public void should_append_newline_when_buffer_is_full() 61 | { 62 | var value = new string('a', TestFormatter.BufferLength); 63 | 64 | _formatter.Write(value); 65 | _formatter.WriteLine(); 66 | 67 | _formatter.GetOutput().SequenceEqual(value[..^Environment.NewLine.Length] + Environment.NewLine).ShouldBeTrue(); 68 | } 69 | 70 | private class TestFormatter : Formatter 71 | { 72 | public static int BufferLength { get; } = new TestFormatter().GetRemainingBuffer().Length; 73 | 74 | protected override void WriteMessage(LoggedMessage message) 75 | { 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using NUnit.Framework; 8 | using ZeroLog.Appenders; 9 | using ZeroLog.Configuration; 10 | 11 | namespace ZeroLog.Tests; 12 | 13 | [TestFixture] 14 | [Explicit("Manual")] 15 | public class IntegrationTests 16 | { 17 | private PerformanceAppender _performanceAppender; 18 | private const int _nbThreads = 4; 19 | private const int _queueSize = 1 << 16; 20 | private const int _count = _queueSize / _nbThreads; 21 | private readonly List _enqueueMicros = new(); 22 | 23 | [SetUp] 24 | public void SetUp() 25 | { 26 | _performanceAppender = new PerformanceAppender(_count * _nbThreads); 27 | 28 | LogManager.Initialize(new ZeroLogConfiguration 29 | { 30 | LogMessagePoolSize = _queueSize, 31 | RootLogger = 32 | { 33 | Appenders = { new ConsoleAppender() } 34 | } 35 | }); 36 | 37 | for (int i = 0; i < _nbThreads; i++) 38 | { 39 | _enqueueMicros.Add(new double[_count]); 40 | } 41 | } 42 | 43 | [TearDown] 44 | public void Teardown() 45 | { 46 | LogManager.Shutdown(); 47 | } 48 | 49 | [Test] 50 | public void should_test_console() 51 | { 52 | LogManager.GetLogger(typeof(IntegrationTests)).Info().Append("Hello").Log(); 53 | LogManager.GetLogger(typeof(IntegrationTests)).Warn().Append("Hello").Log(); 54 | LogManager.GetLogger(typeof(IntegrationTests)).Fatal().Append("Hello").Log(); 55 | LogManager.GetLogger(typeof(IntegrationTests)).Error().Append("Hello").Log(); 56 | LogManager.GetLogger(typeof(IntegrationTests)).Debug().Append("Hello").Log(); 57 | } 58 | 59 | [Test] 60 | public void should_test_append() 61 | { 62 | 63 | Console.WriteLine("Starting test"); 64 | var sw = Stopwatch.StartNew(); 65 | 66 | Parallel.For(0, _nbThreads, threadId => 67 | { 68 | var logger = LogManager.GetLogger($"{nameof(IntegrationTests)}{threadId}"); 69 | for (var i = 0; i < _count; i++) 70 | { 71 | var timestamp = Stopwatch.GetTimestamp(); 72 | logger.Info().Append(timestamp).Log(); 73 | _enqueueMicros[threadId][i] = ToMicroseconds(Stopwatch.GetTimestamp() - timestamp); 74 | } 75 | }); 76 | 77 | LogManager.Shutdown(); 78 | var throughput = _count / sw.Elapsed.TotalSeconds; 79 | 80 | Console.WriteLine($"Finished test, throughput is: {throughput:N0} msgs/second"); 81 | 82 | _performanceAppender.PrintTimeTaken(); 83 | 84 | var streamWriter = new StreamWriter(new FileStream("write-times.csv", FileMode.Create)); 85 | foreach (var thread in _enqueueMicros) 86 | { 87 | foreach (var timeTaken in thread) 88 | { 89 | streamWriter.WriteLine(timeTaken); 90 | } 91 | } 92 | Console.WriteLine("Printed total time taken csv"); 93 | } 94 | 95 | private static double ToMicroseconds(long ticks) 96 | { 97 | return unchecked(ticks * 1000000 / (double)(Stopwatch.Frequency)); 98 | } 99 | 100 | [Test] 101 | public void should_not_allocate() 102 | { 103 | const int count = 1000000; 104 | 105 | GC.Collect(2, GCCollectionMode.Forced, true); 106 | var timer = Stopwatch.StartNew(); 107 | var gcCount = GC.CollectionCount(0); 108 | 109 | var logger = LogManager.GetLogger(typeof(IntegrationTests)); 110 | for (var i = 0; i < count; i++) 111 | { 112 | Thread.Sleep(1); 113 | logger.Info().Append("Hello").Log(); 114 | } 115 | 116 | LogManager.Shutdown(); 117 | var gcCountAfter = GC.CollectionCount(0); 118 | timer.Stop(); 119 | 120 | Console.WriteLine("BCL : {0} us/log", timer.ElapsedMilliseconds * 1000.0 / count); 121 | Console.WriteLine("GCs : {0}", gcCountAfter - gcCount); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/LogManagerTests.Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using NUnit.Framework; 4 | using ZeroLog.Configuration; 5 | using ZeroLog.Tests.Support; 6 | 7 | namespace ZeroLog.Tests; 8 | 9 | public partial class LogManagerTests 10 | { 11 | [Test] 12 | public void should_return_configuration() 13 | { 14 | LogManager.Configuration.ShouldBeTheSameAs(_config); 15 | 16 | LogManager.Shutdown(); 17 | LogManager.Configuration.ShouldBeNull(); 18 | } 19 | 20 | [Test] 21 | public void should_apply_appender_changes() 22 | { 23 | var fooLog = LogManager.GetLogger("Foo"); 24 | var barLog = LogManager.GetLogger("Bar"); 25 | 26 | var barAppender = new TestAppender(true); 27 | 28 | _config.Loggers.Add(new LoggerConfiguration(fooLog.Name) { IncludeParentAppenders = false }); 29 | _config.Loggers.Add(new LoggerConfiguration(barLog.Name) { Appenders = { barAppender } }); 30 | 31 | ApplyConfigChanges(); 32 | 33 | var rootSignal = _testAppender.SetMessageCountTarget(1); 34 | var barSignal = barAppender.SetMessageCountTarget(1); 35 | 36 | fooLog.Info("Foo"); 37 | barLog.Info("Bar"); 38 | 39 | rootSignal.Wait(TimeSpan.FromSeconds(1)); 40 | barSignal.Wait(TimeSpan.FromSeconds(1)); 41 | 42 | _testAppender.LoggedMessages.ShouldHaveSingleItem().ShouldEqual("Bar"); 43 | barAppender.LoggedMessages.ShouldHaveSingleItem().ShouldEqual("Bar"); 44 | } 45 | 46 | [Test] 47 | public void should_apply_log_level_changes() 48 | { 49 | var fooLog = LogManager.GetLogger("Foo"); 50 | var barLog = LogManager.GetLogger("Bar"); 51 | 52 | _config.Loggers.Add(new LoggerConfiguration(fooLog.Name) { Level = LogLevel.Warn }); 53 | ApplyConfigChanges(); 54 | 55 | fooLog.IsInfoEnabled.ShouldBeFalse(); 56 | fooLog.IsWarnEnabled.ShouldBeTrue(); 57 | 58 | barLog.IsInfoEnabled.ShouldBeTrue(); 59 | barLog.IsWarnEnabled.ShouldBeTrue(); 60 | } 61 | 62 | [Test] 63 | public void should_not_apply_changes_until_requested() 64 | { 65 | var log = LogManager.GetLogger(); 66 | 67 | _config.Loggers.Add(new LoggerConfiguration(log.Name) { Level = LogLevel.Warn }); 68 | 69 | log.IsInfoEnabled.ShouldBeTrue(); 70 | 71 | ApplyConfigChanges(); 72 | 73 | log.IsInfoEnabled.ShouldBeFalse(); 74 | } 75 | 76 | [Test] 77 | public void should_dispose_active_appenders_on_shutdown() 78 | { 79 | var loggerAppender = new TestAppender(false); 80 | 81 | _config.Loggers.Add(new LoggerConfiguration("Foo") { Appenders = { loggerAppender } }); 82 | ApplyConfigChanges(); 83 | 84 | LogManager.Shutdown(); 85 | 86 | _testAppender.IsDisposed.ShouldBeTrue(); 87 | loggerAppender.IsDisposed.ShouldBeTrue(); 88 | } 89 | 90 | [Test] 91 | public void should_dispose_removed_appenders_on_shutdown() 92 | { 93 | var loggerAppender = new TestAppender(false); 94 | 95 | _config.Loggers.Add(new LoggerConfiguration("Foo") { Appenders = { loggerAppender } }); 96 | ApplyConfigChanges(); 97 | 98 | loggerAppender.IsDisposed.ShouldBeFalse(); 99 | 100 | _config.Loggers.Single().Appenders.Clear(); 101 | ApplyConfigChanges(); 102 | 103 | loggerAppender.IsDisposed.ShouldBeFalse(); 104 | 105 | _config.Loggers.Clear(); 106 | ApplyConfigChanges(); 107 | 108 | loggerAppender.IsDisposed.ShouldBeFalse(); 109 | 110 | LogManager.Shutdown(); 111 | 112 | loggerAppender.IsDisposed.ShouldBeTrue(); 113 | } 114 | 115 | private void ApplyConfigChanges() 116 | { 117 | _config.ApplyChanges(); 118 | _logManager.WaitUntilNewConfigurationIsApplied(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/LogManagerTests.Enums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Tests.Support; 4 | 5 | namespace ZeroLog.Tests; 6 | 7 | [TestFixture] 8 | public class LogManagerEnumTests 9 | { 10 | [Test] 11 | public void should_register_all_assembly_enums() 12 | { 13 | EnumCache.IsRegistered(typeof(ConsoleColor)).ShouldBeFalse(); 14 | LogManager.RegisterAllEnumsFrom(typeof(ConsoleColor).Assembly); 15 | EnumCache.IsRegistered(typeof(ConsoleColor)).ShouldBeTrue(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/LogMessageTests.MiscTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Tests.Support; 4 | 5 | namespace ZeroLog.Tests; 6 | 7 | partial class LogMessageTests 8 | { 9 | [TestFixture] 10 | public class MiscTests : LogMessageTests 11 | { 12 | [Test] 13 | public void should_write_empty_message() 14 | => LogMessage.Empty.ToString().ShouldBeEmpty(); 15 | 16 | [Test] 17 | public void should_write_constant_message() 18 | => new LogMessage("foobar").ToString().ShouldEqual("foobar"); 19 | 20 | [Test] 21 | public void should_truncate_value_types_after_string_capacity_is_exceeded() 22 | { 23 | _logMessage = LogMessage.CreateTestMessage(LogLevel.Info, _bufferLength, 1); 24 | 25 | _logMessage.Append("foo") 26 | .Append(10) 27 | .Append("bar") 28 | .Append(20) 29 | .ToString() 30 | .ShouldEqual("foo10 [TRUNCATED]"); 31 | } 32 | 33 | [Test] 34 | public void should_assign_exception() 35 | { 36 | var ex = new InvalidOperationException(); 37 | _logMessage.WithException(ex); 38 | 39 | _logMessage.Exception.ShouldBeTheSameAs(ex); 40 | } 41 | 42 | [Test] 43 | public void should_append_indirect() 44 | { 45 | _logMessage.Append($"foo {new LogMessage.AppendOperation(40, static (msg, i) => msg.Append(i + 2))} bar") 46 | .ToString() 47 | .ShouldEqual("foo 42 bar"); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/LogMessageTests.UnmanagedTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Configuration; 4 | using ZeroLog.Tests.Support; 5 | 6 | namespace ZeroLog.Tests; 7 | 8 | partial class LogMessageTests 9 | { 10 | [TestFixture] 11 | public class UnmanagedTests : LogMessageTests 12 | { 13 | static UnmanagedTests() 14 | { 15 | LogManager.RegisterUnmanaged(); 16 | 17 | LogManager.RegisterUnmanaged((ref ForwardFormatToOutputStruct _, Span destination, out int written, ReadOnlySpan format) => 18 | { 19 | written = format.Length; 20 | return format.TryCopyTo(destination); 21 | }); 22 | } 23 | 24 | [Test] 25 | public void should_append_formattable_value() 26 | { 27 | var value = new Int64FormattableWrapper { Value = 42 }; 28 | _logMessage.AppendUnmanaged(value).ToString().ShouldEqual("42"); 29 | } 30 | 31 | [Test] 32 | public void should_append_formattable_value_ref() 33 | { 34 | var value = new Int64FormattableWrapper { Value = 42 }; 35 | _logMessage.AppendUnmanaged(ref value).ToString().ShouldEqual("42"); 36 | } 37 | 38 | [Test] 39 | public void should_append_formattable_value_nullable() 40 | { 41 | Int64FormattableWrapper? value = new Int64FormattableWrapper { Value = 42 }; 42 | _logMessage.AppendUnmanaged(value).ToString().ShouldEqual("42"); 43 | } 44 | 45 | [Test] 46 | public void should_append_formattable_value_nullable_ref() 47 | { 48 | Int64FormattableWrapper? value = new Int64FormattableWrapper { Value = 42 }; 49 | _logMessage.AppendUnmanaged(ref value).ToString().ShouldEqual("42"); 50 | } 51 | 52 | [Test] 53 | public void should_append_null_value() 54 | { 55 | _logMessage.AppendUnmanaged((Int64FormattableWrapper?)null).ToString().ShouldEqual(ZeroLogConfiguration.Default.NullDisplayString); 56 | } 57 | 58 | [Test] 59 | public void should_append_null_value_ref() 60 | { 61 | Int64FormattableWrapper? value = null; 62 | _logMessage.AppendUnmanaged(ref value).ToString().ShouldEqual(ZeroLogConfiguration.Default.NullDisplayString); 63 | } 64 | 65 | [Test] 66 | public void should_forward_format() 67 | { 68 | _logMessage.AppendUnmanaged(new ForwardFormatToOutputStruct()).ToString().ShouldEqual(""); 69 | _logMessage.AppendUnmanaged(new ForwardFormatToOutputStruct(), "foo").ToString().ShouldEqual("foo"); 70 | } 71 | 72 | public struct Int64FormattableWrapper : ISpanFormattable 73 | { 74 | public long Value; 75 | 76 | public string ToString(string format, IFormatProvider formatProvider) 77 | => throw new InvalidOperationException(); 78 | 79 | public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) 80 | => Value.TryFormat(destination, out charsWritten, format, provider); 81 | } 82 | 83 | public struct ForwardFormatToOutputStruct; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/LogMessageTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Configuration; 4 | using ZeroLog.Formatting; 5 | using ZeroLog.Tests.Support; 6 | 7 | namespace ZeroLog.Tests; 8 | 9 | public abstract partial class LogMessageTests 10 | { 11 | private const int _bufferLength = 1024; 12 | private const int _stringCapacity = 4; 13 | 14 | private LogMessage _logMessage; 15 | 16 | [SetUp] 17 | public void SetUp() 18 | { 19 | _logMessage = LogMessage.CreateTestMessage(LogLevel.Info, _bufferLength, _stringCapacity); 20 | } 21 | 22 | private void ShouldNotAllocate(Action action) 23 | { 24 | var output = new char[1024]; 25 | var keyValues = new KeyValueList(1024); 26 | 27 | GcTester.ShouldNotAllocate( 28 | () => 29 | { 30 | action.Invoke(); 31 | _logMessage.WriteTo(output, ZeroLogConfiguration.Default, LogMessage.FormatType.Formatted, keyValues); 32 | }, 33 | () => _logMessage.Initialize(null, LogLevel.Info) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using DiffEngine; 3 | using VerifyTests; 4 | 5 | namespace ZeroLog.Tests; 6 | 7 | #pragma warning disable CA2255 8 | 9 | public static class ModuleInitializer 10 | { 11 | [ModuleInitializer] 12 | public static void Initialize() 13 | { 14 | DiffRunner.Disabled = true; 15 | VerifyDiffPlex.Initialize(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/ObjectPoolTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ObjectLayoutInspector; 3 | using ZeroLog.Tests.Support; 4 | 5 | namespace ZeroLog.Tests; 6 | 7 | [TestFixture] 8 | public class ObjectPoolTests 9 | { 10 | [Test] 11 | public void should_provide_items() 12 | { 13 | var pool = new ObjectPool(3, () => new Item()); 14 | pool.Count.ShouldEqual(3); 15 | 16 | pool.TryAcquire(out var a).ShouldBeTrue(); 17 | pool.Count.ShouldEqual(2); 18 | a.ShouldNotBeNull(); 19 | 20 | pool.TryAcquire(out var b).ShouldBeTrue(); 21 | pool.Count.ShouldEqual(1); 22 | b.ShouldNotBeNull(); 23 | 24 | pool.TryAcquire(out var c).ShouldBeTrue(); 25 | pool.Count.ShouldEqual(0); 26 | c.ShouldNotBeNull(); 27 | 28 | pool.TryAcquire(out var d).ShouldBeFalse(); 29 | pool.Count.ShouldEqual(0); 30 | d.ShouldBeNull(); 31 | 32 | a.ShouldNotBeTheSameAs(b); 33 | b.ShouldNotBeTheSameAs(c); 34 | c.ShouldNotBeTheSameAs(a); 35 | } 36 | 37 | [Test] 38 | public void should_return_items() 39 | { 40 | var pool = new ObjectPool(2, () => new Item()); 41 | pool.Count.ShouldEqual(2); 42 | 43 | pool.TryAcquire(out var a).ShouldBeTrue(); 44 | pool.Count.ShouldEqual(1); 45 | a.ShouldNotBeNull(); 46 | 47 | pool.TryAcquire(out var b).ShouldBeTrue(); 48 | pool.Count.ShouldEqual(0); 49 | b.ShouldNotBeNull(); 50 | 51 | pool.TryAcquire(out _).ShouldBeFalse(); 52 | pool.Count.ShouldEqual(0); 53 | 54 | pool.Release(a); 55 | pool.Count.ShouldEqual(1); 56 | 57 | pool.TryAcquire(out var c).ShouldBeTrue(); 58 | pool.Count.ShouldEqual(0); 59 | c.ShouldBeTheSameAs(a); 60 | } 61 | 62 | [Test] 63 | public void should_not_exceed_capacity() 64 | { 65 | var pool = new ObjectPool(2, () => new Item()); 66 | pool.Count.ShouldEqual(2); 67 | 68 | pool.Release(new Item()); 69 | pool.Count.ShouldEqual(3); // Allow one cached instance to exceed the capacity 70 | 71 | pool.Release(new Item()); 72 | pool.Count.ShouldEqual(3); 73 | } 74 | 75 | [Test, Explicit] 76 | public void show_layout() 77 | { 78 | TypeLayout.PrintLayout>(false); 79 | } 80 | 81 | private class Item; 82 | } 83 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/PerformanceAppender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using ZeroLog.Appenders; 5 | using ZeroLog.Formatting; 6 | 7 | namespace ZeroLog.Tests; 8 | 9 | internal class PerformanceAppender : Appender 10 | { 11 | private readonly MessageReceived[] _messages; 12 | private int _count; 13 | 14 | public PerformanceAppender(int expectedEntries) 15 | { 16 | _messages = new MessageReceived[expectedEntries]; 17 | 18 | for (var i = 0; i < expectedEntries; i++) 19 | _messages[i] = new MessageReceived(new char[30]); 20 | } 21 | 22 | public override void WriteMessage(LoggedMessage message) 23 | { 24 | var messageSpan = message.Message; 25 | messageSpan.CopyTo(_messages[_count].StartTimestampInChars); 26 | 27 | _messages[_count].MessageLength = messageSpan.Length; 28 | _messages[_count].EndTimestamp = Stopwatch.GetTimestamp(); 29 | _count++; 30 | } 31 | 32 | private struct MessageReceived(char[] startTimestampInChars) 33 | { 34 | public readonly char[] StartTimestampInChars = startTimestampInChars; 35 | public int MessageLength = 0; 36 | public long EndTimestamp = 0; 37 | } 38 | 39 | public void PrintTimeTaken() 40 | { 41 | var totalTimeCsv = "total-time.csv"; 42 | if (File.Exists(totalTimeCsv)) 43 | File.Delete(totalTimeCsv); 44 | 45 | using var fileStream = new StreamWriter(File.OpenWrite(totalTimeCsv)); 46 | 47 | for (var i = 0; i < _count; i++) 48 | { 49 | var messageReceived = _messages[i]; 50 | var startTime = long.Parse(messageReceived.StartTimestampInChars.AsSpan(0, messageReceived.MessageLength)); 51 | fileStream.WriteLine(ToMicroseconds(messageReceived.EndTimestamp - startTime)); 52 | } 53 | } 54 | 55 | private static double ToMicroseconds(long ticks) 56 | => unchecked(ticks * 1000000 / (double)Stopwatch.Frequency); 57 | } 58 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/PerformanceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using ZeroLog.Configuration; 6 | 7 | namespace ZeroLog.Tests; 8 | 9 | [TestFixture] 10 | [Ignore("Manual")] 11 | public class PerformanceTests 12 | { 13 | private TestAppender _testAppender; 14 | 15 | [SetUp] 16 | public void SetUp() 17 | { 18 | _testAppender = new TestAppender(false); 19 | 20 | LogManager.Initialize(new ZeroLogConfiguration 21 | { 22 | LogMessagePoolSize = 16384, 23 | LogMessageBufferSize = 512, 24 | RootLogger = 25 | { 26 | LogMessagePoolExhaustionStrategy = LogMessagePoolExhaustionStrategy.WaitUntilAvailable, 27 | Appenders = { _testAppender } 28 | } 29 | }); 30 | } 31 | 32 | [TearDown] 33 | public void Teardown() 34 | { 35 | LogManager.Shutdown(); 36 | } 37 | 38 | [Test] 39 | public void should_test_console() 40 | { 41 | LogManager.GetLogger(typeof(PerformanceTests)).Info().Append("Hello ").Append(42).Append(" this is a relatively long message ").Append(12345.4332m).Log(); 42 | } 43 | 44 | [Test] 45 | public void should_run_test() 46 | { 47 | const int threadMessageCount = 1000 * 1000; 48 | const int threadCount = 5; 49 | const int totalMessageCount = threadMessageCount * threadCount; 50 | 51 | var timer = Stopwatch.StartNew(); 52 | 53 | var logger = LogManager.GetLogger(typeof(PerformanceTests)); 54 | 55 | var signal = _testAppender.SetMessageCountTarget(totalMessageCount); 56 | var utcNow = DateTime.UtcNow; 57 | 58 | Parallel.For(0, threadCount, _ => 59 | { 60 | for (var k = 0; k < threadMessageCount; k++) 61 | { 62 | logger.Info().Append("Hello ").Append(42).Append(utcNow).Append(42.56).Append(" this is a relatively long message ").Append(12345.4332m).Log(); 63 | } 64 | }); 65 | 66 | var timedOut = !signal.Wait(TimeSpan.FromSeconds(10)); 67 | 68 | timer.Stop(); 69 | if (timedOut) 70 | Assert.Fail("Timeout"); 71 | 72 | Console.WriteLine($"Total message count : {totalMessageCount:N0} messages"); 73 | Console.WriteLine($"Thread message count : {threadMessageCount:N0} messages"); 74 | Console.WriteLine($"Thread count : {threadCount} threads"); 75 | Console.WriteLine($"Elapsed time : {timer.ElapsedMilliseconds:N0} ms"); 76 | Console.WriteLine($"Message rate : {totalMessageCount / timer.Elapsed.TotalSeconds:N0} m/s"); 77 | Console.WriteLine($"Average log cost : {timer.ElapsedMilliseconds * 1000 / (double)totalMessageCount:N3} µs"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/RunnerTests.Sync.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ZeroLog.Configuration; 3 | using ZeroLog.Tests.Support; 4 | 5 | namespace ZeroLog.Tests; 6 | 7 | [TestFixture] 8 | public class SyncRunnerTests 9 | { 10 | private TestAppender _testAppender; 11 | private SyncRunner _runner; 12 | private Log _log; 13 | 14 | [SetUp] 15 | public void SetUpFixture() 16 | { 17 | _testAppender = new TestAppender(true); 18 | 19 | var config = new ZeroLogConfiguration 20 | { 21 | LogMessagePoolSize = 10, 22 | LogMessageBufferSize = 256, 23 | AppendingStrategy = AppendingStrategy.Synchronous, 24 | RootLogger = 25 | { 26 | Appenders = { _testAppender } 27 | } 28 | }; 29 | 30 | _runner = new SyncRunner(config); 31 | 32 | _log = new Log(nameof(SyncRunnerTests)); 33 | _log.UpdateConfiguration(_runner, config); 34 | } 35 | 36 | [TearDown] 37 | public void Teardown() 38 | { 39 | _runner.Dispose(); 40 | } 41 | 42 | [Test] 43 | public void should_flush_appenders_immediately() 44 | { 45 | _log.Info("Foo"); 46 | _log.Info("Bar"); 47 | _log.Info("Baz"); 48 | 49 | _testAppender.FlushCount.ShouldEqual(3); 50 | 51 | _log.Info("Foo"); 52 | _testAppender.FlushCount.ShouldEqual(4); 53 | } 54 | 55 | [Test] 56 | public void should_apply_configuration_updates() 57 | { 58 | _runner.UpdateConfiguration(new ZeroLogConfiguration 59 | { 60 | NullDisplayString = "Foo" 61 | }); 62 | 63 | _log.Info(null); 64 | _testAppender.LoggedMessages.ShouldHaveSingleItem().ShouldEqual("Foo"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/SanityChecks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using PublicApiGenerator; 7 | using VerifyNUnit; 8 | 9 | namespace ZeroLog.Tests; 10 | 11 | [TestFixture] 12 | public class SanityChecks 13 | { 14 | [Test] 15 | public Task should_export_expected_namespaces() 16 | { 17 | return Verifier.Verify( 18 | typeof(LogManager).Assembly 19 | .ExportedTypes 20 | .Select(i => i.Namespace) 21 | .OrderBy(i => i) 22 | .Distinct() 23 | ); 24 | } 25 | 26 | [Test] 27 | public Task should_export_expected_types() 28 | { 29 | return Verifier.Verify( 30 | typeof(LogManager).Assembly 31 | .ExportedTypes 32 | .Select(i => i.FullName) 33 | .OrderBy(i => i) 34 | .Distinct() 35 | ); 36 | } 37 | 38 | [Test] 39 | public Task should_have_expected_public_api() 40 | { 41 | return Verifier.Verify( 42 | typeof(LogManager).Assembly 43 | .GeneratePublicApi(new ApiGeneratorOptions 44 | { 45 | IncludeAssemblyAttributes = false, 46 | ExcludeAttributes = 47 | [ 48 | typeof(ObsoleteAttribute).FullName, 49 | #if NET7_0_OR_GREATER 50 | typeof(CompilerFeatureRequiredAttribute).FullName 51 | #endif 52 | ] 53 | }) 54 | ).UniqueForTargetFrameworkAndVersion(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/SanityChecks.should_export_expected_namespaces.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | ZeroLog, 3 | ZeroLog.Appenders, 4 | ZeroLog.Configuration, 5 | ZeroLog.Formatting 6 | ] -------------------------------------------------------------------------------- /src/ZeroLog.Tests/SanityChecks.should_export_expected_types.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | ZeroLog.Appenders.Appender, 3 | ZeroLog.Appenders.ConsoleAppender, 4 | ZeroLog.Appenders.DateAndSizeRollingFileAppender, 5 | ZeroLog.Appenders.NoopAppender, 6 | ZeroLog.Appenders.StreamAppender, 7 | ZeroLog.Appenders.TextWriterAppender, 8 | ZeroLog.Configuration.AppenderConfiguration, 9 | ZeroLog.Configuration.AppendingStrategy, 10 | ZeroLog.Configuration.ILoggerConfigurationCollection, 11 | ZeroLog.Configuration.LoggerConfiguration, 12 | ZeroLog.Configuration.LogMessagePoolExhaustionStrategy, 13 | ZeroLog.Configuration.RootLoggerConfiguration, 14 | ZeroLog.Configuration.ZeroLogConfiguration, 15 | ZeroLog.Formatting.DefaultFormatter, 16 | ZeroLog.Formatting.Formatter, 17 | ZeroLog.Formatting.KeyValueList, 18 | ZeroLog.Formatting.KeyValueList+Enumerator, 19 | ZeroLog.Formatting.LoggedKeyValue, 20 | ZeroLog.Formatting.LoggedMessage, 21 | ZeroLog.Log, 22 | ZeroLog.Log+DebugInterpolatedStringHandler, 23 | ZeroLog.Log+ErrorInterpolatedStringHandler, 24 | ZeroLog.Log+FatalInterpolatedStringHandler, 25 | ZeroLog.Log+InfoInterpolatedStringHandler, 26 | ZeroLog.Log+TraceInterpolatedStringHandler, 27 | ZeroLog.Log+WarnInterpolatedStringHandler, 28 | ZeroLog.LogLevel, 29 | ZeroLog.LogManager, 30 | ZeroLog.LogMessage, 31 | ZeroLog.LogMessage+AppendInterpolatedStringHandler, 32 | ZeroLog.LogMessage+AppendOperation`1, 33 | ZeroLog.UnmanagedFormatterDelegate`1 34 | ] -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Snippets.Init.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ZeroLog.Configuration; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace ZeroLog.Tests.IsolatedNamespace; 6 | 7 | // This initializer is not supposed to be executed, it only defines a snippet for the readme. 8 | // Do not put unit tests in this namespace. 9 | 10 | #region NUnitInitializer 11 | 12 | [SetUpFixture] 13 | public class Initializer 14 | { 15 | [OneTimeSetUp] 16 | public void SetUp() 17 | => LogManager.Initialize(ZeroLogConfiguration.CreateTestConfiguration()); 18 | 19 | [OneTimeTearDown] 20 | public void TearDown() 21 | => LogManager.Shutdown(); 22 | } 23 | 24 | #endregion 25 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Snippets.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using NUnit.Framework; 4 | using ZeroLog.Appenders; 5 | using ZeroLog.Configuration; 6 | 7 | namespace ZeroLog.Tests; 8 | 9 | [TestFixture] 10 | public class Snippets 11 | { 12 | #region GetLogger 13 | 14 | private static readonly Log _log = LogManager.GetLogger(typeof(YourClass)); 15 | 16 | #endregion 17 | 18 | [SuppressMessage("ReSharper", "UnusedMember.Local")] 19 | private static void Init() 20 | { 21 | #region Initialize 22 | 23 | LogManager.Initialize(new ZeroLogConfiguration 24 | { 25 | RootLogger = 26 | { 27 | Appenders = 28 | { 29 | new ConsoleAppender() 30 | } 31 | } 32 | }); 33 | 34 | #endregion 35 | } 36 | 37 | [Test] 38 | public void StringInterpolationApi() 39 | { 40 | #region StringInterpolationApi 41 | 42 | var date = DateTime.Today.AddDays(1); 43 | _log.Info($"Tomorrow ({date:yyyy-MM-dd}) will be in {GetNumberOfSecondsUntilTomorrow():N0} seconds."); 44 | 45 | #endregion 46 | } 47 | 48 | [Test] 49 | public void StringBuilderApi() 50 | { 51 | #region StringBuilderApi 52 | 53 | _log.Info() 54 | .Append("Tomorrow (") 55 | .Append(DateTime.Today.AddDays(1), "yyyy-MM-dd") 56 | .Append(") will be in ") 57 | .Append(GetNumberOfSecondsUntilTomorrow(), "N0") 58 | .Append(" seconds.") 59 | .Log(); 60 | 61 | #endregion 62 | } 63 | 64 | [Test] 65 | public void StructuredData() 66 | { 67 | #region StructuredData 68 | 69 | _log.Info() 70 | .Append("Tomorrow is another day.") 71 | .AppendKeyValue("NumSecondsUntilTomorrow", GetNumberOfSecondsUntilTomorrow()) 72 | .Log(); 73 | 74 | #endregion 75 | } 76 | 77 | private static int GetNumberOfSecondsUntilTomorrow() 78 | => 10; 79 | 80 | private class YourClass; 81 | } 82 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Support/AssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using JetBrains.Annotations; 4 | using NUnit.Framework; 5 | 6 | namespace ZeroLog.Tests.Support; 7 | 8 | #nullable enable 9 | 10 | #if NET6_0_OR_GREATER 11 | [System.Diagnostics.StackTraceHidden] 12 | #endif 13 | internal static class AssertExtensions 14 | { 15 | public static void ShouldEqual(this T? actual, T? expected) 16 | => Assert.That(actual, Is.EqualTo(expected)); 17 | 18 | public static void ShouldNotEqual(this T? actual, T? expected) 19 | => Assert.That(actual, Is.Not.EqualTo(expected)); 20 | 21 | public static void ShouldBeTrue(this bool actual) 22 | => Assert.That(actual, Is.True); 23 | 24 | public static void ShouldBeFalse(this bool actual) 25 | => Assert.That(actual, Is.False); 26 | 27 | [ContractAnnotation("notnull => halt")] 28 | public static void ShouldBeNull(this object? actual) 29 | => Assert.That(actual, Is.Null); 30 | 31 | [ContractAnnotation("null => halt")] 32 | public static T ShouldNotBeNull([System.Diagnostics.CodeAnalysis.NotNull] this T? actual) 33 | where T : class 34 | { 35 | Assert.That(actual, Is.Not.Null); 36 | return actual ?? throw new AssertionException("Expected non-null"); 37 | } 38 | 39 | [ContractAnnotation("null => halt")] 40 | public static T ShouldBe(this object? actual) 41 | where T : class 42 | { 43 | Assert.That(actual, Is.InstanceOf()); 44 | return (T)actual!; 45 | } 46 | 47 | public static void ShouldBeTheSameAs(this T? actual, T? expected) 48 | where T : class 49 | => Assert.That(actual, Is.SameAs(expected)); 50 | 51 | public static void ShouldNotBeTheSameAs(this T? actual, T? expected) 52 | where T : class 53 | => Assert.That(actual, Is.Not.SameAs(expected)); 54 | 55 | public static void ShouldBeEmpty(this T actual) 56 | => Assert.That(actual, Is.Empty); 57 | 58 | public static void ShouldNotBeEmpty(this T actual) 59 | => Assert.That(actual, Is.Not.Empty); 60 | 61 | public static void ShouldContain(this string actual, string expected) 62 | => Assert.That(actual, Contains.Substring(expected)); 63 | 64 | public static void ShouldNotContain(this string actual, string expected) 65 | => Assert.That(actual, Does.Not.Contain(expected)); 66 | 67 | public static void ShouldBeEquivalentTo(this IEnumerable? actual, IEnumerable expected) 68 | => Assert.That(actual, Is.EquivalentTo(expected)); 69 | 70 | public static void ShouldEndWith(this string? actual, string expected) 71 | => Assert.That(actual, Does.EndWith(expected)); 72 | 73 | public static void ShouldBeLessThanOrEqualTo(this T? actual, T expected) where T : notnull 74 | => Assert.That(actual, Is.LessThanOrEqualTo(expected)); 75 | 76 | public static T ShouldHaveSingleItem(this IEnumerable? actual) 77 | { 78 | var list = actual as ICollection ?? actual.ShouldNotBeNull().ToList(); 79 | Assert.That(list.Count, Is.EqualTo(1)); 80 | return list.Single(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Support/GcTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace ZeroLog.Tests.Support; 5 | 6 | #nullable enable 7 | 8 | internal static class GcTester 9 | { 10 | public static void ShouldNotAllocate(Action action, Action? afterWarmup = null) 11 | { 12 | // Warmup 13 | action.Invoke(); 14 | afterWarmup?.Invoke(); 15 | 16 | var bytesBefore = GC.GetAllocatedBytesForCurrentThread(); 17 | 18 | action.Invoke(); 19 | 20 | var bytesAfter = GC.GetAllocatedBytesForCurrentThread(); 21 | var allocatedBytes = bytesAfter - bytesBefore; 22 | 23 | Assert.That(allocatedBytes, Is.Zero, $"{allocatedBytes} bytes allocated"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Support/GcTesterTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace ZeroLog.Tests.Support; 4 | 5 | [TestFixture] 6 | public class GcTesterTests 7 | { 8 | [Test] 9 | public void should_detect_allocations() 10 | { 11 | Assert.Throws(() => GcTester.ShouldNotAllocate(() => _ = new object())); 12 | } 13 | 14 | [Test] 15 | public void should_not_detect_warmup_allocations() 16 | { 17 | GcTester.ShouldNotAllocate(() => { }, () => _ = new object()); 18 | } 19 | 20 | [Test] 21 | public void should_not_throw_when_there_are_no_allocations() 22 | { 23 | var callCount = 0; 24 | var afterWarmupCount = 0; 25 | 26 | GcTester.ShouldNotAllocate(() => ++callCount, () => ++afterWarmupCount); 27 | 28 | callCount.ShouldEqual(2); 29 | afterWarmupCount.ShouldEqual(1); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Support/HexUtilsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Formatting; 4 | 5 | namespace ZeroLog.Tests.Support; 6 | 7 | [TestFixture] 8 | public unsafe class HexUtilsTests 9 | { 10 | [Test] 11 | public void should_append_value_as_hex_1() 12 | { 13 | Span buffer = new char[2 * sizeof(int)]; 14 | var x = 0x1234abcd; 15 | var xPtr = (byte*)&x; 16 | HexUtils.AppendValueAsHex(xPtr, sizeof(int), buffer); 17 | var s = buffer.ToString(); 18 | var expected = BitConverter.IsLittleEndian ? "cdab3412" : "1234abcd"; 19 | Assert.That(s, Is.EqualTo(expected)); 20 | } 21 | 22 | [Test] 23 | public void should_append_value_as_hex_2() 24 | { 25 | Span buffer = new char[2 * sizeof(int)]; 26 | var x = 0x01020304; 27 | var xPtr = (byte*)&x; 28 | HexUtils.AppendValueAsHex(xPtr, sizeof(int), buffer); 29 | var s = buffer.ToString(); 30 | var expected = BitConverter.IsLittleEndian ? "04030201" : "01020304"; 31 | Assert.That(s, Is.EqualTo(expected)); 32 | } 33 | 34 | [Test] 35 | public void should_append_value_as_hex_3() 36 | { 37 | Span buffer = new char[2 * sizeof(int)]; 38 | var x = 0x10203040; 39 | var xPtr = (byte*)&x; 40 | HexUtils.AppendValueAsHex(xPtr, sizeof(int), buffer); 41 | var s = buffer.ToString(); 42 | var expected = BitConverter.IsLittleEndian ? "40302010" : "10203040"; 43 | Assert.That(s, Is.EqualTo(expected)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Support/TestTimeProvider.cs: -------------------------------------------------------------------------------- 1 | #if NET8_0_OR_GREATER 2 | 3 | using System; 4 | 5 | namespace ZeroLog.Tests.Support; 6 | 7 | internal class TestTimeProvider : TimeProvider 8 | { 9 | public static DateTime ExampleTimestamp { get; } = new(2020, 1, 2, 3, 4, 5, 6, 7); 10 | 11 | public DateTime Timestamp { get; set; } = DateTime.UtcNow; 12 | 13 | public override DateTimeOffset GetUtcNow() 14 | => Timestamp; 15 | } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Support/TypeUtilTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using NUnit.Framework; 5 | using ZeroLog.Support; 6 | 7 | namespace ZeroLog.Tests.Support; 8 | 9 | public class TypeUtilTests 10 | { 11 | [Test] 12 | public void should_round_trip_enum() 13 | { 14 | var typeHandle = TypeUtil.TypeHandle; 15 | var type = TypeUtil.GetTypeFromHandle(typeHandle); 16 | 17 | type.ShouldEqual(typeof(DayOfWeek)); 18 | } 19 | 20 | [Test] 21 | public void should_identify_unmanaged_types() 22 | { 23 | IsUnmanaged().ShouldBeTrue(); 24 | IsUnmanaged().ShouldBeTrue(); 25 | 26 | IsUnmanaged().ShouldBeFalse(); 27 | 28 | IsUnmanaged().ShouldBeTrue(); 29 | IsUnmanaged().ShouldBeTrue(); 30 | 31 | IsUnmanaged().ShouldBeTrue(); 32 | IsUnmanaged().ShouldBeTrue(); 33 | 34 | IsUnmanaged().ShouldBeTrue(); 35 | IsUnmanaged().ShouldBeTrue(); 36 | 37 | IsUnmanaged().ShouldBeFalse(); 38 | IsUnmanaged().ShouldBeFalse(); 39 | 40 | IsUnmanaged().ShouldBeFalse(); 41 | IsUnmanaged().ShouldBeFalse(); 42 | 43 | IsUnmanaged().ShouldBeTrue(); 44 | IsUnmanaged().ShouldBeTrue(); 45 | 46 | IsUnmanaged>>().ShouldBeTrue(); 47 | IsUnmanaged>?>().ShouldBeTrue(); 48 | 49 | IsUnmanaged>>().ShouldBeFalse(); 50 | IsUnmanaged>?>().ShouldBeFalse(); 51 | 52 | bool IsUnmanaged() 53 | { 54 | var expectedResult = !RuntimeHelpers.IsReferenceOrContainsReferences(); 55 | TypeUtil.GetIsUnmanagedSlow(typeof(T)).ShouldEqual(expectedResult); 56 | return expectedResult; 57 | } 58 | } 59 | 60 | [StructLayout(LayoutKind.Sequential)] 61 | private unsafe struct UnmanagedStruct 62 | { 63 | public int Field; 64 | public int* Field2; 65 | } 66 | 67 | [StructLayout(LayoutKind.Sequential)] 68 | private struct UnmanagedStructNested 69 | { 70 | public UnmanagedStruct Field; 71 | } 72 | 73 | [StructLayout(LayoutKind.Sequential)] 74 | private struct ManagedStruct 75 | { 76 | public string Field; 77 | } 78 | 79 | [StructLayout(LayoutKind.Sequential)] 80 | private struct ManagedStructNested 81 | { 82 | public ManagedStruct Field; 83 | } 84 | 85 | [StructLayout(LayoutKind.Auto)] 86 | private struct UnmanagedAutoLayoutStruct 87 | { 88 | public int Field; 89 | } 90 | 91 | [StructLayout(LayoutKind.Auto)] 92 | private struct GenericStruct 93 | { 94 | public T Field; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/TestAppender.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using ZeroLog.Appenders; 4 | using ZeroLog.Formatting; 5 | 6 | namespace ZeroLog.Tests; 7 | 8 | public class TestAppender(bool captureLoggedMessages) : Appender 9 | { 10 | private int _messageCount; 11 | private ManualResetEventSlim _signal; 12 | private int _messageCountTarget; 13 | 14 | public List LoggedMessages { get; } = new(); 15 | public int FlushCount { get; private set; } 16 | public bool IsDisposed { get; private set; } 17 | 18 | public ManualResetEventSlim WaitOnWriteEvent { get; set; } 19 | 20 | public ManualResetEventSlim SetMessageCountTarget(int expectedMessageCount) 21 | { 22 | _signal = new ManualResetEventSlim(false); 23 | _messageCount = 0; 24 | _messageCountTarget = expectedMessageCount; 25 | return _signal; 26 | } 27 | 28 | public override void WriteMessage(LoggedMessage message) 29 | { 30 | if (captureLoggedMessages) 31 | LoggedMessages.Add(message.ToString()); 32 | 33 | if (++_messageCount == _messageCountTarget) 34 | _signal.Set(); 35 | 36 | WaitOnWriteEvent?.Wait(); 37 | } 38 | 39 | public override void Flush() 40 | { 41 | base.Flush(); 42 | 43 | ++FlushCount; 44 | } 45 | 46 | public override void Dispose() 47 | { 48 | base.Dispose(); 49 | 50 | IsDisposed = true; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/TestLogMessageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Configuration; 4 | 5 | namespace ZeroLog.Tests; 6 | 7 | #nullable enable 8 | 9 | internal class TestLogMessageProvider : ILogMessageProvider 10 | { 11 | private bool _isAcquired; 12 | private bool _isSubmitted; 13 | private readonly LogMessage _message = LogMessage.CreateTestMessage(LogLevel.Info, 128, 16); 14 | 15 | public LogMessage AcquireLogMessage(LogMessagePoolExhaustionStrategy poolExhaustionStrategy) 16 | { 17 | if (_isAcquired) 18 | throw new InvalidOperationException("The message is already acquired"); 19 | 20 | _isAcquired = true; 21 | return _message; 22 | } 23 | 24 | public void Submit(LogMessage message) 25 | { 26 | if (!ReferenceEquals(message, _message)) 27 | throw new InvalidOperationException("Unexpected message submitted"); 28 | 29 | if (!_isAcquired) 30 | throw new InvalidOperationException("Message submitted multiple times"); 31 | 32 | _isAcquired = false; 33 | _isSubmitted = true; 34 | } 35 | 36 | public LogMessage GetSubmittedMessage() 37 | { 38 | if (!_isSubmitted) 39 | Assert.Fail("No message was submitted"); 40 | 41 | return _message; 42 | } 43 | 44 | public void ShouldNotBeLogged() 45 | { 46 | if (_isAcquired || _isSubmitted) 47 | Assert.Fail("A message has been logged"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/UninitializedLogManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using ZeroLog.Configuration; 4 | using ZeroLog.Tests.Support; 5 | 6 | namespace ZeroLog.Tests; 7 | 8 | [TestFixture, NonParallelizable] 9 | public class UninitializedLogManagerTests 10 | { 11 | private TestAppender _testAppender; 12 | 13 | [SetUp] 14 | public void SetUpFixture() 15 | { 16 | _testAppender = new TestAppender(true); 17 | } 18 | 19 | [TearDown] 20 | public void Teardown() 21 | { 22 | LogManager.Shutdown(); 23 | } 24 | 25 | [Test] 26 | public void should_log_without_initialize() 27 | { 28 | LogManager.GetLogger("Test").Info("Test"); 29 | } 30 | 31 | [Test] 32 | public void should_log_correctly_when_logger_is_retrieved_before_log_manager_is_initialized() 33 | { 34 | var log = LogManager.GetLogger(); 35 | 36 | LogManager.Initialize(new ZeroLogConfiguration 37 | { 38 | LogMessagePoolSize = 10, 39 | RootLogger = 40 | { 41 | Appenders = { _testAppender } 42 | } 43 | }); 44 | 45 | var signal = _testAppender.SetMessageCountTarget(1); 46 | 47 | log.Info("Lol"); 48 | 49 | signal.Wait(TimeSpan.FromSeconds(1)).ShouldBeTrue(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/Wait.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using JetBrains.Annotations; 5 | 6 | namespace ZeroLog.Tests; 7 | 8 | public static class Wait 9 | { 10 | public static void Until([InstantHandle] Func exitCondition, TimeSpan timeout) 11 | { 12 | var sw = Stopwatch.StartNew(); 13 | 14 | while (sw.Elapsed < timeout) 15 | { 16 | if (exitCondition()) 17 | return; 18 | 19 | Thread.Sleep(10); 20 | } 21 | 22 | throw new TimeoutException(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ZeroLog.Tests/ZeroLog.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0;net8.0;net7.0;net6.0 4 | true 5 | $(NoWarn);CS8002 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | TextTemplatingFileGenerator 26 | LogTests.Messages.cs 27 | 28 | 29 | 30 | 31 | 32 | True 33 | True 34 | LogTests.Messages.tt 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/ZeroLog.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abc-Arbitrage/ZeroLog/3d621607d0faa4b23d11cb27ed7722a22e39ae0b/src/ZeroLog.snk -------------------------------------------------------------------------------- /src/ZeroLog.v3.ncrunchsolution: -------------------------------------------------------------------------------- 1 |  2 | 3 | False 4 | False 5 | True 6 | True 7 | 8 | -------------------------------------------------------------------------------- /src/ZeroLog/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/ZeroLog/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Defines if sequence points should be generated for each emitted IL instruction. Default value: Debug 12 | 13 | 14 | 15 | 16 | 17 | Insert sequence points in Debug builds only (this is the default). 18 | 19 | 20 | 21 | 22 | Insert sequence points in Release builds only. 23 | 24 | 25 | 26 | 27 | Always insert sequence points. 28 | 29 | 30 | 31 | 32 | Never insert sequence points. 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Defines how warnings should be handled. Default value: Warnings 41 | 42 | 43 | 44 | 45 | 46 | Emit build warnings (this is the default). 47 | 48 | 49 | 50 | 51 | Do not emit warnings. 52 | 53 | 54 | 55 | 56 | Treat warnings as errors. 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 68 | 69 | 70 | 71 | 72 | A comma-separated list of error codes that can be safely ignored in assembly verification. 73 | 74 | 75 | 76 | 77 | 'false' to turn off automatic generation of the XML Schema file. 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/ZeroLog/Properties/AssemblyData.cs: -------------------------------------------------------------------------------- 1 | namespace ZeroLog; 2 | 3 | internal static class AssemblyData 4 | { 5 | public const string PublicKey = "0024000004800000940000000602000000240000525341310004000001000100412d6f3465c8e1cf93290cf32a96409f7af178bc04bc0623501333f0b61006ccc34d98b1e79e8c1453ef3f8b8ecef2b879c5a7fa8b4aab5848f2e3a9abf6a84260bc87112269206dcbfa1ac1309aa9a815bb7f7d6695f7c38fdaa6ce9a17a055c7f5dc93224f20607e6097fd89fc30b12326fa2fdae7015f99f0e8589bb072a2"; 6 | } 7 | -------------------------------------------------------------------------------- /src/ZeroLog/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using ZeroLog; 3 | 4 | [assembly: InternalsVisibleTo($"ZeroLog.Tests, PublicKey={AssemblyData.PublicKey}")] 5 | [assembly: InternalsVisibleTo($"ZeroLog.Tests.NetStandard, PublicKey={AssemblyData.PublicKey}")] 6 | [assembly: InternalsVisibleTo($"ZeroLog.Benchmarks, PublicKey={AssemblyData.PublicKey}")] 7 | 8 | #if NET 9 | [module: SkipLocalsInit] 10 | #endif 11 | -------------------------------------------------------------------------------- /src/ZeroLog/ZeroLog.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0;net8.0;net7.0;net6.0;netstandard2.0 4 | enable 5 | true 6 | 7 | 8 | 9 | true 10 | ZeroLog 11 | README.md 12 | icon.png 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/ZeroLog/ZeroLog.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json", 3 | "MaxWidth": 200, 4 | "Convention": "InPlaceOverwrite" 5 | } --------------------------------------------------------------------------------