Problem
BGLoggerConfiguration sets up daily archiving of the file target like this (main, 02325dd; the file is identical in v3.0.8):
src/BAUERGROUP.Shared.Core/Logging/BGLoggerConfiguration.cs:550 TargetFile.ArchiveFileName = @"${gdc:item=LogDirectory}\${gdc:item=ApplicationName}.{#}.log";
src/BAUERGROUP.Shared.Core/Logging/BGLoggerConfiguration.cs:551 TargetFile.ArchiveSuffixFormat = "yyyy-MM-dd";
BGLoggerConfiguration.cs:552-553 ArchiveEvery = FileArchivePeriod.Day; and MaxArchiveFiles = 30;
The library uses NLog 6.1.3 (Directory.Packages.props). In NLog 6, ArchiveSuffixFormat is not a date format. It is a string.Format pattern where {0} is the archive sequence number and {1} is the archive date:
- NLog v6.1.3
src/NLog/Targets/FileTarget.cs:1119: string.Format(cultureInfo, ArchiveSuffixFormat, sequenceNumber, fileLastModifiedObj)
- NLog wiki, File-target,
archiveSuffixFormat: "Works like string.Format where {0} outputs archive-sequence-number and {1} outputs the archiveFileName-timestamp. Default: _{0:00}."
"yyyy-MM-dd" has no placeholder, so every archive gets the same literal suffix yyyy-MM-dd. NLog reports no error. At startup it formats the suffix once and would report a failure as a configuration error (FileTarget.cs:568-571, 1117-1128), but string.Format does not fail on a format without placeholders. {#} is obsolete in NLog 6, and the ArchiveFileName setter removes .{#} (FileTarget.cs:328-333). The archive path is therefore <App> + yyyy-MM-dd + .log = <App>yyyy-MM-dd.log (FileTarget.cs:1095-1110).
Because ArchiveFileName is set, NLog uses LegacyArchiveFileNameHandler (FileTarget.cs:1396-1399):
- First day change:
<App>.log is moved to <App>yyyy-MM-dd.log.
- Every later day change: the suffix has no
{0}, so NLog cannot find a sequence number and uses 0 (LegacyArchiveFileNameHandler.cs:261-264). The archive file already exists, so NLog appends the active file to it and truncates the active file (LegacyArchiveFileNameHandler.cs:185-190).
This came in with the NLog 6 migration in bbd5318 (2026-01-16, first released in v2.0.1). That commit replaced ArchiveNumbering = ArchiveNumberingMode.Date and ArchiveDateFormat = "yyyy-MM-dd" with ArchiveSuffixFormat = "yyyy-MM-dd". NLog's own translation of the obsolete ArchiveDateFormat would have been _{1:yyyy-MM-dd}_{0:00} (FileTarget.cs:255).
Observed on 2026-09-15 on Windows 11 (10.0.26200). The test was a console app that references Shared.Core 3.0.8 (NLog 6.1.3). It uses the library's configuration unchanged, except that LogDirectory points to a temp folder. It forces real day changes with an NLog TimeSource that moves forward one day per step, not with size-based archiving. Result after 4 day changes (NLog internal log, info level):
Archive moving file from '<root>\Logging\LogArchiveProbe.log' to '<root>\Logging\LogArchiveProbeyyyy-MM-dd.log'
Archive appending to already existing file: <root>\Logging\LogArchiveProbeyyyy-MM-dd.log
Cleanup file archive Truncate Active File. Delete file: '<root>\Logging\LogArchiveProbe.log'
Archive appending to already existing file: <root>\Logging\LogArchiveProbeyyyy-MM-dd.log
Cleanup file archive Truncate Active File. Delete file: '<root>\Logging\LogArchiveProbe.log'
Archive appending to already existing file: <root>\Logging\LogArchiveProbeyyyy-MM-dd.log
Cleanup file archive Truncate Active File. Delete file: '<root>\Logging\LogArchiveProbe.log'
LogArchiveProbe.log 362 bytes 3 lines days=[4]
LogArchiveProbeyyyy-MM-dd.log 1442 bytes 12 lines days=[0,1,2,3]
The result on Ubuntu 26.04 (WSL, native Linux file system) is the same. The only difference there is the backslash in the file names from #134.
Retention does not work. The library sets only MaxArchiveFiles = 30. MaxArchiveDays is not set, and its default of 0 turns it off. Cleanup counts the files that match <App>*.log (LegacyArchiveFileNameHandler.cs:92-96, wildcard from BaseFileArchiveHandler.cs:378-431). There is only ever one archive file, so the limit is never reached (BaseFileArchiveHandler.cs:84-86). Nothing is deleted, and the file grows without limit, stored as UTF-16 (BGLoggerConfiguration.cs:554). Test with MaxArchiveFiles = 3 set at runtime and 6 day changes: no file was deleted, and LogArchiveProbeyyyy-MM-dd.log held days 0-5 (18 lines). MaxArchiveDays would not help either. It checks a file's creation time (BaseFileArchiveHandler.cs:326-341), so after N days it would delete the whole combined file, including yesterday's log. That last point comes from reading the source and was not tested.
Proposal
Put the date placeholder into the suffix and remove the obsolete {#}:
TargetFile.ArchiveFileName = "${gdc:item=LogDirectory}${dir-separator}${gdc:item=ApplicationName}.log"; // ${dir-separator}: see #134
TargetFile.ArchiveSuffixFormat = "_{1:yyyy-MM-dd}_{0:00}";
Archives are then named <App>_2026-09-14_00.log. The date is the last write time of the archived file (LegacyArchiveFileNameHandler.cs:165-167), which is the day its lines belong to. From the source, _01 should appear only if the same day is archived twice; that case was not tested. The active file stays <App>.log, so the log window and tail tools keep working as before.
Verified with the same console app. Changing only ArchiveSuffixFormat at runtime, on Windows and on Linux:
Archive moving file from '<root>\Logging\LogArchiveProbe.log' to '<root>\Logging\LogArchiveProbe_2026-09-15_00.log'
Archive moving file from '<root>\Logging\LogArchiveProbe.log' to '<root>\Logging\LogArchiveProbe_2026-09-16_00.log'
Archive moving file from '<root>\Logging\LogArchiveProbe.log' to '<root>\Logging\LogArchiveProbe_2026-09-17_00.log'
Archive moving file from '<root>\Logging\LogArchiveProbe.log' to '<root>\Logging\LogArchiveProbe_2026-09-18_00.log'
With MaxArchiveFiles = 3 and 6 day changes, retention works:
Cleanup file archive MaxArchiveFiles=3. Delete file: '<root>\Logging\LogArchiveProbe_2026-09-15_00.log'
Cleanup file archive MaxArchiveFiles=3. Delete file: '<root>\Logging\LogArchiveProbe_2026-09-16_00.log'
Cleanup file archive MaxArchiveFiles=3. Delete file: '<root>\Logging\LogArchiveProbe_2026-09-17_00.log'
LogArchiveProbe.log 3 lines days=[6]
LogArchiveProbe_2026-09-18_00.log 3 lines days=[3]
LogArchiveProbe_2026-09-19_00.log 3 lines days=[4]
LogArchiveProbe_2026-09-20_00.log 3 lines days=[5]
The exact two lines above, including ${dir-separator}, gave the same archive names and the same deletions on Windows and on Ubuntu 26.04 (WSL). On Linux the archives land in a real Logging directory. The active file keeps the backslash until FileName is fixed as well (#134).
Existing installations: the old <App>yyyy-MM-dd.log also matches the cleanup wildcard. It is the oldest file, so it is deleted once the limit is reached. This was verified with MaxArchiveFiles = 3: after two new archives, the old file was deleted right before the third new archive was written. With the default of 30, it stays for about a month. It can be large, so it is worth a line in the release notes.
Alternatives:
.{1:yyyy-MM-dd} restores the names from before v2.0.1 (<App>.2026-09-14.log, verified). Without {0}, a second archive for the same date is appended to that file (LegacyArchiveFileNameHandler.cs:185-190). That is harmless with daily archiving only, but it breaks if ArchiveAboveSize is added later.
- The handler NLog 6 prefers: no
ArchiveFileName and _{0:00} (FileTarget.cs:307-309, 1396-1397). It avoids moving files, but the active file changes name instead. After 3 day changes, <App>.log still held day 0 and the current log was <App>_03.log (verified). That breaks the rule that <App>.log is the current log, and the names carry no date.
- Leaving out
ArchiveFileName and keeping _{1:yyyy-MM-dd}_{0:00} gives the same files, because NLog falls back to FileName when the suffix contains {1 (FileTarget.cs:314, verified). This behaviour is less obvious, so setting it explicitly is clearer.
Regression test (in tests/BAUERGROUP.Shared.Test, xunit + FluentAssertions):
-
Quick test: the FILE target's suffix must include the date and must differ for two different days. Both checks were run in the console app: as shipped, both days give yyyy-MM-dd (both checks false). With the fix, _2026-09-14_00 and _2026-09-15_00 (both true):
_ = BGLogger.Configuration;
var target = LogManager.Configuration!.FindTargetByName<FileTarget>("FILE")!;
string Suffix(DateTime day) => string.Format(CultureInfo.InvariantCulture, target.ArchiveSuffixFormat, 0, day);
Suffix(new DateTime(2026, 9, 14)).Should().Contain("2026-09-14");
Suffix(new DateTime(2026, 9, 15)).Should().NotBe(Suffix(new DateTime(2026, 9, 14)));
-
Optional end-to-end test: set LogDirectory to a temp folder and use an NLog TimeSource that moves forward one day per step. FromSystemTime must map file timestamps to the day that was active when they were written. Set MaxArchiveFiles = 2, then run three day changes. Expect exactly two archive files with different names, and no file name that contains yyyy. TimeSource.Current is global, so restore it afterwards and keep the test out of parallel test collections. The test takes a few seconds, because short real sleeps separate the fake days.
Related
Context
bgIndustrialAutomation Client (bauer-group/OT-AutomationClient, Shared.Core 3.0.8) uses the library's file target unchanged. It does not set LogDirectory or any archive option, and only reads the target's Layout (LogWindow.axaml.cs:88). Windows installations of the client therefore run this configuration. Their log folder under %ProgramData% should hold one <App>yyyy-MM-dd.log that keeps growing, instead of up to 30 day files. This is derived from the test above; no production station was checked. This was found while checking Linux logging (#134). Until this is fixed, an application can set ArchiveSuffixFormat on the FILE target after BGLogger.Configuration has been created, then call LogManager.ReconfigExistingLoggers(). The test above did exactly that.
Problem
BGLoggerConfigurationsets up daily archiving of the file target like this (main, 02325dd; the file is identical in v3.0.8):src/BAUERGROUP.Shared.Core/Logging/BGLoggerConfiguration.cs:550TargetFile.ArchiveFileName = @"${gdc:item=LogDirectory}\${gdc:item=ApplicationName}.{#}.log";src/BAUERGROUP.Shared.Core/Logging/BGLoggerConfiguration.cs:551TargetFile.ArchiveSuffixFormat = "yyyy-MM-dd";BGLoggerConfiguration.cs:552-553ArchiveEvery = FileArchivePeriod.Day;andMaxArchiveFiles = 30;The library uses NLog 6.1.3 (
Directory.Packages.props). In NLog 6,ArchiveSuffixFormatis not a date format. It is astring.Formatpattern where{0}is the archive sequence number and{1}is the archive date:src/NLog/Targets/FileTarget.cs:1119:string.Format(cultureInfo, ArchiveSuffixFormat, sequenceNumber, fileLastModifiedObj)archiveSuffixFormat: "Works likestring.Formatwhere{0}outputs archive-sequence-number and{1}outputs thearchiveFileName-timestamp. Default:_{0:00}.""yyyy-MM-dd"has no placeholder, so every archive gets the same literal suffixyyyy-MM-dd. NLog reports no error. At startup it formats the suffix once and would report a failure as a configuration error (FileTarget.cs:568-571,1117-1128), butstring.Formatdoes not fail on a format without placeholders.{#}is obsolete in NLog 6, and theArchiveFileNamesetter removes.{#}(FileTarget.cs:328-333). The archive path is therefore<App>+yyyy-MM-dd+.log=<App>yyyy-MM-dd.log(FileTarget.cs:1095-1110).Because
ArchiveFileNameis set, NLog usesLegacyArchiveFileNameHandler(FileTarget.cs:1396-1399):<App>.logis moved to<App>yyyy-MM-dd.log.{0}, so NLog cannot find a sequence number and uses 0 (LegacyArchiveFileNameHandler.cs:261-264). The archive file already exists, so NLog appends the active file to it and truncates the active file (LegacyArchiveFileNameHandler.cs:185-190).This came in with the NLog 6 migration in bbd5318 (2026-01-16, first released in v2.0.1). That commit replaced
ArchiveNumbering = ArchiveNumberingMode.DateandArchiveDateFormat = "yyyy-MM-dd"withArchiveSuffixFormat = "yyyy-MM-dd". NLog's own translation of the obsoleteArchiveDateFormatwould have been_{1:yyyy-MM-dd}_{0:00}(FileTarget.cs:255).Observed on 2026-09-15 on Windows 11 (10.0.26200). The test was a console app that references Shared.Core 3.0.8 (NLog 6.1.3). It uses the library's configuration unchanged, except that
LogDirectorypoints to a temp folder. It forces real day changes with an NLogTimeSourcethat moves forward one day per step, not with size-based archiving. Result after 4 day changes (NLog internal log, info level):The result on Ubuntu 26.04 (WSL, native Linux file system) is the same. The only difference there is the backslash in the file names from #134.
Retention does not work. The library sets only
MaxArchiveFiles = 30.MaxArchiveDaysis not set, and its default of 0 turns it off. Cleanup counts the files that match<App>*.log(LegacyArchiveFileNameHandler.cs:92-96, wildcard fromBaseFileArchiveHandler.cs:378-431). There is only ever one archive file, so the limit is never reached (BaseFileArchiveHandler.cs:84-86). Nothing is deleted, and the file grows without limit, stored as UTF-16 (BGLoggerConfiguration.cs:554). Test withMaxArchiveFiles = 3set at runtime and 6 day changes: no file was deleted, andLogArchiveProbeyyyy-MM-dd.logheld days 0-5 (18 lines).MaxArchiveDayswould not help either. It checks a file's creation time (BaseFileArchiveHandler.cs:326-341), so after N days it would delete the whole combined file, including yesterday's log. That last point comes from reading the source and was not tested.Proposal
Put the date placeholder into the suffix and remove the obsolete
{#}:Archives are then named
<App>_2026-09-14_00.log. The date is the last write time of the archived file (LegacyArchiveFileNameHandler.cs:165-167), which is the day its lines belong to. From the source,_01should appear only if the same day is archived twice; that case was not tested. The active file stays<App>.log, so the log window and tail tools keep working as before.Verified with the same console app. Changing only
ArchiveSuffixFormatat runtime, on Windows and on Linux:With
MaxArchiveFiles = 3and 6 day changes, retention works:The exact two lines above, including
${dir-separator}, gave the same archive names and the same deletions on Windows and on Ubuntu 26.04 (WSL). On Linux the archives land in a realLoggingdirectory. The active file keeps the backslash untilFileNameis fixed as well (#134).Existing installations: the old
<App>yyyy-MM-dd.logalso matches the cleanup wildcard. It is the oldest file, so it is deleted once the limit is reached. This was verified withMaxArchiveFiles = 3: after two new archives, the old file was deleted right before the third new archive was written. With the default of 30, it stays for about a month. It can be large, so it is worth a line in the release notes.Alternatives:
.{1:yyyy-MM-dd}restores the names from before v2.0.1 (<App>.2026-09-14.log, verified). Without{0}, a second archive for the same date is appended to that file (LegacyArchiveFileNameHandler.cs:185-190). That is harmless with daily archiving only, but it breaks ifArchiveAboveSizeis added later.ArchiveFileNameand_{0:00}(FileTarget.cs:307-309,1396-1397). It avoids moving files, but the active file changes name instead. After 3 day changes,<App>.logstill held day 0 and the current log was<App>_03.log(verified). That breaks the rule that<App>.logis the current log, and the names carry no date.ArchiveFileNameand keeping_{1:yyyy-MM-dd}_{0:00}gives the same files, because NLog falls back toFileNamewhen the suffix contains{1(FileTarget.cs:314, verified). This behaviour is less obvious, so setting it explicitly is clearer.Regression test (in
tests/BAUERGROUP.Shared.Test, xunit + FluentAssertions):Quick test: the
FILEtarget's suffix must include the date and must differ for two different days. Both checks were run in the console app: as shipped, both days giveyyyy-MM-dd(both checks false). With the fix,_2026-09-14_00and_2026-09-15_00(both true):Optional end-to-end test: set
LogDirectoryto a temp folder and use an NLogTimeSourcethat moves forward one day per step.FromSystemTimemust map file timestamps to the day that was active when they were written. SetMaxArchiveFiles = 2, then run three day changes. Expect exactly two archive files with different names, and no file name that containsyyyy.TimeSource.Currentis global, so restore it afterwards and keep the test out of parallel test collections. The test takes a few seconds, because short real sleeps separate the fake days.Related
ArchiveFileName(line 550) also needs${dir-separator}, so both fixes fit in one change toBGLoggerConfiguration.cs:547-551. The proposal in BGLogger file target uses a hard-coded backslash, so on Linux/macOS the log is written as a file named "Logging\<App>.log" #134 still keeps.{#}inArchiveFileName. NLog removes it anyway, and this issue drops it.Context
bgIndustrialAutomation Client (bauer-group/OT-AutomationClient, Shared.Core 3.0.8) uses the library's file target unchanged. It does not set
LogDirectoryor any archive option, and only reads the target'sLayout(LogWindow.axaml.cs:88). Windows installations of the client therefore run this configuration. Their log folder under%ProgramData%should hold one<App>yyyy-MM-dd.logthat keeps growing, instead of up to 30 day files. This is derived from the test above; no production station was checked. This was found while checking Linux logging (#134). Until this is fixed, an application can setArchiveSuffixFormaton theFILEtarget afterBGLogger.Configurationhas been created, then callLogManager.ReconfigExistingLoggers(). The test above did exactly that.