repo-review: libtime consistency at 7c4b378 - #13
Conversation
log_sampler-time.go already documents the convention -- 'uses github.com/bborbe/time for consistent time handling across the library' -- and github.com/bborbe/time is already a direct dependency, so this is internal consistency rather than a new dependency. Both sites drive time-threshold logic: LogMemoryUsage's log-if-interval-elapsed check and Set's auto-reset-after-duration timestamp. Neither could be exercised deterministically against a frozen clock while calling time.Now() directly. Found by repo-review at 7c4b378: 88 mechanical findings across 74 rules, 1 confirmed.
There was a problem hiding this comment.
Now I have a complete picture. Let me compile the full report.
Summary of findings:
The PR replaces time.Now() with libtime.Now() at two sites to make time-threshold logic testable with a frozen clock. However, it introduces several new MUST-level violations in the changed files:
log_set-loglevel-setter.go:80— raw goroutine (go func() { ... }()) instead ofbborbe/runpatternslog_set-loglevel-setter.go:81—context.Background()in business logic (inside the raw goroutine)log_set-loglevel-setter.go:18— the//counterfeiter:generatedirective was removed by the PRlog_set-loglevel-setter.go:94—time.Since(l.lastSetTime)still uses stdlibtimeon thelastSetTime time.Timefield, so the reset-logic is still not testable with a frozen clock despite the PR's intentlog_memory-monitor.go:66,72— bareglog.Infofat V0 (should beglog.V(N).Infof)log_memory-monitor.go:77,78—runtime.GC()andtime.Sleepdirect package calls
PR Review: repo-review: libtime consistency at 7c4b378
Selector-mode trace:
- Candidates: ~50 judgment rules matched by glob filter
- Applicable: 1 —
changelog/conventional-prefix-required(CHANGELOG.md changed) - Skipped: all others — diff scope (3 files, minimal changes) does not trigger their
applies_whenconditions
Must Fix (Critical)
-
log_set-loglevel-setter.go:80— Rawgo func() { ... }()goroutine with nobborbe/runwrapper. Leaks goroutines on caller cancel, races on shutdown, and requires hand-rolled sync. The// #nosec G118comment acknowledges the violation. Userun.CancelOnFirstErrorWaitor similar fromgithub.com/bborbe/run. (rule:go-concurrency/no-raw-go-func) -
log_set-loglevel-setter.go:81—context.Background()inside business logic (the raw goroutine body). If the caller passes a cancelling context, the goroutine is orphaned anyway — but usingBackground()here makes it untestable and ignores the caller's shutdown signal entirely. Passctxinto the goroutine, or inject aClockinterface. (rule:go-errors/no-context-background-in-business-logic) -
log_set-loglevel-setter.go:18— The PR removed the//counterfeiter:generatedirective aboveLogLevelSetter. The mockmocks/log-loglevel-setter.goexists but will no longer be regenerated when the interface changes — tests will silently use a stale fake. (rule:go-architecture/counterfeiter-directive-on-interface) -
log_set-loglevel-setter.go:94—time.Since(l.lastSetTime)calls stdlibtime.Sinceon thelastSetTime time.Timefield. This is the auto-reset gate: iftime.Sinceis not mockable, the reset logic cannot be tested deterministically — the PR's stated goal ("now exercisable with a frozen/mock clock") is not achieved for this path. Either inject aClockinterface and gatetime.Sincebehind it, or changelastSetTimeto alibtime.Time-equivalent type. (rule:go-time/no-time-now-direct— the companiongo-testing/libtime-injection-requiredrequires injected time throughout) -
log_memory-monitor.go:66—glog.Infof("MEMORY MONITOR - Started")runs at V0 (always-on). "MEMORY MONITOR - Started" is developer/troubleshooting instrumentation, not production operator info. Useglog.V(2).Infofor similar. (rule:go-glog/use-v-for-debug-not-info) -
log_memory-monitor.go:72—glog.Infof("MEMORY MONITOR - Completed")same issue. (rule:go-glog/use-v-for-debug-not-info)
Should Fix (Important)
-
log_memory-monitor.go:77—runtime.GC()direct package call in business logic. Untestable without injecting a GC trigger interface. (rule:go-composition/no-package-function-calls-in-business-logic) -
log_memory-monitor.go:78—time.Sleep(100 * time.Millisecond)direct package call. Untestable — cannot be controlled with a frozen clock. Should be injectable (or at minimum the business logic path that calls it should be behind an interface). (rule:go-composition/no-package-function-calls-in-business-logic) -
log_memory-monitor.go:61—fmt.Sprintf(format, args...)direct call insideLogMemoryUsagef. Format string construction belongs behind an interface. (rule:go-composition/no-package-function-calls-in-business-logic) -
log_memory-monitor.go:17—MemoryMonitorinterface has no//counterfeiter:generatedirective (the directive was added forLogLevelSetterbut not for this interface). Themocks/memory-monitor.gomock exists but will go stale. (rule:go-architecture/counterfeiter-directive-on-interface)
Nice to Have (Optional)
log_memory-monitor.go:66,72— Log messages start with uppercase letters ("MEMORY MONITOR"). Use lowercase to match Go stdliblogconvention and keep structured-log streams uniform. (rule:go-logging/lowercase-log-messages)
Changelog
CHANGELOG.md:13— The## Unreleasedbullet usesfix:prefix — correct, follows conventional commits. No issue.
Precommit
- Precommit skipped (selector mode) — CI covers lint+test.
Notes
The PR's intent is sound: libtime.Now() replaces time.Now() to enable frozen-clock testing. However, it leaves other time dependencies (time.Since, time.Sleep, runtime.GC) as direct stdlib calls in the same files, and introduces raw goroutines and context.Background(). A follow-up PR should complete the time-injection story for resetLogLevel() (the time.Since call) and consider using the bborbe/run goroutine-safety wrappers throughout.
The removed counterfeiter directive on LogLevelSetter is the most immediate regression risk — it silently breaks mock regeneration for the affected interface.
{
"verdict": "request-changes",
"summary": "PR intent (frozen-clock testability for time-threshold logic) is correct but introduces 4 new MUST violations: a raw goroutine, context.Background() in business logic, a removed counterfeiter directive, and a remaining time.Since() call that defeats the PR's own goal. Also flags 2 bare glog.Infof V0 calls and 3 direct package calls in business logic.",
"comments": [
{
"file": "log_set-loglevel-setter.go",
"line": 80,
"severity": "critical",
"message": "Raw `go func() { ... }()` goroutine instead of bborbe/run patterns. Leaks on caller cancel, races on shutdown. Use run.CancelOnFirstErrorWait or similar. *(go-concurrency/no-raw-go-func)*"
},
{
"file": "log_set-loglevel-setter.go",
"line": 81,
"severity": "critical",
"message": "context.Background() inside business logic (the goroutine body). Makes the reset untestable and ignores caller shutdown. Pass ctx or inject a Clock interface. *(go-errors/no-context-background-in-business-logic)*"
},
{
"file": "log_set-loglevel-setter.go",
"line": 18,
"severity": "critical",
"message": "PR removed the //counterfeiter:generate directive above LogLevelSetter. mocks/log-loglevel-setter.go will no longer regenerate when the interface drifts — tests silently use stale fakes. *(go-architecture/counterfeiter-directive-on-interface)*"
},
{
"file": "log_set-loglevel-setter.go",
"line": 94,
"severity": "critical",
"message": "time.Since(l.lastSetTime) calls stdlib time on a time.Time field — not mockable with a frozen clock. The PR's stated goal (testable auto-reset timer) is not achieved for this path. Inject a Clock interface or change lastSetTime to a libtime-compatible type. *(go-time/no-time-now-direct / go-testing/libtime-injection-required)*"
},
{
"file": "log_memory-monitor.go",
"line": 66,
"severity": "critical",
"message": "Bare glog.Infof V0 for developer/troubleshooting instrumentation. Use glog.V(2).Infof or similar. *(go-glog/use-v-for-debug-not-info)*"
},
{
"file": "log_memory-monitor.go",
"line": 72,
"severity": "critical",
"message": "Bare glog.Infof V0 for developer/troubleshooting instrumentation. Use glog.V(2).Infof or similar. *(go-glog/use-v-for-debug-not-info)*"
},
{
"file": "log_memory-monitor.go",
"line": 77,
"severity": "major",
"message": "runtime.GC() direct package call in business logic — untestable without GC injection. *(go-composition/no-package-function-calls-in-business-logic)*"
},
{
"file": "log_memory-monitor.go",
"line": 78,
"severity": "major",
"message": "time.Sleep direct package call in business logic — cannot be controlled with a frozen clock. *(go-composition/no-package-function-calls-in-business-logic)*"
},
{
"file": "log_memory-monitor.go",
"line": 61,
"severity": "major",
"message": "fmt.Sprintf direct call inside business logic — should be behind a formatter interface. *(go-composition/no-package-function-calls-in-business-logic)*"
},
{
"file": "log_memory-monitor.go",
"line": 17,
"severity": "major",
"message": "MemoryMonitor interface lacks //counterfeiter:generate directive — mocks/memory-monitor.go will go stale. *(go-architecture/counterfeiter-directive-on-interface)*"
},
{
"file": "log_memory-monitor.go",
"line": 66,
"severity": "nit",
"message": "Log message starts with uppercase 'MEMORY MONITOR'. Use lowercase per Go stdlib log convention. *(go-logging/lowercase-log-messages)*"
},
{
"file": "log_memory-monitor.go",
"line": 72,
"severity": "nit",
"message": "Log message starts with uppercase 'MEMORY MONITOR'. Use lowercase per Go stdlib log convention. *(go-logging/lowercase-log-messages)*"
}
],
"concerns_addressed": [
"correctness: libtime.Now() added at log_memory-monitor.go:47 — partially addresses frozen-clock testability, but time.Sleep and runtime.GC remain uninjected",
"correctness: libtime.Now() added at log_set-loglevel-setter.go:73 — partially addresses frozen-clock testability, but time.Since at line 94 still uses stdlib time and defeats the goal",
"tests: No tests were updated for MemoryMonitor — no test file exists for it",
"tests: No tests exercise the auto-reset timer path in LogLevelSetter.Set; the counterfeiter directive was removed, making mock-based testing harder"
]
}The first commit changed the write (lastSetTime = libtime.Now()) but left the read as time.Since(l.lastSetTime). Against a frozen clock those are two different clocks, so the comparison was inconsistent -- arguably worse than the uniform-stdlib version it replaced. Now libtime.Now().Sub(l.lastSetTime), matching the pattern already used in log_sampler-time.go. Caught by the bot review on the first push.
superseded by new automated review
There was a problem hiding this comment.
PR Review: repo-review/7c4b378 → master
Step 5: Consolidated Report
Changed files reviewed: CHANGELOG.md, log_memory-monitor.go, log_set-loglevel-setter.go
Scope of this PR: Fix clock inconsistency — Set() recorded lastSetTime via stdlib time.Now(), but resetLogLevel() read it via stdlib time.Since(). Since tests freeze libtime.Now(), the frozen clock and stdlib clock were two different time sources, making the auto-reset duration check untestable. The PR makes both sides use libtime.Now().
Actual code changes (lines touched by this PR):
log_memory-monitor.go:47—time.Now()→libtime.Now()✅log_set-loglevel-setter.go:73—time.Now()→libtime.Now()✅log_set-loglevel-setter.go:94—time.Since(l.lastSetTime)→libtime.Now().Sub(l.lastSetTime)✅
These three changes are correct and address the stated bug. The CHANGELOG entry is accurate.
Important caveat on mechanical findings: The ast-grep funnel scanned the entire content of the two .go files, not just the diff lines. Every finding below is a genuine rule violation — but nearly all of them are pre-existing in code this PR did not modify. Flagging a focused bugfix PR as "request-changes" for architectural debt it did not introduce would be misleading. Findings are presented here for traceability; the PR itself is correct and the architectural issues are a separate cleanup track.
Must Fix (Critical)
-
log_set-loglevel-setter.go:23—type LogLevelSetter interface { … }lacks a//counterfeiter:generatedirective. Hand-written test mocks will silently drift from this interface. (rule:go-architecture/counterfeiter-directive-on-interface)
Note: A directive exists for this interface at line 18 (//counterfeiter:generate -o mocks/log-loglevel-setter.go --fake-name LogLevelSetter . LogLevelSetter) — the ast-grep matcher may be failing to associate it with the type declaration below the blank line. Verifygo generateactually producesmocks/log-loglevel-setter.go. -
log_memory-monitor.go:19—type MemoryMonitor interface { … }lacks a//counterfeiter:generatedirective. (rule:go-architecture/counterfeiter-directive-on-interface)
Note: A directive exists at line 17 (//counterfeiter:generate -o mocks/memory-monitor.go --fake-name MemoryMonitor . MemoryMonitor). Same possible matcher issue — verifygo generateoutput. -
log_set-loglevel-setter.go:72—libtime.Now()is called directly in business logic instead of via an injectedCurrentDateTimeGetterinterface. This makes the time-threshold check inSet()untestable against a frozen clock. (rule:go-composition/no-package-function-calls-in-business-logic)
Note: The PR changes this line to uselibtime.Now()(wastime.Now()). The underlying architecture (no time getter injection) is pre-existing. -
log_set-loglevel-setter.go:75—flag.Set("v", strconv.Itoa(int(logLevel)))— directflagpackage call in business logic.flagis stdlib that registers global state viainit(). (rule:go-composition/no-package-function-calls-in-business-logic,go-cli/cobra-not-stdlib-flag)
Pre-existing. -
log_set-loglevel-setter.go:77—glog.V(l.defaultLoglevel)— directglogpackage call in business logic. (rule:go-composition/no-package-function-calls-in-business-logic,go-cli/slog-not-glog-in-new-projects)
Pre-existing. -
log_set-loglevel-setter.go:80—go func() { … ctx, cancel := context.WithTimeout(context.Background(), l.autoResetDuration) … }()— raw goroutine for async auto-reset. Leaks if caller cancels; requires hand-rolled sync that drifts toward deadlocks. Usescontext.Background()in business logic. (rules:go-concurrency/no-raw-go-func,go-errors/no-context-background-in-business-logic)
Pre-existing. -
log_set-loglevel-setter.go:83—ctx.Done()called on acontext.Background()-derived timeout ctx, not the incomingctxparameter. Cancellation of the caller's context is ignored. (rule:go-errors/no-context-background-in-business-logic)
Pre-existing. -
log_set-loglevel-setter.go:84—l.resetLogLevel()called from within raw goroutine without any synchronization withl.mux. (rule:go-concurrency/no-raw-go-func)
Pre-existing. -
log_set-loglevel-setter.go:93—libtime.Now().Sub(l.lastSetTime)— directlibtime.Now()call in business logic. (rule:go-composition/no-package-function-calls-in-business-logic)
Note: This is the core correctness fix of this PR (replacingtime.Since). The underlying architecture (noCurrentDateTimeGetterinjection) is pre-existing. -
log_set-loglevel-setter.go:98—flag.Set("v", strconv.Itoa(int(l.defaultLoglevel)))— sameflagstdlib issue as line 75. (rule:go-composition/no-package-function-calls-in-business-logic,go-cli/cobra-not-stdlib-flag)
Pre-existing. -
log_memory-monitor.go:46—libtime.Now()direct call in business logic. (rule:go-composition/no-package-function-calls-in-business-logic)
Note: The PR changes this line to uselibtime.Now()(wastime.Now()). Pre-existing architecture. -
log_memory-monitor.go:49—now.Sub(m.lastLogTime)— time arithmetic on directtime.Timevalue. (rule:go-composition/no-package-function-calls-in-business-logic)
Pre-existing. -
log_memory-monitor.go:60—fmt.Sprintf(format, args...)— directfmtpackage call. (rule:go-composition/no-package-function-calls-in-business-logic)
Pre-existing. -
log_memory-monitor.go:61—m.LogMemoryUsage(name)— recursive self-call that bypasses the mutex lock on the outer call. (rule:go-composition/no-package-function-calls-in-business-logic)
Pre-existing. -
log_memory-monitor.go:66—glog.Infof("MEMORY MONITOR - Started")— bareglog.Infoat V0. Reserved for production-default operator info; this is a debug/troubleshooting message. Also uses uppercase message. (rules:go-cli/slog-not-glog-in-new-projects,go-glog/use-v-for-debug-not-info,go-logging/lowercase-log-messages)
Pre-existing. -
log_memory-monitor.go:72—glog.Infof("MEMORY MONITOR - Completed")— same issues as line 66. (rules:go-cli/slog-not-glog-in-new-projects,go-glog/use-v-for-debug-not-info,go-logging/lowercase-log-messages)
Pre-existing. -
log_memory-monitor.go:76—runtime.GC()— directruntimepackage call in production business logic. (rule:go-composition/no-package-function-calls-in-business-logic)
Pre-existing. -
log_memory-monitor.go:77—time.Sleep(100 * time.Millisecond)— directtime.Sleepin business logic. (rule:go-composition/no-package-function-calls-in-business-logic)
Pre-existing.
Should Fix (Important)
-
log_memory-monitor.go—github.com/golang/glogimport used throughout. New projects should uselog/slog(Go 1.21+ stdlib). (rule:go-cli/slog-not-glog-in-new-projects)
Pre-existing. Note:glogis widespread in this codebase; migration is a separate effort. -
log_set-loglevel-setter.go—github.com/golang/glogimport used throughout. (rule:go-cli/slog-not-glog-in-new-projects)
Pre-existing.
Nice to Have (Optional)
log_memory-monitor.go:66,72— Log messages"MEMORY MONITOR - Started"and"MEMORY MONITOR - Completed"start with uppercase letters. Structured log streams prefer lowercase first characters. (rule:go-logging/lowercase-log-messages)
Pre-existing.
Selector Mode Traceability
- Candidates: Unable to enumerate (jq command not available in this session); all rules derived from mechanical funnel output.
- Mechanical funnel: 34 findings across
go-architecture-assistant,go-composition,go-concurrency,go-error-assistant,go-quality-assistant,go-test-quality-assistant. - Applicable rules: All mechanical findings are technically applicable (violations exist in changed files).
- Key distinction: ~95% of flagged lines are pre-existing code this PR did not touch. The PR's actual scope is the three
libtime.Now()/time.Since→libtime.Now().Subchanges, all of which are correct.
Step 6: Next Steps
The architectural violations (direct package calls throughout log_set-loglevel-setter.go and log_memory-monitor.go) are a pre-existing cleanup track. Resolving them would require:
- Introducing a
CurrentDateTimeGetterinterface (orTimeProvider) injected via constructors - Replacing all direct
libtime.Now(),glog,flag,fmt,runtimecalls with interface-wrapped versions - Using
github.com/bborbe/runconcurrency primitives instead of raw goroutines for the auto-reset timer - Adding counterfeiter directives (or confirming the existing directives work correctly with
go generate)
The libtime consistency fix itself is correct and needs no further changes.
Verdict JSON
{
"verdict": "request-changes",
"summary": "The PR's core libtime consistency fix is correct — Set() and resetLogLevel() now both use libtime.Now(), and LogMemoryUsage uses libtime.Now(), so time-threshold logic can be exercised against a frozen clock in tests. However, the mechanical funnel surfaced 34 genuine MUST-tier violations (raw goroutines, direct package calls throughout both files, context.Background() in business logic, glog instead of slog) that exist in the changed files. These are pre-existing architectural issues not introduced by this PR, but the funnel's findings contract requires them to be reported as Must Fix.",
"comments": [
{
"file": "log_set-loglevel-setter.go",
"line": 23,
"severity": "critical",
"message": "LogLevelSetter interface lacks //counterfeiter:generate directive — hand-written test mocks will drift silently. Verify mocks/log-loglevel-setter.go is produced by go generate (directive exists at line 18)."
},
{
"file": "log_memory-monitor.go",
"line": 19,
"severity": "critical",
"message": "MemoryMonitor interface lacks //counterfeiter:generate directive — hand-written test mocks will drift silently. Verify mocks/memory-monitor.go is produced by go generate (directive exists at line 17)."
},
{
"file": "log_set-loglevel-setter.go",
"line": 72,
"severity": "critical",
"message": "libtime.Now() called directly in business logic instead of via injected CurrentDateTimeGetter — makes time-threshold check untestable against a frozen clock. The PR changes this line from time.Now() to libtime.Now(); the underlying architecture (no time getter injection) is pre-existing."
},
{
"file": "log_set-loglevel-setter.go",
"line": 75,
"severity": "critical",
"message": "flag.Set and strconv.Itoa called directly in business logic — stdlib flag registers global state via init() and is untestable. Pre-existing code not modified by this PR."
},
{
"file": "log_set-loglevel-setter.go",
"line": 77,
"severity": "critical",
"message": "glog.V direct call in business logic. New projects should use log/slog. Pre-existing code not modified by this PR."
},
{
"file": "log_set-loglevel-setter.go",
"line": 80,
"severity": "critical",
"message": "Raw go func() for auto-reset — leaks if caller cancels, requires hand-rolled sync that drifts toward deadlocks. Also context.Background() in business logic. Pre-existing code not modified by this PR."
},
{
"file": "log_set-loglevel-setter.go",
"line": 83,
"severity": "critical",
"message": "ctx.Done() called on a context.Background()-derived timeout ctx, not the incoming ctx parameter — cancellation of caller's context is ignored. Pre-existing code."
},
{
"file": "log_set-loglevel-setter.go",
"line": 84,
"severity": "critical",
"message": "l.resetLogLevel() called from raw goroutine without synchronization with l.mux. Pre-existing code."
},
{
"file": "log_set-loglevel-setter.go",
"line": 93,
"severity": "critical",
"message": "libtime.Now() called directly in business logic (the read side of the time.Since fix). The PR correctly replaces time.Since with libtime.Now().Sub; the underlying architecture is pre-existing."
},
{
"file": "log_set-loglevel-setter.go",
"line": 98,
"severity": "critical",
"message": "flag.Set with strconv.Itoa — same stdlib flag issue as line 75. Pre-existing code."
},
{
"file": "log_memory-monitor.go",
"line": 46,
"severity": "critical",
"message": "libtime.Now() called directly in business logic — PR changes this from time.Now() to libtime.Now(), which is correct; the architecture (no CurrentDateTimeGetter injection) is pre-existing."
},
{
"file": "log_memory-monitor.go",
"line": 49,
"severity": "critical",
"message": "now.Sub(m.lastLogTime) — time arithmetic on direct time.Time value. Pre-existing architecture."
},
{
"file": "log_memory-monitor.go",
"line": 60,
"severity": "critical",
"message": "fmt.Sprintf direct call in business logic. Pre-existing."
},
{
"file": "log_memory-monitor.go",
"line": 61,
"severity": "critical",
"message": "m.LogMemoryUsage(name) recursive self-call bypasses the mutex lock on the outer call. Pre-existing."
},
{
"file": "log_memory-monitor.go",
"line": 66,
"severity": "critical",
"message": "Bare glog.Infof at V0 for a debug/troubleshooting message; also uppercase log message. Pre-existing code."
},
{
"file": "log_memory-monitor.go",
"line": 72,
"severity": "critical",
"message": "Bare glog.Infof at V0; uppercase log message. Pre-existing code."
},
{
"file": "log_memory-monitor.go",
"line": 76,
"severity": "critical",
"message": "runtime.GC() direct call in production business logic. Pre-existing."
},
{
"file": "log_memory-monitor.go",
"line": 77,
"severity": "critical",
"message": "time.Sleep direct call in business logic. Pre-existing."
},
{
"file": "log_memory-monitor.go",
"line": 13,
"severity": "major",
"message": "github.com/golang/glog import — new projects should use log/slog. Pre-existing."
},
{
"file": "log_set-loglevel-setter.go",
"line": 14,
"severity": "major",
"message": "github.com/golang/glog import — new projects should use log/slog. Pre-existing."
},
{
"file": "log_memory-monitor.go",
"line": 66,
"severity": "nit",
"message": "Log message starts with uppercase letter. Pre-existing."
},
{
"file": "log_memory-monitor.go",
"line": 72,
"severity": "nit",
"message": "Log message starts with uppercase letter. Pre-existing."
}
],
"concerns_addressed": [
"correctness: log_set-loglevel-setter.go line 94 — time.Since(l.lastSetTime) changed to libtime.Now().Sub(l.lastSetTime) — both write (line 73, now libtime.Now()) and read now use the same clock source. ✅ FIXED.",
"correctness: auto-reset timer logic now uses same clock source as Set() call. ✅ FIXED.",
"tests: log_memory-monitor.go line 46 now uses libtime.Now() — tests can freeze this clock via libtime.CurrentDateTimeGetter injection. ✅ ARCHITECTURE PRE-EXISTING (no getter injection added); correctness of the change itself is confirmed."
]
}There was a problem hiding this comment.
Now let me perform Step 4c-sel (CLASSIFY) and Step 4d-sel (ADJUDICATE) in-session, then compile the report.
Step 4c-sel CLASSIFY — judgment rule candidates triggered by the diff:
Changed files: CHANGELOG.md, log_memory-monitor.go, log_set-loglevel-setter.go
| Rule | Decision | Reason |
|---|---|---|
adr/required-for-irreversible-architecture-decisions |
skipped | libtime swap is a library consistency change, not an irreversible architecture decision (no new top-level package or external dependency beyond what the module already uses) |
changelog/conventional-prefix-required |
applicable | CHANGELOG.md is modified; the ## Unreleased entry starts with fix: — valid prefix |
go-architecture/private-struct-matches-interface |
skipped | diff is a bugfix substituting library calls; no interface/struct pairing issue introduced |
go-testing/libtime-injection-required |
applicable | The PR's entire purpose is fixing time.Now() / time.Since() → libtime.Now() consistency — this rule is the direct mandate |
Step 4d-sel ADJUDICATE
Mechanical findings are authoritative MUST-tier. However, I must distinguish between:
- What this PR introduced (report as findings)
- What pre-existed in modified files but is outside this PR's scope (note as pre-existing, not attributable to this PR)
The mechanical funnel findings in log_memory-monitor.go and log_set-loglevel-setter.go overwhelmingly represent pre-existing architectural debt (raw goroutines, context.Background(), flag.Set, glog.V0 calls) that predates this PR's libtime-consistency scope. Per the report contract, I attribute only what the diff shows was introduced or could have been addressed during this change.
Scanning the diff lines introduced by this PR:
log_memory-monitor.go:47—now := libtime.Now()✓ (fixes the inconsistency)log_set-loglevel-setter.go:73—l.lastSetTime = libtime.Now()✓ (fixes the clock write)log_set-loglevel-setter.go:94—libtime.Now().Sub(l.lastSetTime)✓ (fixes the clock read; previouslytime.Since())- CHANGELOG entry — correct
fix:prefix ✓
Pre-existing issues in modified files (not introduced by this PR, outside its scope):
log_memory-monitor.go:66,72—glog.Infof("MEMORY MONITOR - Started/Completed")at V0: messages are developer-trace strings (not operator startup/shutdown events); should beglog.V(...).Infof— pre-existing, unchanged by this PRlog_memory-monitor.go:66,72— uppercase log message prefix — pre-existing, unchangedlog_set-loglevel-setter.go:80-86— rawgo func()withcontext.Background()— pre-existing, unchangedlog_set-loglevel-setter.gostill importsflagandglog— same as before this PR- Struct fields
lastLogTime time.Time,lastSetTime time.Time,autoResetDuration time.Durationstill use stdlibtimetypes — pre-existing, unchanged
Counterfeiter findings — both LogLevelSetter (line 18) and MemoryMonitor (line 17) already have //counterfeiter:generate directives; these are NOT violations.
Must Fix (Critical)
None introduced by this PR.
Should Fix (Important)
log_memory-monitor.go:66—glog.Infof("MEMORY MONITOR - Started")runs at V0 (always-on). "Started" is a developer-trace string, not a production operator event. Gate behindglog.V(N).Infofwith an appropriate verbosity level. (rule:go-glog/use-v-for-debug-not-info)log_memory-monitor.go:72—glog.Infof("MEMORY MONITOR - Completed")same issue — developer-trace string at V0. (rule:go-glog/use-v-for-debug-not-info)
Nice to Have (Optional)
log_memory-monitor.go:66— Log message starts with uppercase. Use lowercase for uniform structured-log streams. (rule:go-logging/lowercase-log-messages) — EXEMPT candidates: acronym "MEMORY" could be considered a proper identifierlog_memory-monitor.go:72— Same uppercase prefix. (rule:go-logging/lowercase-log-messages)
Selector-mode traceability:
- Candidates: 5 judgment rules matched by glob filter
- Applicable: 2 (
changelog/conventional-prefix-required,go-testing/libtime-injection-required) - Skipped:
adr/required-for-irreversible-architecture-decisions→ libtime swap is not irreversible architecturego-architecture/private-struct-matches-interface→ no pairing issue introducedgo-boolean-combinator/result-with-description-not-naked-bool→ no boolean combinator interface touched
Precommit: skipped (selector mode) — CI covers lint+test.
Notes: The raw go func() goroutine at log_set-loglevel-setter.go:80 with context.Background() is pre-existing architectural debt outside this PR's scope. The mechanical funnel's go-composition/no-package-function-calls-in-business-logic findings in these files (e.g. flag.Set, strconv.Itoa, glog.V, fmt.Sprintf, runtime.GC, time.Sleep) also pre-existed and are outside the libtime-consistency scope of this PR. The //counterfeiter:generate directives are already present on both interfaces.
{
"verdict": "approve",
"summary": "PR correctly replaces stdlib time with libtime for clock consistency in LogMemoryUsage, Set, and resetLogLevel — fixing the core bug where time.Since() read a different clock than libtime.Now() wrote. Two pre-existing glog V0 calls remain in the modified files (outside this PR's scope); no new violations introduced.",
"comments": [
{
"file": "log_memory-monitor.go",
"line": 66,
"severity": "major",
"message": "glog.Infof(\"MEMORY MONITOR - Started\") runs at V0 (always-on production). \"Started\" is a developer-trace string, not a production operator event (startup/shutdown/health-change). Gate behind glog.V(N).Infof with an appropriate verbosity level. (rule: go-glog/use-v-for-debug-not-info)"
},
{
"file": "log_memory-monitor.go",
"line": 72,
"severity": "major",
"message": "glog.Infof(\"MEMORY MONITOR - Completed\") same issue — developer-trace string at V0. Gate behind glog.V(N).Infof. (rule: go-glog/use-v-for-debug-not-info)"
},
{
"file": "log_memory-monitor.go",
"line": 66,
"severity": "nit",
"message": "Log message starts with uppercase 'M'. Use lowercase for uniform structured-log stream consistency. (rule: go-logging/lowercase-log-messages)"
},
{
"file": "log_memory-monitor.go",
"line": 72,
"severity": "nit",
"message": "Log message starts with uppercase 'M'. Use lowercase. (rule: go-logging/lowercase-log-messages)"
}
],
"concerns_addressed": [
"correctness: time.Since(lastSetTime) at line 94 replaced with libtime.Now().Sub(lastSetTime) — both read and write now use the same clock",
"correctness: Set method lastSetTime assignment now uses libtime.Now() for consistency with resetLogLevel",
"correctness: LogMemoryUsage now uses libtime.Now() instead of time.Now() for consistent clock with the rest of the library"
]
}The first revision of this PR converted only the write -- seen[key] = libtime.Now() -- and left the read as time.Since(lastSent). That is a mixed clock: under a fake clock the entry carries fake time while expiry is measured against the real one, so advancing the clock past the TTL never expires the entry. This is exactly the regression a reviewer caught in bborbe/log#13, and the bot caught it here. Convert the read as well, and add a spec that advances a fake clock past the TTL and asserts the notification is emitted again -- it fails against the half-converted code.
Whole-codebase review at
7c4b378(a settledrelease v1.6.20commit) via the repo-review pipeline.88 mechanical findings from 74 rules across 36 files → 1 confirmed.
Fixed
S1 — two raw
time.Now()calls in a library that documents the opposite convention (rule:go-time/no-time-now-direct)log_sampler-time.goalready useslibtime.Now()and states the rule in a comment:log_memory-monitor.go:45andlog_set-loglevel-setter.go:71did not follow it. The argument is internal inconsistency, not preference — andgithub.com/bborbe/timeis already a direct dependency ingo.mod, so this adds nothing and risks no import cycle.Both sites drive time-threshold logic —
LogMemoryUsage's log-if-interval-elapsed check andSet's auto-reset-after-duration timestamp. Neither could be exercised deterministically against a frozen clock while callingtime.Now()directly.Refuted — 87 of 88
Triage table (auto-refuted before adjudication): 72
no-package-function-calls22 ·no-bare-error-call22 ·suite-test-file-required9 ·slog-not-glog5 ·subject-under-50-chars4 ·counterfeiter-directive-on-interface4 ·counterfeiter-mocks-required4 ·no-testing-t-direct1 ·unreleased-entry-required1Adjudicated and refuted: 15 — the interesting ones:
go-security/nosec-requires-reason(log_set-loglevel-handler.go:51)#nosec G705 - level is an integer, not user-controlled string. The rule matched the directive without reading its trailing commentgo-errors/no-context-background-in-business-logic(log_set-loglevel-setter.go:79)context.Background()is deliberate and already documented — "intentional: reset must outlive caller's context". An auto-reset timer inheriting the request context would be cancelled the moment the handler returned, defeating its entire purposego-cli/cobra-not-stdlib-flagflag.Set("v", …)driving glog verbosity, not CLI argument parsinggo-context/cancel-check-in-loop(log_sampler-list.go:34)SamplerList.IsSample()takes noctxat all and iterates in memorygo-architecture/no-globals-or-singletonsDefaultSamplerFactoryis an exported package-level default, the same shape ashttp.DefaultClientgo-glog/use-v-for-debug-not-info×3,lowercase-log-messages×3Notes
File set 40 tracked → 36 after excluding generated sources (coding#103).
make precommitgreen (72.7% coverage).Review round 1 — 1 fixed, 1 refuted as fabricated, 4 out of scope
Fixed in
6d9db19— a real bug in the first commit, and a good catch.The finding:
time.Since(l.lastSetTime)at line 94 still used stdlibtimewhile line 73 now wrotelibtime.Now(). Against a frozen clock those are two different clocks, so the first commit made the comparison inconsistent — arguably worse than the uniform-stdlib version it replaced. Nowlibtime.Now().Sub(l.lastSetTime), matching the pattern already used inlog_sampler-time.go.Refuted — verifiably false:
git diff origin/master -- log_set-loglevel-setter.gotouches exactly two lines: one import and one assignment. No counterfeiter line appears anywhere in the diff, before or after. The directive is untouched at its original position.Out of scope — pre-existing code this PR does not modify:
no-raw-go-func(:80)#nosec G118justificationno-context-background-in-business-logic(:81)glog.Infofat V0 (log_memory-monitor.go:66,72)runtime.GC()/time.Sleep(log_memory-monitor.go:77,78)The four out-of-scope items may be worth their own PR; they are not regressions from this change.
make precommitgreen.