GE-Enforce: roll undated logs so retention can actually reach them

The prune drops a *.log by LastWriteTime. A log written to a fixed filename is
appended every cycle, so its LastWriteTime is always now, it can never be older
than any cutoff, and it grows without limit. ntlars-backup.log, eventsaver.log
and shopdb-collector-key.log all do this. On one part-marker PC eventsaver.log
had reached 11,000 lines and ntlars-backup.log 3,234, of which 3,217 were the
same "Throttled" line - the four events that mattered were unfindable without
grep -v.

Undated logs are now rolled to <name>-YYYYMMDD.log before the prune runs, which
stops them being written to and lets them age out on the existing 30 day
retention. The owning scripts need no change: they append with Add-Content or
Tee-Object, which recreate a missing file on the next write. This matters
because those three scripts are live-share artifacts, not in this repo.

Rolled under the date of its FIRST LINE, so the stamp matches the contents.
That is also the only trustworthy signal: CreationTime is not, because NTFS
file tunneling gives a recreated file the old creation time when it reappears
within 15 seconds, so a busy log keyed on that would look stale the instant it
rolled and would roll again every cycle.

A log with no parseable timestamp is left alone until it passes 5MB, so an
unrecognised format still cannot grow forever. Empty files and already-stamped
files are skipped. Rolling onto an existing target appends rather than
overwriting, so a second roll on one day loses neither side.

Verified against real files under pwsh, including the append branch and the
owner recreating the file afterwards. The first attempt keyed on CreationTime
and a second used [datetime]::TryParse with an untyped $null, which throws "no
overload" and would have made the whole roll a silent no-op; the ref is now
pre-typed.
This commit is contained in:
cproudlock
2026-08-11 09:03:28 -04:00
parent 6d5fee786c
commit b075939c38

View File

@@ -46,13 +46,76 @@ Write-EnforceLog '==============================================================
Write-EnforceLog "=== GE-Enforce session start (PID $PID, user $env:USERNAME) ==="
Write-EnforceLog '================================================================'
$logRoots = @('C:\Logs\Shopfloor', 'C:\Logs\SFLD', 'C:\Logs\Keyence')
# --- Roll undated logs so they can age ---
# The prune below drops a *.log by LastWriteTime. A log written to a FIXED
# filename is appended every cycle, so its LastWriteTime is always now and it
# can never be older than any cutoff - it grows forever. ntlars-backup.log,
# eventsaver.log and shopdb-collector-key.log all do this; eventsaver.log had
# reached 11,000 lines on a single PC.
#
# Rolling one to <name>-YYYYMMDD.log stops it being written to, so the prune
# takes it 30 days later, and the script that owns it needs no change: they all
# append with Add-Content or Tee-Object, which recreate a missing file on the
# next write. A file already carrying a date stamp is left alone.
#
# Rolled under the date of its FIRST LINE, so the stamp matches the lines
# inside. That timestamp is also the only trustworthy signal available:
# CreationTime is not, because of NTFS file tunneling - rename a file away and
# let the owner recreate it within 15 seconds, and the new file INHERITS the
# old creation time. Keyed on that, a busy log would look stale the moment it
# was rolled and would roll again every cycle, forever.
$rolledCount = 0
$today = (Get-Date).Date
foreach ($root in $logRoots) {
if (-not (Test-Path $root)) { continue }
Get-ChildItem -Path $root -Filter '*.log' -File -ErrorAction SilentlyContinue |
Where-Object { $_.BaseName -notmatch '\d{8}$' -and $_.Length -gt 0 } |
ForEach-Object {
$firstline = Get-Content -LiteralPath $_.FullName -TotalCount 1 -ErrorAction SilentlyContinue
# MUST be pre-typed. TryParse takes [ref][datetime], and passing a
# [ref] to an untyped $null throws "cannot find an overload", which
# would make this whole roll a silent no-op.
$started = [datetime]::MinValue
if ($firstline -match '(\d{4}-\d{2}-\d{2})') {
[void][datetime]::TryParse($matches[1], [ref]$started)
}
# No parseable date: fall back to size so a log with a format we do
# not recognise still cannot grow without limit.
if ($started -eq [datetime]::MinValue) {
if ($_.Length -lt 5MB) { return }
$started = $today.AddDays(-1)
}
if ($started.Date -ge $today) { return }
$stamp = $started.ToString('yyyyMMdd')
$target = Join-Path $_.DirectoryName ('{0}-{1}.log' -f $_.BaseName, $stamp)
# An existing target means the roll already ran for that day; append
# to it rather than losing either side.
try {
if (Test-Path -LiteralPath $target) {
Get-Content -LiteralPath $_.FullName -ErrorAction Stop |
Add-Content -LiteralPath $target -ErrorAction Stop
Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop
} else {
Move-Item -LiteralPath $_.FullName -Destination $target -ErrorAction Stop
}
$rolledCount++
} catch {}
}
}
if ($rolledCount -gt 0) {
Write-EnforceLog "Rolled $rolledCount undated log file(s) so retention can age them"
}
# --- Log retention prune ---
# Drops *.log files older than $retentionDays from the shopfloor log roots.
# Cheap (flat dir scan, no recursion). Runs every cycle. Today's
# enforce-YYYYMMDD.log is never touched (LastWriteTime = now).
$retentionDays = 30
$prunedCount = 0
foreach ($root in @('C:\Logs\Shopfloor', 'C:\Logs\SFLD', 'C:\Logs\Keyence')) {
foreach ($root in $logRoots) {
if (-not (Test-Path $root)) { continue }
$cutoff = (Get-Date).AddDays(-$retentionDays)
Get-ChildItem -Path $root -Filter '*.log' -File -ErrorAction SilentlyContinue |