Review 1.2: read only the default trace's current file between rollovers

With ALTER TRACE the error log collector read every default trace file
(up to 5 x 20 MB) every 5 minutes. While the current file is the one read
last time, everything since then is in it, so only that file is read;
after a rollover (and the first time) all files are read once as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Søren Eggert Lundsteen Olsen 2026-10-02 11:38:34 +02:00
parent 8e551bcaa6
commit d9e3b4ee6d
2 changed files with 16 additions and 6 deletions

View file

@ -69,13 +69,14 @@ public sealed class ErrorLogCollector(int loginFailureBurst = 20) : Collector<Er
List<ErrorLogEntry> entries; List<ErrorLogEntry> entries;
ErrorLogSource source; ErrorLogSource source;
string? coverage = null; string? coverage = null;
string? traceFile = null;
if (errorLog && await ReadErrorLogAsync(connection, since, now, offset, ct).ConfigureAwait(false) is { } fromLog) if (errorLog && await ReadErrorLogAsync(connection, since, now, offset, ct).ConfigureAwait(false) is { } fromLog)
{ {
(entries, source) = (fromLog, ErrorLogSource.ErrorLog); (entries, source) = (fromLog, ErrorLogSource.ErrorLog);
} }
else if (await ReadDefaultTraceAsync(connection, since, offset, ct).ConfigureAwait(false) is { } fromTrace) else if (await ReadDefaultTraceAsync(connection, since, offset, previous?.TraceFile, ct).ConfigureAwait(false) is { } fromTrace)
{ {
(entries, source, coverage) = (fromTrace, ErrorLogSource.DefaultTrace, DefaultTraceCoverage); (entries, source, coverage, traceFile) = (fromTrace.Entries, ErrorLogSource.DefaultTrace, DefaultTraceCoverage, fromTrace.File);
} }
else else
{ {
@ -103,6 +104,7 @@ public sealed class ErrorLogCollector(int loginFailureBurst = 20) : Collector<Er
Groups = groups, Groups = groups,
Recent = ErrorLogAnalysis.Recent(previous?.Recent ?? [], groups, context.Now), Recent = ErrorLogAnalysis.Recent(previous?.Recent ?? [], groups, context.Now),
Coverage = coverage, Coverage = coverage,
TraceFile = traceFile,
LoginFailureBurst = loginFailureBurst, LoginFailureBurst = loginFailureBurst,
}; };
} }
@ -126,8 +128,8 @@ public sealed class ErrorLogCollector(int loginFailureBurst = 20) : Collector<Er
} }
/// <summary>ErrorLog (22) and Audit Login Failed (20) events from the default trace, or null without ALTER TRACE.</summary> /// <summary>ErrorLog (22) and Audit Login Failed (20) events from the default trace, or null without ALTER TRACE.</summary>
private static async Task<List<ErrorLogEntry>?> ReadDefaultTraceAsync(DbConnection connection, DateTimeOffset since, TimeSpan offset, private static async Task<(List<ErrorLogEntry> Entries, string File)?> ReadDefaultTraceAsync(DbConnection connection, DateTimeOffset since, TimeSpan offset,
CancellationToken cancellationToken) string? previousFile, CancellationToken cancellationToken)
{ {
try try
{ {
@ -138,9 +140,14 @@ public sealed class ErrorLogCollector(int loginFailureBurst = 20) : Collector<Er
return null; return null;
} }
// All rollover files (up to 100 MB) every 5 minutes is not cheap. While the current file is the one read last
// time, everything since then is in it; after a rollover (and the first time) all files are read once.
var files = string.Equals(path, previousFile, StringComparison.OrdinalIgnoreCase)
? Db.Literal(path) + ", 1"
: Db.Literal(PrivilegeAnalysis.TraceFileBase(path)) + ", DEFAULT";
var sql = $""" var sql = $"""
SELECT StartTime, EventClass, Error, Severity, CAST(TextData AS nvarchar(2000)) SELECT StartTime, EventClass, Error, Severity, CAST(TextData AS nvarchar(2000))
FROM sys.fn_trace_gettable({Db.Literal(PrivilegeAnalysis.TraceFileBase(path))}, DEFAULT) FROM sys.fn_trace_gettable({files})
WHERE EventClass IN (20, 22) AND StartTime > '{Local(since, offset)}' WHERE EventClass IN (20, 22) AND StartTime > '{Local(since, offset)}'
"""; """;
var rows = await Db.QueryAsync(connection, sql, r => (At: Utc(r.DateTime(0)) - offset, Class: r.Int(1), Error: r.NullableInt(2), Severity: r.NullableInt(3), var rows = await Db.QueryAsync(connection, sql, r => (At: Utc(r.DateTime(0)) - offset, Class: r.Int(1), Error: r.NullableInt(2), Severity: r.NullableInt(3),
@ -164,7 +171,7 @@ public sealed class ErrorLogCollector(int loginFailureBurst = 20) : Collector<Er
// StartTime is compared to whole seconds above, so the entries of that second that were read last time // StartTime is compared to whole seconds above, so the entries of that second that were read last time
// (the newest of them is ReadUntil) come back; leave them out, or they are counted again every collection. // (the newest of them is ReadUntil) come back; leave them out, or they are counted again every collection.
return [.. entries.Where(e => e.At > since)]; return ([.. entries.Where(e => e.At > since)], path);
} }
catch (DbException) catch (DbException)
{ {

View file

@ -54,6 +54,9 @@ public sealed record ErrorLogSnapshot
/// <summary>The newest entry read; the next collection continues after it.</summary> /// <summary>The newest entry read; the next collection continues after it.</summary>
public required DateTimeOffset ReadUntil { get; init; } public required DateTimeOffset ReadUntil { get; init; }
/// <summary>The default trace's current file when it was read; while it stays the same, only that file is read next time.</summary>
public string? TraceFile { get; init; }
/// <summary>What was found since the previous collection, grouped.</summary> /// <summary>What was found since the previous collection, grouped.</summary>
public IReadOnlyList<ErrorLogGroup> Groups { get; init; } = []; public IReadOnlyList<ErrorLogGroup> Groups { get; init; } = [];