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)
{