From 76e30b7cc40a6f8605494ea37fd3bcf5239ee297 Mon Sep 17 00:00:00 2001 From: Paul Bleess <8421069+pableess@users.noreply.github.com> Date: Fri, 19 Jan 2024 22:57:34 -0600 Subject: [PATCH] update benchmarks and docs --- README.md | 63 ++++++++++--------- benchmark/Benchmarks/Benchmarks.csproj | 2 +- .../JsonFileBenchmarks.Karambolo.cs | 10 +-- benchmark/Benchmarks/JsonFileBenchmarks.cs | 13 ++-- .../MultiFileBenchmarks.Karambolo.cs | 30 +++++++-- benchmark/Benchmarks/MultiFileBenchmarks.cs | 9 +-- benchmark/Benchmarks/Program.cs | 14 ++--- .../SimpleFileBenchmarks.Karambolo.cs | 16 ++++- .../Benchmarks/SimpleFileBenchmarks.NReco.cs | 15 ++++- benchmark/Benchmarks/SimpleFileBenchmarks.cs | 18 +++++- .../CompositeFileLogger.cs | 49 ++++++++------- .../SimpleFileFormatter.cs | 11 +++- 12 files changed, 157 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 234d0d2..5912377 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,21 @@ # Bleess.Extensions.Logging.File High performance rolling file logger for Microsoft.Extensions.Logging with no other 3rd party dependencies. Modeled after the standard console logger. -- Rolling files, max file size, max number or files, rolling by time period +- Rolling files, max file size, max number of files, rolling by time period - Text or Json output as well as custom formatters -- Standard idomatic configuration (similar to other MS logging providers) using IConfiguration or configuration callbacks +- Standard idiomatic configuration (similar to other MS logging providers) using IConfiguration or configuration callbacks - Abitity to update settings such as log level, filter rules, or log file path while application is running - Logging scopes and activity tracking support -- High performance using dedicated write thread and +- High performance using dedicated write thread - Ability to specify multiple log files with independent settings - -This project is very similar to nReco/logging with a few additions: multiple files, logging scopes, json output, streamlined configuration, and abiltity to modify settings while running. - ## Usage Add the nuget package Bleess.Extensions.Logging.File - The log provider is configured just like any other Microsoft.Extensions.Logging providers. There are extensions methods on the ILogBuilder to add the provider. - - When using Host.CreateDefaultBuilder you only need to call `AddFile()`, and the logger will be configured using configuration providers. There are also other overloads to configure the logger using options callbacks etc. +The log provider is configured just like any other Microsoft.Extensions.Logging providers. There are extensions methods on the ILogBuilder to add the provider. + +When using Host.CreateDefaultBuilder you only need to call `AddFile()`, and the logger will be configured using configuration providers. There are also other overloads to configure the logger using options callbacks etc. ```csharp logBuilder.AddFile(); @@ -139,7 +136,6 @@ Example configuration }, ``` - ## Rolling Behavior Log files can have a max file size at which time a new file will be create with a incremented id appended. You may also specify a maximum number of files to retain. Once the maximum number of files has been reached, the oldest will be overwritten. Using RollInterval setting, you can also specify that a date will be appended to the file name and the files will roll according to the date in 'yyyyMMddHH' format. @@ -159,16 +155,19 @@ BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3 AMD Ryzen 5 5600H with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores .NET SDK 8.0.100 [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 - Job-WGADCC : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + Job-HSNTJM : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -IterationCount=2 LaunchCount=2 WarmupCount=10 +IterationCount=10 LaunchCount=2 WarmupCount=10 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|--------------- |-----------:|---------:|----------:|-------:|-------:|----------:| -| BleessWrite | 662.4 ns | 190.9 ns | 29.54 ns | 0.1068 | - | 904 B | -| KaramboloWrite | 1,064.2 ns | 975.5 ns | 150.96 ns | 0.0839 | 0.0381 | 709 B | -| NRecoWrite | 1,585.5 ns | 139.9 ns | 21.65 ns | 0.1621 | 0.0076 | 1371 B | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|----------------------- |----------------:|--------------:|--------------:|----------:|---------:|-----------:| +| Bleess_single_write | 659.6 ns | 20.31 ns | 22.57 ns | 0.1068 | 0.0038 | 904 B | +| Karambolo_single_write | 679.7 ns | 82.32 ns | 94.80 ns | 0.0839 | - | 706 B | +| NReco_single_write | 1,547.5 ns | 13.10 ns | 14.56 ns | 0.1640 | 0.0057 | 1373 B | +| Bleess_10000_writes | 6,469,397.9 ns | 163,216.34 ns | 181,414.53 ns | 1078.1250 | - | 9040006 B | +| Karambolo_10000_write | 6,522,278.8 ns | 318,328.54 ns | 366,587.62 ns | 843.7500 | - | 7083785 B | +| NReco_10000_write | 15,962,119.6 ns | 380,106.19 ns | 406,709.37 ns | 1625.0000 | 125.0000 | 13713687 B | #### Json file @@ -178,15 +177,17 @@ BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3 AMD Ryzen 5 5600H with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores .NET SDK 8.0.100 [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 - Job-WGADCC : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + Job-HSNTJM : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -IterationCount=2 LaunchCount=2 WarmupCount=10 +IterationCount=10 LaunchCount=2 WarmupCount=10 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|--------------- |---------:|---------:|----------:|-------:|-------:|----------:| -| BleessWrite | 621.8 ns | 162.6 ns | 25.16 ns | 0.1068 | - | 904 B | -| KaramboloWrite | 985.7 ns | 721.7 ns | 111.68 ns | 0.0839 | 0.0381 | 710 B | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|---------------------------- |---------:|----------:|----------:|-------:|-------:|-------:|----------:| +| Bleess_single_write_json | 1.990 μs | 0.0469 μs | 0.0521 μs | 0.9766 | - | - | 7.99 KB | +| Karambolo_single_write_json | 2.118 μs | 0.1781 μs | 0.2051 μs | 0.3204 | 0.0458 | 0.0153 | 2.65 KB | + + #### Multi-File ``` @@ -195,19 +196,19 @@ BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3 AMD Ryzen 5 5600H with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores .NET SDK 8.0.100 [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 - Job-WGADCC : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + Job-HSNTJM : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -IterationCount=2 LaunchCount=2 WarmupCount=10 +IterationCount=10 LaunchCount=2 WarmupCount=10 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|--------------- |---------:|----------:|----------:|-------:|-------:|----------:| -| BleessWrite | 1.441 μs | 0.2313 μs | 0.0358 μs | 0.2213 | - | 1856 B | -| KaramboloWrite | 1.335 μs | 0.2949 μs | 0.0456 μs | 0.0877 | 0.0458 | 734 B | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|--------------------------------- |---------:|----------:|----------:|-------:|-------:|----------:| +| Bleess_multifile_single_write | 1.374 μs | 0.0396 μs | 0.0441 μs | 0.1984 | - | 1.67 KB | +| Karambolo_multifile_single_write | 1.554 μs | 0.1430 μs | 0.1647 μs | 0.1602 | 0.0229 | 1.33 KB | ## Credits - - Most of the code was a adapted from dotnet source code (specifically Microsoft.Extensions.Logging.Console) https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.Logging.Console - - The FileWriter was adapted from https://github.com/nreco/logging + - Some of the code was a adapted from dotnet source code (specifically Microsoft.Extensions.Logging.Console) https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.Logging.Console + - The FileWriter was originally adapted from https://github.com/nreco/logging, but has since been significantly modified. diff --git a/benchmark/Benchmarks/Benchmarks.csproj b/benchmark/Benchmarks/Benchmarks.csproj index 02d46fb..9cdf20a 100644 --- a/benchmark/Benchmarks/Benchmarks.csproj +++ b/benchmark/Benchmarks/Benchmarks.csproj @@ -10,7 +10,7 @@ - + diff --git a/benchmark/Benchmarks/JsonFileBenchmarks.Karambolo.cs b/benchmark/Benchmarks/JsonFileBenchmarks.Karambolo.cs index 6fcdf26..ce50b60 100644 --- a/benchmark/Benchmarks/JsonFileBenchmarks.Karambolo.cs +++ b/benchmark/Benchmarks/JsonFileBenchmarks.Karambolo.cs @@ -24,14 +24,15 @@ public void SetupKaramboloLogging() ServiceCollection sc = new ServiceCollection(); sc.AddLogging(logBuilder => { - logBuilder.AddFile(o => + logBuilder.AddJsonFile(o => { - o.MaxFileSize = 1024 * 1024 * 1024; // 1 MB + o.MaxFileSize = 1024 * 1024 * 1024; // 1 GB o.IncludeScopes = false; + o.MaxQueueSize = 1024; o.Files = new LogFileOptions[] { - new LogFileOptions{ Path = "logs/karambolo.txt" } + new LogFileOptions{ Path = "logs/karambolo.json" } }; }); }); @@ -43,7 +44,8 @@ public void SetupKaramboloLogging() [Benchmark] - public void KaramboloWrite() + [BenchmarkCategory("json")] + public void Karambolo_single_write_json() { _karamboloLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); } diff --git a/benchmark/Benchmarks/JsonFileBenchmarks.cs b/benchmark/Benchmarks/JsonFileBenchmarks.cs index f6e6adf..ecbba32 100644 --- a/benchmark/Benchmarks/JsonFileBenchmarks.cs +++ b/benchmark/Benchmarks/JsonFileBenchmarks.cs @@ -17,7 +17,7 @@ namespace Benchmarks { [MemoryDiagnoser()] - [SimpleJob(launchCount: 2, warmupCount: 10, iterationCount: 2)] + [SimpleJob(2, 10, 10)] public partial class JsonFileBenchmarks { @@ -36,15 +36,14 @@ public void SetupBleessLogging() ServiceCollection sc = new ServiceCollection(); sc.AddLogging(lobBuilder => { - lobBuilder.AddSimpleFile(o => + lobBuilder.AddJsonFile(o => { o.Path = "logs/Bleess.txt"; - o.MaxFileSizeInMB = 1024 * 1024; // 1 GB + o.MaxFileSizeInMB = 1024; // 1 GB o.MaxNumberFiles = 31; }, o => { - o.SingleLine = true; o.EmptyLineBetweenMessages = true; o.IncludeScopes = false; o.UseUtcTimestamp = true; @@ -55,14 +54,12 @@ public void SetupBleessLogging() _bleessLogger = sp.GetRequiredService().CreateLogger("default"); - - - } [Benchmark] - public void BleessWrite() + [BenchmarkCategory("json")] + public void Bleess_single_write_json() { _bleessLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); } diff --git a/benchmark/Benchmarks/MultiFileBenchmarks.Karambolo.cs b/benchmark/Benchmarks/MultiFileBenchmarks.Karambolo.cs index d48b839..816eb11 100644 --- a/benchmark/Benchmarks/MultiFileBenchmarks.Karambolo.cs +++ b/benchmark/Benchmarks/MultiFileBenchmarks.Karambolo.cs @@ -13,6 +13,7 @@ // alias ns using Karambolo::Microsoft.Extensions.Logging; using Karambolo::Karambolo.Extensions.Logging.File; +using Microsoft.Extensions.Options; namespace Benchmarks { @@ -26,13 +27,25 @@ public void SetupKaramboloLogging() { logBuilder.AddFile(o => { - o.MaxFileSize = 1024 * 1024 * 1024; // 1 MB + o.MaxFileSize = 1024 * 1024 * 1024; // 1 GB o.IncludeScopes = false; + o.MaxQueueSize = 1024; - o.Files = new LogFileOptions[] + o.Files = new LogFileOptions[] { - new LogFileOptions{ Path = "logs/karambolo1.txt" }, - new LogFileOptions{ Path = "logs/karambolo2.txt" }, + new LogFileOptions{ Path = "logs/karambolo1.txt" } + }; + }); + + logBuilder.AddFile(configure: o => + { + o.MaxFileSize = 1024 * 1024 * 1024; // 1 GB + o.IncludeScopes = false; + o.MaxQueueSize = 1024; + + o.Files = new LogFileOptions[] + { + new LogFileOptions{ Path = "logs/karambolo2.txt" } }; }); }); @@ -44,9 +57,16 @@ public void SetupKaramboloLogging() [Benchmark] - public void KaramboloWrite() + [BenchmarkCategory("multifile")] + public void Karambolo_multifile_single_write() { _karamboloLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); } } + + [ProviderAlias("File2")] + class AltFileLoggerProvider : FileLoggerProvider + { + public AltFileLoggerProvider(FileLoggerContext context, IOptionsMonitor options, string optionsName) : base(context, options, optionsName) { } + } } diff --git a/benchmark/Benchmarks/MultiFileBenchmarks.cs b/benchmark/Benchmarks/MultiFileBenchmarks.cs index 21ac320..4ace9e5 100644 --- a/benchmark/Benchmarks/MultiFileBenchmarks.cs +++ b/benchmark/Benchmarks/MultiFileBenchmarks.cs @@ -17,7 +17,7 @@ namespace Benchmarks { [MemoryDiagnoser()] - [SimpleJob(launchCount: 2, warmupCount: 10, iterationCount: 2)] + [SimpleJob(2, 10, 10)] public partial class MultiFileBenchmarks { @@ -71,15 +71,12 @@ public void SetupBleessLogging() var sp = sc.BuildServiceProvider(); _bleessLogger = sp.GetRequiredService().CreateLogger("default"); - - - - } [Benchmark] - public void BleessWrite() + [BenchmarkCategory("multifile")] + public void Bleess_multifile_single_write() { _bleessLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); } diff --git a/benchmark/Benchmarks/Program.cs b/benchmark/Benchmarks/Program.cs index 09d3275..829f2d1 100644 --- a/benchmark/Benchmarks/Program.cs +++ b/benchmark/Benchmarks/Program.cs @@ -6,14 +6,12 @@ internal class Program { static void Main(string[] args) { - // simple - BenchmarkRunner.Run(); - - // json - BenchmarkRunner.Run(); - - // composite - BenchmarkRunner.Run(); + BenchmarkRunner.Run( + [ + BenchmarkConverter.TypeToBenchmarks(typeof(SimpleFileBenchmarks)), + BenchmarkConverter.TypeToBenchmarks(typeof(JsonFileBenchmarks)), + BenchmarkConverter.TypeToBenchmarks(typeof(MultiFileBenchmarks)) + ]); } } } diff --git a/benchmark/Benchmarks/SimpleFileBenchmarks.Karambolo.cs b/benchmark/Benchmarks/SimpleFileBenchmarks.Karambolo.cs index 6f2f116..703428b 100644 --- a/benchmark/Benchmarks/SimpleFileBenchmarks.Karambolo.cs +++ b/benchmark/Benchmarks/SimpleFileBenchmarks.Karambolo.cs @@ -27,6 +27,9 @@ public void SetupKaramboloLogging() logBuilder.AddFile(o => { o.MaxFileSize = 1024 * 1024 * 1024; // 1 MB + + o.MaxQueueSize = 1024; + o.IncludeScopes = false; o.Files = new LogFileOptions[] @@ -43,9 +46,20 @@ public void SetupKaramboloLogging() [Benchmark] - public void KaramboloWrite() + [BenchmarkCategory("text")] + public void Karambolo_single_write() { _karamboloLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); } + + [Benchmark] + [BenchmarkCategory("text", "10000")] + public void Karambolo_10000_write() + { + for (int i = 0; i < 10000; i++) + { + _karamboloLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); + } + } } } diff --git a/benchmark/Benchmarks/SimpleFileBenchmarks.NReco.cs b/benchmark/Benchmarks/SimpleFileBenchmarks.NReco.cs index 3f98b8e..fde4fc6 100644 --- a/benchmark/Benchmarks/SimpleFileBenchmarks.NReco.cs +++ b/benchmark/Benchmarks/SimpleFileBenchmarks.NReco.cs @@ -26,7 +26,7 @@ public void SetupNRecoLogging() logBuilder.AddFile("logs/NReco.txt", o => { - o.FileSizeLimitBytes = 1024 * 1024 * 1024; // 1 MB + o.FileSizeLimitBytes = 1024 * 1024 * 1024; // 1 GB o.MaxRollingFiles = 31; o.UseUtcTimestamp = true; }); @@ -39,9 +39,20 @@ public void SetupNRecoLogging() [Benchmark] - public void NRecoWrite() + [BenchmarkCategory("text")] + public void NReco_single_write() { _nRecoLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); } + + [Benchmark] + [BenchmarkCategory("text", "10000")] + public void NReco_10000_write() + { + for (int i = 0; i < 10000; i++) + { + _nRecoLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); + } + } } } diff --git a/benchmark/Benchmarks/SimpleFileBenchmarks.cs b/benchmark/Benchmarks/SimpleFileBenchmarks.cs index 4fefaf4..1bc1746 100644 --- a/benchmark/Benchmarks/SimpleFileBenchmarks.cs +++ b/benchmark/Benchmarks/SimpleFileBenchmarks.cs @@ -17,7 +17,8 @@ namespace Benchmarks { [MemoryDiagnoser()] - [SimpleJob(launchCount: 2, warmupCount: 10, iterationCount: 2)] + [SimpleJob(2, 10, 10)] + public partial class SimpleFileBenchmarks { @@ -40,7 +41,7 @@ public void SetupBleessLogging() lobBuilder.AddSimpleFile(o => { o.Path = "logs/Bleess.txt"; - o.MaxFileSizeInMB = 1024 * 1024; // 1 GB + o.MaxFileSizeInMB = 1024; // 1 GB o.MaxNumberFiles = 31; }, o => @@ -63,9 +64,20 @@ public void SetupBleessLogging() [Benchmark] - public void BleessWrite() + [BenchmarkCategory("text")] + public void Bleess_single_write() { _bleessLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); } + + [Benchmark] + [BenchmarkCategory("text", "10000")] + public void Bleess_10000_writes() + { + for (int i = 0; i < 10000; i++) + { + _bleessLogger!.LogError("This is a test message with some parameters {a}, {b}, {c}", 100, "some string", true); + } + } } } diff --git a/src/Bleess.Extensions.Logging.File/CompositeFileLogger.cs b/src/Bleess.Extensions.Logging.File/CompositeFileLogger.cs index 5cbdcaf..85192bc 100644 --- a/src/Bleess.Extensions.Logging.File/CompositeFileLogger.cs +++ b/src/Bleess.Extensions.Logging.File/CompositeFileLogger.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; #nullable enable @@ -12,45 +14,46 @@ namespace Bleess.Extensions.Logging.File; internal sealed class CompositeFileLogger : ILogger { - private readonly ConcurrentDictionary _loggers; + // because sub providers don't change immutable dictionary will have less allocations on iteration + private volatile ImmutableDictionary _loggers; private IExternalScopeProvider? _scopeProvider; public CompositeFileLogger(string category, IEnumerable loggers, IExternalScopeProvider? scopeProvider) { Category = category; _scopeProvider = scopeProvider; - _loggers = new ConcurrentDictionary(loggers.ToDictionary(l => l.SubProviderName, l => l)) ?? throw new ArgumentNullException(nameof(loggers)); + _loggers = ImmutableDictionary.CreateRange( + loggers.ToDictionary(l => l.SubProviderName, l => l)) ?? throw new ArgumentNullException(nameof(loggers)); } + public IEnumerable SubLoggers => _loggers.Values; + /// /// Gets the category /// public string Category { get; } - /// - /// Gets the sub loggers - /// - public IEnumerable SubLoggers => _loggers.Values; - + public void Update(string provider, LogLevel? minLogLevel, Func? filter) { - if (_loggers.TryGetValue(provider, out var cur)) + var updateBuilder = ImmutableDictionary.CreateBuilder(); + + foreach (var l in _loggers) { - var newVal = new SubFileLoggerInfo(cur.Logger, cur.SubProviderName, minLogLevel, filter); - _loggers.TryUpdate(provider, newVal, cur); - } - } + if (l.Key == provider) + { + var newVal = new SubFileLoggerInfo(l.Value.Logger, l.Value.SubProviderName, minLogLevel, filter); + updateBuilder.Add(provider, newVal); + } + else + { + updateBuilder.Add(l); + } - public void Add(SubFileLoggerInfo info) - { - _loggers.TryAdd(info.SubProviderName, info); - } - public void Add(string providerKey) - { - _loggers.TryRemove(providerKey, out _); + Interlocked.Exchange(ref _loggers, updateBuilder.ToImmutable()); + } } - internal IExternalScopeProvider? ScopeProvider { get => _scopeProvider; @@ -78,11 +81,11 @@ IDisposable ILogger.BeginScope(TState state) public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - foreach (var logger in _loggers.Values) + foreach (var logger in _loggers) { - if (IsEnabled(logger, logLevel)) + if (IsEnabled(logger.Value, logLevel)) { - logger.Logger.Log(logLevel, eventId, state, exception, formatter); + logger.Value.Logger.Log(logLevel, eventId, state, exception, formatter); } } } diff --git a/src/Bleess.Extensions.Logging.File/SimpleFileFormatter.cs b/src/Bleess.Extensions.Logging.File/SimpleFileFormatter.cs index 0568ad5..597778b 100644 --- a/src/Bleess.Extensions.Logging.File/SimpleFileFormatter.cs +++ b/src/Bleess.Extensions.Logging.File/SimpleFileFormatter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text; using System.Threading; @@ -46,7 +47,15 @@ public override void Write(in LogEntry logEntry, IExternalScopeP if (timestampFormat != null) { DateTimeOffset dateTimeOffset = GetCurrentDateTime(formatterOptions); - timestamp = dateTimeOffset.ToString(timestampFormat) + " "; + + if (formatterOptions.InvariantTimestampFormat) + { + timestamp = dateTimeOffset.ToString(timestampFormat, CultureInfo.InvariantCulture) + " "; + } + else + { + timestamp = dateTimeOffset.ToString(timestampFormat) + " "; + } } if (timestamp != null) {