├── .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 | }
--------------------------------------------------------------------------------