Changelog
This file tracks all notable changes to the OzaLog package (formerly Ozakboy.NLOG). Version numbers follow Semantic Versioning.
3.3.0 - 2026-09-14
Host-facing lifecycle control:
LOG.Flush()/LOG.Shutdown()and their async counterparts, plus log files you can actually open while the process is running. Everything here is additive — no existing signature changed, no default value changed — so upgrading from 3.2.0 needs no code changes.The reason this matters: logging is asynchronous, so until now there was no way to ask "is what I wrote actually on disk?" other than sleeping and hoping. Hosts that are about to be killed (container stop, crash handler, an app-wide
finally) had no barrier to wait on, and anything that shut the logger down had no way to stop the background worker.
Added
LOG.Flush() — a real barrier, not a sleep
LOG.Flush()/LOG.Flush(int timeoutMs)blocks until every entry written so far has reached the disk, then returnstrue. Returnsfalseon timeout (default 10 s) or when the logger is already shut down.- The calling thread helps drain the queue rather than waiting out the dispatcher's
FlushIntervalMs, so the guarantee is exact: write 1000 entries, callFlush(), and the file has 1000 lines by the time it returns — no sleep, no re-read, no retry loop. LOG.FlushAsync(CancellationToken)/LOG.FlushAsync(int timeoutMs, CancellationToken)returnTask<bool>on every target framework,netstandard2.0included. A cancelled token returnsfalserather than throwingOperationCanceledException.- Both pipelines (main logger and Quote) are flushed, and both force an
fsync— the reason a host callsFlushis that the process may be killed next, so handing the buffer to the OS is not enough.
LOG.Shutdown() — stop the background work
LOG.Shutdown()/LOG.Shutdown(int timeoutMs)/LOG.ShutdownAsync(...): flush, then stop the dispatcher, the periodic disk-flush timer and the retention-cleanup timer, then close every open log file.- Idempotent. The first call returns
true, later calls returnfalse— that is a report, not an error. It also interleaves safely with the built-inProcessExit/UnhandledExceptioncleanup in either order, so a host that callsShutdown()explicitly and then exits normally does not double-run the cleanup or trip over half-closed streams. - Writes after
Shutdownare silently discarded — no exception, not even a console line. A background-logging library 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.IsShutdownreports the current state.
Configure after Shutdown
LOG.Configure(...)remains non-reentrant while the pipeline is alive (unchanged v3.0 behavior — it exists so nobody swaps settings mid-run), but it is now allowed again afterShutdown: it restarts the pipeline and clears the discard state. That restriction only ever made sense while something was running.- The options are reset to defaults on restart, so the previous round's settings cannot linger as invisible state that nobody configured and nobody can see.
Fixed
Log files could not be opened by external tools while running
- Files were opened with
FileShare.Read, which only admits readers that open the file read-only.tail, editors and most log viewers open withFileAccess.ReadWriteand were rejected with a sharing violation — in practice, you could not watch the log while the process was running, which is most of the point of a local file logger. - Files are now opened with
FileShare.ReadWrite(both the main logger and the Quote pipeline). The writer keeps its append-only handle; this only widens what other processes are permitted to do with the file.
Technical
- Removed the
DocumentationFileproperty from the project file. It pinned all five target frameworks to a singlefile.xmlin the project directory (five builds racing to write the same file) and shipped the docs aslib/<tfm>/file.xml— a name IntelliSense ignores, since it only looks for<AssemblyName>.xml. The bilingual XML documentation was invisible to consumers all along. With onlyGenerateDocumentationFileleft, each framework generates its ownOzaLog.xmland the package now carrieslib/<tfm>/OzaLog.xml, which IntelliSense picks up. The stalefile.xmlchecked into the repository has been removed. - Both pipelines now track outstanding entries with a pending counter instead of inferring completion from an empty queue. 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
Flushthat trusted the queue would return one entry early. The counter is incremented before the enqueue for the same reason. - The dispatcher's semaphore and
CancellationTokenSourceare rebuilt on eachInitialize, so the pipeline can genuinely restart afterShutdown; theProcessExit/UnhandledExceptionhandlers are registered once per process, so a restart does not stack up duplicate cleanup handlers. FileStreamPool.FlushAllandQuoteFileStreamPool.FlushAllgained aflushToDiskoverload (the no-argument version keeps its existing behavior). The periodic timer still passesfalse— hand the buffer to the OS, throughput first;Flush/Shutdownpasstrueand force thefsync.- New internal coordinator
LogLifecycle— the single place that owns the shutdown flag and sequences both pipelines, soLOGitself stays a thin facade. - New xUnit tests:
LifecycleTests(a 1000-entry burst is exactly 1000 lines whenFlushreturns; writes afterShutdownthrow nothing and leave no trace in any file;Shutdownis idempotent and survives theProcessExitcleanup running afterwards;Configurestays non-reentrant untilShutdown; logging resumes after a restart) andFileShareTests(the file is readable and read-write openable while the writer holds it, and the writer keeps working afterwards). Cross-class test parallelization is disabled at assembly level, because shutting the pipeline down is process-global state and would otherwise cut other test classes off mid-write. 81 tests green (73 in 3.2.0). - Zero NuGet dependencies on
net8.0/net9.0/net10.0unchanged; build verified across all five target frameworks with 0 errors and 0 warnings.
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 = trueif you need to reorder entries after the fact. Not planned to change: the 1 ms cache is what keeps the caller-side cost at a few nanoseconds. - Synchronous mode (
EnableAsyncLogging = false) still has no retention cleanup —KeepDaysis enforced by a background cleaner that only the async pipeline starts. Carried over from 3.2.0.
3.2.0 - 2026-09-11
Three fixes, all of them changing what actually lands in your log files: every
Error/Fatalentry was written twice, synchronous mode (EnableAsyncLogging = false) produced empty files, and curly braces in a message were doubled, leaving serialized exceptions as unparsable JSON. No public API signature changes and no default value changes, so this is not a major release — but because the log content changes it ships as a minor rather than a patch: patches tend to be applied by automated upgrade tooling, while a minor makes people glance at these notes first.Check three things before upgrading: (1) counting errors from your logs → the old numbers were inflated; (2) using synchronous mode → your old logs are empty; (3) parsing JSON out of your logs → braces are no longer doubled, adjust your parsing.
Fixed
Error / Fatal / immediateFlush entries were written twice
AsyncLogHandler.Enqueueenqueued the item (written later by the dispatcher) and wrote it synchronously on the caller thread to guarantee it reached disk before a crash — so the same log entry appeared as two identical lines in the file. This affected everyError_Log/Fatal_Logoverload and any call at any level withimmediateFlush: true; ordinaryTrace/Debug/Info/Warn/CustomNamecalls were not affected.- Note: if you have been counting errors from your logs, every number before 3.2.0 was inflated. Not a clean 2x either — it fluctuated between 1x and 2x, because the queued copy could still be discarded by drop-oldest backpressure when the queue saturated, so the heavier the load the lower the actual duplication rate. Alert thresholds based on error rates need to be recalibrated.
- These entries now take only the synchronous caller-thread write and are no longer enqueued. The immediate-flush performance characteristic is fully preserved (still
Flush(flushToDisk: true)right after the write). Side benefit: auto-flush entries no longer pass through the queue, so drop-oldest backpressure can never discard them.
Synchronous mode (EnableAsyncLogging = false) produced 0-byte files
- The synchronous path wrote into a
StreamWriterwithAutoFlush = falseand nothing ever flushed it: the dispatcher, the 100 ms periodic disk-flush timer and theProcessExitshutdown hook are all started byAsyncLogHandler.Initialize, which synchronous mode never reaches. The log file was created and no error was raised, but the content stayed in the buffer and was lost when the process exited. - Note: if you have been using synchronous mode, your log files have been empty all along.
- Synchronous mode now flushes after every entry (at
StreamWriter/FileStreamlevel, no forcedfsync— same behavior as the periodic flush in async mode), so the content is readable right away. Writes are still guarded by theFileStreamPoollock, so the path remains thread-safe. Synchronous and asynchronous mode produce byte-identical formatting for the same entry. - Queue-full backpressure has been drop-oldest since v3.0 (not a downgrade to synchronous writing), so that path was not affected by this bug.
KeepDaysretention cleanup still does not run in synchronous mode (the cleaner is started by the async pipeline). Clean old date directories yourself for now, or use asynchronous mode; fixing it needs an extra background timer and is deferred to the next release.
Curly braces were doubled, leaving exception JSON unparsable
- Serialized exceptions landed in the file like this:
{{ "Type": "System.InvalidOperationException", "Data": {{}} }}— every brace doubled, so no parser could read it, which blocked any structured analysis of your error logs. - Cause:
LOG.Logunconditionally doubled every{and}in the message on the caller thread so thatAppendFormatwould not mistake them for format placeholders. But the no-arguments path appends the message withStringBuilder.Appendand never goes throughAppendFormat, so nothing ever undid the doubling. Exception and object overloads never carry arguments, so every single one was affected. - Note: if you have been parsing JSON out of your logs, braces are no longer doubled — adjust any parsing (or the regex / replace you wrote to work around it).
- Braces in a message now reach the file exactly as written, with or without arguments. Text mode and JSON mode (
OutputFormat = Json), synchronous and asynchronous mode all agree. - This also fixes mixing literal braces with placeholders:
LOG.Info_Log("cfg {\"retry\":3} user {0}", new[] { "alice" })used to hit aFormatExceptionand fall back to the raw message with{0}unsubstituted; it now correctly producescfg {"retry":3} user alice.
Technical
- Brace escaping moved from the caller thread into the formatter, and now happens only on the
AppendFormatpath (when arguments are present). It scans character by character: valid format items ({0},{1,-8},{2:F4}) are preserved, everything else is escaped — including an index beyond the argument range such as{5}, which used to throwFormatExceptionand is now emitted as a literal. Side benefit: the caller thread no longer pays for oneRegex.IsMatchand twostring.Replacecalls per entry, which is closer to the v3.0 "zero formatting on the caller" design. FileStreamPool.Flushgained aflushToDiskparameter (defaulttrue, existing callers unchanged); synchronous mode passesfalseto avoid anfsyncper entry.- New internal method
LogText.WriteSync(synchronous mode: write + flush);LogText.Add_LogText(v2.x compatibility entry point) now routes through it as well. - Removed the
Microsoft.SourceLink.GitHubpackage reference. SourceLink has shipped in the SDK since .NET 8;PublishRepositoryUrl+EmbedUntrackedSourcesalone produce an identical nuspec<repository ... branch=... commit=... />, and the PDBs of all five target frameworks (netstandard2.0/netstandard2.1included) carry full source-link mappings — the nuspec is byte-identical before and after removal. This also clears the NU1902 vulnerability advisory on its transitiveMicrosoft.Build.Tasks.Gitdependency. Build-time change only; consumers are unaffected. - Added bilingual XML documentation for the remaining 16 public members of
LogConfiguration. The library now builds with 0 warnings on all five target frameworks. - New xUnit tests:
BraceEscapingTests(brace behavior with and without arguments, both output formats, both write paths; exception JSON verified by actually parsing it withSystem.Text.Jsonrather than comparing strings),DuplicateWriteTests(Error / Fatal /immediateFlushwritten exactly once, other levels unchanged) andSyncModeWriteTests(sync write readable immediately, same line as the async dispatcher, no lost lines under concurrency).AutoFlushLevelTests(guards againstLogLevel.CustomName = 99being treated as auto-flush) still passes — 73 tests green. - The
OzaLog.Testsmoke program gained a third CLI argumentwrite-mode(async/sync, defaultasync) so synchronous output can be inspected directly. - Build verified across all 5 TargetFrameworks (
netstandard2.0/netstandard2.1/net8.0/net9.0/net10.0) with 0 errors and 0 warnings.
3.1.0 - 2026-05-14
Three new capabilities: customizable time/thread display, configurable output format (txt/log/json), and a dedicated Quote pipeline for high-frequency tick/quote data with Binance-aligned schema. All additions are backward compatible — defaults preserve v3.0 behavior.
Added
Customizable Time & Thread Display
LogOptions.TimeFormat(default"HH:mm:ss.fff") — free-form .NET DateTime format string for the message prefix. Falls back to default on parse failure.LogOptions.ShowThreadId(defaulttrue) andLogOptions.ShowThreadName(defaultfalse) — independently toggle thread ID / name in the prefix. WhenShowThreadName=truebut the calling thread has no name (Thread.Name == null), the entire thread segment is omitted.LogOptions.HighPrecisionTimestamp(defaultfalse) — opt-inStopwatch-hybrid mode that reconstructs µs-level precision from the 1ms cache; raises caller-side ticks read cost from ~5ns to ~30ns.
Multiple Output Formats
LogOptions.OutputFormat(defaultLogOutputFormat.Txt) — global format selector:Txt/Log(same content, different extension) /Json(NDJSON with fixed schema{ts, lv, nm, tid?, tn?, msg, data?}).- JSON timestamps emit as epoch_ms integers. Field names use short forms (
lv,nm,tid,tn) for compactness.
Quote (Tick/Ticker) Pipeline
LOG.Quote(...)andLOG.QuoteTicker(...)— public API for high-frequency quote/ticker data with field names aligned to Binance REST 24hr Ticker schema (Last,LastQty,Bid,BidQty,Ask,AskQty,Open,PrevClose,High,Low,Volume,QuoteVolume).QuoteRecord(publicreadonly struct) — A2 core API for zero-allocation enqueue. Convenience A1 overloads for the common cases (tick only / bid+ask / full ticker / ticker+extras).QuoteOptions(opt-in viaopt.ConfigureQuote(q => q.Enable = true), default off) — independent async pipeline with its own dispatcher, queue, andFileStreamPool. ConfigurableOutputFormat(Txt/Log/Json),MaxOpenStreams(default 500),MaxQueueSize(default 50000),MaxBatchSize,FlushIntervalMs,OnDropped(long)callback.QuoteRecord.Extras(IReadOnlyDictionary<string, object>) for flexible attributes andQuoteRecord.ExtrasJson(raw pre-serialized JSON string for the zero-overhead path) — mutually exclusive; setting both throwsArgumentExceptionat the call site.- File naming:
{baseDir}/{LogPath}/{yyyyMMdd}/{QuotePath}/{Bucket}_{Symbol}_Quote.{ext}— no nested subdirectories. - Symbol/bucket sanitization: file-system-invalid characters (
/ \ : * ? " < > |) are automatically replaced with-in filenames; the original symbol/bucket text is preserved in the file content.
Tests
- Four new xUnit test files covering custom time formats, NDJSON formatting, Quote schema/error scenarios, and filename sanitization (48 tests total, all passing).
OzaLog.Test/Program.csrewritten to a comprehensive v3.1 smoke-test covering every API surface and error path in a single linear run, with optional CLI args for format selection (txt/log/json).
Improved
LogItemcarriesThreadNameso the dispatcher thread can render the calling thread's name (previously unavailable post-enqueue).- All Quote API overloads funnel through
LOG.Quote(in QuoteRecord)for centralized validation. Errors (null/empty Symbol or Bucket,Extras/ExtrasJsonboth set,Extraskey colliding with a reserved field) throwArgumentExceptionsynchronously on the calling thread — not deferred to the dispatcher. LogFormatterretains a fast path for the defaultHH:mm:ss.fffformat (hand-written, zero-allocation); other formats route throughDateTime.ToStringwithFormatExceptionfallback to default.FileStreamPoolsupports per-output extension (.txt/.log/.json) with corresponding part-detection logic for size-based file splitting.
Technical
System.Text.Json: bumped from8.0.5→9.0.16fornetstandard2.0/netstandard2.1targets (net8.0/net9.0/net10.0still use the BCL built-in — zero NuGet dependencies).Microsoft.SourceLink.GitHub: bumped from8.0.0→10.0.300(build-only,PrivateAssets=all, no consumer impact).- New internal types:
JsonLogFormatter,QuoteFormatter,QuoteFileStreamPool,QuoteLogHandler. Quote pipeline runs entirely in parallel with the mainAsyncLogHandler— they share no locks or stream pools. - Build verified across all 5 TargetFrameworks (
netstandard2.0/netstandard2.1/net8.0/net9.0/net10.0) with 0 errors.
3.0.1 - 2026-05-09
Metadata + repository improvements release. No library code changes — the OzaLog assembly is byte-identical to v3.0.0 (Deterministic build).
Improved
- NuGet package metadata refreshed: cleaner
Description(highlightsLOG.Info_Log("...")API + HFT pipeline + zero dependencies + crypto tick stream use case), updatedPackageTags(addedozalog,hft,high-performance,zero-dependency; removed misleadingnlogtag), polishedTitle. PackageReleaseNotesnow uses absolute GitHub URLs for cross-references (NuGet doesn't render relative paths).
Technical
- New project website: Nuxt 4 + @nuxt/content + Tailwind CSS, deployed to GitHub Pages → https://ozakboy.github.io/OzaLog/
- Repository documentation restructured: all user-facing docs moved to
docs/{en,zh-TW}/bilingual tree (changelog.md,migration.md, plus templates forgetting-started.md,configuration.md,api.md,async-pipeline.md,benchmarks.md). - GitHub Actions auto-deploys the site on push to main.
- Sponsor page added with USDT (BEP20) wallet + Binance Pay QR.
uplogrelease flow extended: now also creates GitHub Release and pushes to NuGet.org automatically.
Notes
- For migration from v2.x see migration guide.
3.0.0 - 2026-05-09
Breaking Changes
- Package renamed:
Ozakboy.NLOG→OzaLog. The previous package on NuGet is deprecated and points here. See migration guide for the upgrade guide. - Namespace renamed:
ozakboy.LOG→OzaLog. Allusingstatements in consumer code must be updated. - Removed TargetFrameworks: dropped
.NET Framework 4.6.2,net6.0,net7.0(all EOL). Now supportsnetstandard2.0,netstandard2.1,net8.0,net9.0,net10.0. - Enum typo fixed:
LogLevel.CostomName→LogLevel.CustomName. The public methodLOG.CustomName_Log(...)was already correctly named — only the underlying enum value was renamed.
Added
- HFT-grade async pipeline:
ConcurrentQueue<struct LogItem>+ persistent FileStream pool + 1ms cached timestamp + drop-oldest backpressure. LogOptions.EnableGlobalExceptionCapture(defaultfalse) — opt-in subscription toAppDomain.UnhandledExceptionandTaskScheduler.UnobservedTaskException, auto-logs at Fatal level with synchronous immediate flush.LogOptions.MaxOpenFileStreams(default100) — LRU upper bound; exceeding closes the least recently written stream.LogOptions.DiskFlushIntervalMs(default100) — periodicFlush()interval for persistent FileStreams.LogOptions.OnDropped(defaultnull) — callback invoked when the async queue drops the oldest item under backpressure.OzaLog.Tests/— xUnit test project covering concurrency, LRU, day rollover, backpressure, GlobalExceptionCapture toggle, and format correctness.OzaLog.Benchmarks/— BenchmarkDotNet project comparing OzaLog with ZLogger, ZeroLog, and Serilog.MIGRATION.md— upgrade guide fromOzakboy.NLOGv2.x.
Improved
- Zero NuGet dependencies on
net8.0/net9.0/net10.0(System.Text.Json is built into the BCL on these targets). - Formatting work moved off the calling thread — callers only enqueue the raw tuple
(level, name, message, args, ticks, threadId); nostring.Formaton the hot path. - Persistent FileStreams eliminate per-batch open/close, reducing syscall cost to near-zero.
- Day rollover handled inline in the dispatcher (compares cached ticks date vs. stream date).
- Expired-log cleanup moved to a background timer (was on the hot path in v2.x).
Fixed
- Double-format bug in
LOG.cswhereConsole.WriteLine(formattedMessage, args)could throwFormatExceptionif the formatted message coincidentally contained{0}-style tokens. - Auto-flush level selection —
ErrorandFatalnow correctly trigger immediate flush regardless of the caller'simmediateFlushargument.
Technical
LogItemchanged from class toreadonly struct— zero GC on the hot path.- New
Core/TimestampCache.cs— background timer updatesvolatile long _currentTicksevery 1ms; callers only read. - New
Core/FileStreamPool.cs— persistent FileStreams keyed by(level, name)with LRU eviction. - New
Core/LogRetentionCleaner.cs— background expired-log cleanup, off the hot path. - New
Core/GlobalExceptionCapture.cs— opt-in global exception subscription. - Build verified across all 5 TargetFrameworks with 0 warnings / 0 errors.
2.1.0 - 2024
Added
- Added support for .NET 8.0
- Introduced async logging with configurable batch processing
- Added customizable directory structure for different log levels
- Added support for custom log types (
CustomName_Log) - Added console output support
Improved
- Enhanced file management with automatic log rotation
- Enhanced exception handling and serialization
- Improved configuration system with more options
- Better handling of file paths across operating systems
Technical
- Improved thread safety and performance
- Implemented intelligent file size management
History prior to 2.1.0 is not fully tracked here. See git history and the NuGet package page for details.