OzaLog 3.3.0
dotnet add package OzaLog --version 3.3.0
NuGet\Install-Package OzaLog -Version 3.3.0
<PackageReference Include="OzaLog" Version="3.3.0" />
<PackageVersion Include="OzaLog" Version="3.3.0" />
<PackageReference Include="OzaLog" />
paket add OzaLog --version 3.3.0
#r "nuget: OzaLog, 3.3.0"
#:package OzaLog@3.3.0
#addin nuget:?package=OzaLog&version=3.3.0
#tool nuget:?package=OzaLog&version=3.3.0
OzaLog
Disclaimer: OzaLog is not related to NLog (jkowalski's package). This is a separate, independent library.
Migrating from
Ozakboy.NLOGv2.x? See Migration Guide. The previous package has been deprecated and renamed toOzaLog.
A lean, lightweight .NET local file logging library with the simplest possible static API. No DI, no LoggerFactory, zero NuGet dependencies on net8+. Designed for high-throughput multi-threaded scenarios such as cryptocurrency tick streams.
Why OzaLog
- Simplest possible API —
LOG.Info_Log("hello")works without any setup - Single purpose — local file logging only, no abstractions
- Zero dependencies — net8+ targets have no NuGet dependencies
- HFT-tuned hot path — persistent FileStream pool with LRU eviction, cached timestamp, struct-based queue, drop-oldest backpressure
- Faster than NLog and Serilog in HFT-style multi-thread scenarios (see Benchmarks below)
What OzaLog is NOT
- ❌ Not a
Microsoft.Extensions.Loggingprovider — does not integrate withILogger<T>DI - ❌ Not a structured logger — no
{Property}placeholder capture - ❌ Not a multi-target logger — file output only (no Console / Database / remote sinks)
- ❌ Not configurable via XML / appsettings.json — programmatic configuration only
If any of the above is a hard requirement, use NLog or Serilog instead.
Supported Frameworks
- .NET 8.0 / 9.0 / 10.0 (LTS + current)
- .NET Standard 2.0 / 2.1 (legacy compatibility)
Dropped in OzaLog v3.0: .NET Framework 4.6.2, .NET 6.0, .NET 7.0 (all EOL).
Installation
dotnet add package OzaLog
Or via Package Manager Console:
Install-Package OzaLog
Quick Start
using OzaLog;
LOG.Info_Log("Hello, World!");
LOG.Error_Log("Something went wrong");
LOG.CustomName_Log("BTC", "tick: 67890.12");
That's it — no Configure call required (defaults work). For advanced configuration:
LOG.Configure(o =>
{
o.KeepDays = -7; // keep last 7 days of logs
o.SetFileSizeInMB(50); // split files at 50 MB
o.EnableAsyncLogging = true; // default true
o.EnableConsoleOutput = true; // also write to console
o.MaxOpenFileStreams = 100; // LRU upper bound
o.DiskFlushIntervalMs = 100; // periodic flush
o.EnableGlobalExceptionCapture = false; // opt-in: auto-log unhandled exceptions
o.OnDropped = () => Interlocked.Increment(ref _dropCount);
o.ConfigureAsync(a =>
{
a.MaxBatchSize = 1000;
a.MaxQueueSize = 100_000;
a.FlushIntervalMs = 100;
});
});
Log File Layout
{AppRoot}/
└── logs/
└── 20260509/ # yyyyMMdd date folder
└── LogFiles/ # type subfolder (configurable)
├── Info_Log.txt
├── Error_Log.txt
├── BTC_Log.txt # CustomName logs go here
└── ETH_Log.txt
Use options.LogPath to change the root, and options.TypeDirectories.*Path to give each level its own folder.
Logging Methods
Every level has the same 5 overloads:
LOG.Info_Log(string message);
LOG.Info_Log(string message, bool writeTxt);
LOG.Info_Log(string message, string[] args, bool writeTxt = true, bool immediateFlush = false);
LOG.Info_Log<T>(T obj, bool writeTxt = true, bool immediateFlush = false) where T : class;
LOG.Info_Log<T>(string message, T obj, bool writeTxt = true, bool immediateFlush = false) where T : class;
Available levels: Trace_Log / Debug_Log / Info_Log / Warn_Log / Error_Log / Fatal_Log.
For custom log buckets:
LOG.CustomName_Log("BTC", "tick: 67890.12"); // → BTC_Log.txt
LOG.CustomName_Log("API", "external call"); // → API_Log.txt
Exception Logging
try { /* code */ }
catch (Exception ex)
{
LOG.Error_Log(ex); // serialized as JSON
LOG.Error_Log("operation context", ex); // with custom message
}
Exception details (Type, Message, StackTrace, InnerException, Data dictionary, additional properties) are automatically captured.
Global Exception Capture (opt-in)
LOG.Configure(o => o.EnableGlobalExceptionCapture = true);
Subscribes to AppDomain.UnhandledException and TaskScheduler.UnobservedTaskException and logs them as Fatal with synchronous flush to ensure crash logs land on disk.
Note: This does not cover WPF/WinForms UI thread exceptions or ASP.NET Core middleware exceptions — those need to be hooked separately by the application.
Flush & Shutdown (v3.3+)
Logging is asynchronous by default, so an entry you just wrote is not on disk yet. Whenever the process is about to be killed — a container stop, a finally around the whole app, a crash handler — or you are about to read the file you just wrote to, ask for a barrier:
LOG.Flush(); // blocks until everything written so far is on disk
await LOG.FlushAsync(); // same, without blocking the thread
Flush() returns true once the queue has drained and the files have been flushed to disk, false on timeout (10 s by default — pass your own with LOG.Flush(timeoutMs)). The calling thread helps drain the queue rather than waiting out the dispatcher's interval, so this is a real barrier and not a sleep: write 1000 entries, call Flush(), and the file has 1000 lines by the time it returns.
At the end of the process, shut the logger down:
LOG.Shutdown(); // flush, stop the background worker and timers, close every file
await LOG.ShutdownAsync(); // async counterpart
After Shutdown(), every logging call is silently discarded — no exception, no console output. A logger must not take its host down on the way out, and a host that is already shutting down should not have to guard every log statement. LOG.IsShutdown reports the current state.
Shutdown() is idempotent: the first call returns true, later calls return false (which is not an error), and it interleaves safely with the built-in ProcessExit cleanup in either order.
To log again in the same process, call Configure — allowed after Shutdown, and only then; while the pipeline is alive Configure stays non-reentrant. The options reset to their defaults on restart, so the previous round's settings cannot linger as invisible state:
LOG.Shutdown();
LOG.Configure(o => o.LogPath = "logs2"); // pipeline restarts, IsShutdown is false again
None of these methods throw, on any path. FlushAsync / ShutdownAsync return Task<bool> on every target framework, netstandard2.0 included, and a cancelled token returns false instead of throwing OperationCanceledException.
Known Limitations
- Entries written within the same millisecond are not guaranteed to land in file order. The timestamp cache has 1 ms resolution and the queue is drained in batches, so two entries sharing a millisecond may appear in either order. Set
HighPrecisionTimestamp = trueif you need to reorder entries after the fact. - Synchronous mode (
EnableAsyncLogging = false) has no retention cleanup.KeepDaysis enforced by a background cleaner that only the async pipeline starts. Clean old date directories yourself, or use asynchronous mode. - Log files are opened for append with
FileShare.ReadWrite(since v3.3.0), so other processes can read — and open read-write — a file while it is being written. Other writers appending to the same file at the same time are not coordinated by OzaLog.
Benchmarks
Measured against ZLogger 2.5.10, ZeroLog 2.6.1, Serilog 4.2.0 + Sinks.File 6.0.0 on .NET 10.0.7 (AMD Ryzen 9 9950X3D, BenchmarkDotNet 0.14):
S1 — Single short message
| Method | Mean | Allocated |
|---|---|---|
| OzaLog | 65.96 ns | 151 B |
| ZLogger | 219.53 ns | 278 B |
| ZeroLog | 12.19 ns | 0 B |
| Serilog | 168.87 ns | 160 B |
S3 — HFT 8 thread × 50 products × 2000 logs = 800 K writes
| Method | Mean | Allocated |
|---|---|---|
| OzaLog | 3,047 μs | 3.94 MB |
| ZLogger | 4,996 μs | 5.21 KB |
| ZeroLog | 649 μs | 3.35 KB |
| Serilog | 11,092 μs | 8.27 MB |
Verdict: OzaLog is faster than ZLogger and Serilog in both scenarios. ZeroLog wins on raw speed (it uses source generators for true zero-allocation), but OzaLog's static API is simpler.
→ Run benchmarks yourself: dotnet run -c Release --project OzaLog.Benchmarks
License
MIT License
Support
- GitHub Issues: Report Issues
- Pull Requests: Contribute Code
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Text.Json (>= 9.0.16)
-
.NETStandard 2.1
- System.Text.Json (>= 9.0.16)
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v3.3.0 — Host-facing lifecycle control. LOG.Flush() / LOG.Shutdown() and their async counterparts, plus log files you can now actually open while the process is running. Additive only: no existing signature changed, no default value changed.
ADDED:
- LOG.Flush() / LOG.Flush(int timeoutMs) — blocks until every entry written so far has reached the disk, then returns true. The calling thread helps drain the queue instead of waiting out the dispatcher's interval, so this is a real barrier, not a sleep: after Flush() returns, a 1000-entry burst is 1000 lines in the file. Returns false on timeout (default 10 s) or after Shutdown. Never throws.
- LOG.FlushAsync(CancellationToken) / LOG.FlushAsync(int timeoutMs, CancellationToken) — Task<bool>-returning versions, available on every target framework including netstandard2.0. Cancellation returns false rather than throwing OperationCanceledException.
- LOG.Shutdown() / LOG.Shutdown(int) / LOG.ShutdownAsync(...) — flushes, then stops the dispatcher, the periodic disk-flush timer and the retention-cleanup timer, and closes every open log file. Idempotent: the first call returns true, later calls return false, and that is not an error. Safe to interleave with the ProcessExit / UnhandledException cleanup in either order.
- After Shutdown, every logging call is silently discarded — no exception, no console noise. A background-logging library must not take the host down on its way out, and a host that is already shutting down should not have to guard every log statement.
- LOG.Configure(...) is still non-reentrant while the pipeline is alive, but it is now allowed again after Shutdown: it restarts the pipeline and clears the discard state. The options are reset to defaults on restart, so the previous round's settings cannot linger as invisible state.
- LOG.IsShutdown — reports whether the logger is currently shut down.
FIXED:
- Log files were opened with FileShare.Read, which only admits readers that open read-only. tail, editors and most log viewers open with FileAccess.ReadWrite and were rejected with a sharing violation — in practice, you could not watch the log while the process was running. Files are now opened with FileShare.ReadWrite. The writer keeps exclusive append semantics; this only widens what other processes may do with the file.
TECHNICAL:
- Removed the DocumentationFile property from the project file. It pinned every target framework to a single file.xml in the project directory (five frameworks racing to write the same file) and shipped the docs as lib/<tfm>/file.xml, which IntelliSense ignores — it only looks for <AssemblyName>.xml. The bilingual XML documentation was therefore invisible to consumers all along. The package now carries lib/<tfm>/OzaLog.xml and IntelliSense picks it up.
- Both pipelines track outstanding entries with a pending counter rather than inferring completion from queue emptiness. An empty queue does not mean the work is done: the dispatcher may have dequeued an item that has not reached the file yet, and a Flush that trusted the queue would return one entry early.
- The dispatcher's semaphore and cancellation token are rebuilt on each Initialize so the pipeline can restart after Shutdown; the ProcessExit / UnhandledException handlers are registered only once, so a restart does not run the same cleanup several times over.
- FileStreamPool.FlushAll and QuoteFileStreamPool.FlushAll gained a flushToDisk overload. The periodic timer still passes false (hand the buffer to the OS, performance first); Flush and Shutdown pass true and force an fsync, because the reason a host calls them is that the process may be killed next.
- New tests: LifecycleTests (Flush lands exactly 1000 of 1000 entries, writes after Shutdown throw nothing and leave no trace, Shutdown is idempotent alongside the ProcessExit cleanup, Configure stays non-reentrant until Shutdown, logging resumes after a restart) and FileShareTests (the file is readable, read-write openable, and still writable while the writer holds it). Cross-class test parallelism is disabled, since shutting the pipeline down is process-global state. 81 tests green.
KNOWN LIMITATIONS:
- Entries written within the same millisecond are not guaranteed to land in file order. The timestamp cache has 1 ms resolution and the queue is drained in batches; use HighPrecisionTimestamp if you need to order entries after the fact.
- Synchronous mode (EnableAsyncLogging = false) still has no retention cleanup: KeepDays does not take effect without the async pipeline. Clean old date directories yourself, or use asynchronous mode.
Full changelog: https://github.com/ozakboy/OzaLog/blob/main/docs/en/changelog.md
Async pipeline details: https://github.com/ozakboy/OzaLog/blob/main/docs/en/async-pipeline.md
API reference: https://github.com/ozakboy/OzaLog/blob/main/docs/en/api.md
Website: https://ozakboy.github.io/OzaLog/