Stop paying for filtered log messages; sample per-event hot-path logs

SecureLogger's public wrappers evaluated their message autoclosure before
the level check inside log(), so every filtered debug message across the
codebase still paid for string interpolation - hundreds of call sites on
the per-packet/per-event hot paths. The wrappers now guard the level
first, and debug() compiles out of release builds entirely. Regression
tests verify filtered messages are never constructed.

The two heaviest per-event debug logs (NostrRelayManager inbound events,
ChatNostrCoordinator geo events) are now sampled every 100th with a
running count, so debug-enabled dev builds stop emitting hundreds of
lines per minute in busy geohashes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-06-10 20:55:25 +02:00
co-authored by Claude Fable 5
parent 3caf2d7663
commit 4b287f7490
5 changed files with 83 additions and 7 deletions
@@ -101,7 +101,8 @@ public final class SecureLogger {
// MARK: - Global Threshold
/// Minimum level that will be logged. Defaults to .info. Override via env BITCHAT_LOG_LEVEL.
private static let minimumLevel: LogLevel = {
/// Internal-settable so tests can verify level filtering; app code should not mutate it.
internal static var minimumLevel: LogLevel = {
let env = ProcessInfo.processInfo.environment["BITCHAT_LOG_LEVEL"]?.lowercased()
switch env {
case "debug": return .debug
@@ -121,23 +122,33 @@ public final class SecureLogger {
public extension SecureLogger {
// Each wrapper checks the level BEFORE evaluating the autoclosure so
// filtered messages never pay for string interpolation this matters on
// hot paths that log per packet/event. Debug compiles out of release
// builds entirely (the core log() drops .debug there anyway).
static func debug(_ message: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) {
#if DEBUG
guard shouldLog(.debug) else { return }
log(message(), category: category, level: .debug, file: file, line: line, function: function)
#endif
}
static func info(_ message: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) {
guard shouldLog(.info) else { return }
log(message(), category: category, level: .info, file: file, line: line, function: function)
}
static func warning(_ message: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) {
guard shouldLog(.warning) else { return }
log(message(), category: category, level: .warning, file: file, line: line, function: function)
}
static func error(_ message: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) {
guard shouldLog(.error) else { return }
log(message(), category: category, level: .error, file: file, line: line, function: function)
}