Skip to content

Add log4cats logging with --verbose and --debug - #127

Merged
rochala merged 4 commits into
mainfrom
feat/log4cats-verbose-debug
Aug 22, 2026
Merged

Add log4cats logging with --verbose and --debug#127
rochala merged 4 commits into
mainfrom
feat/log4cats-verbose-debug

Conversation

@rochala

@rochala rochala commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Closes #63. Closes #56.

There was no way to see what cellar was doing. DocstringExtractor swallowed every failure with catch case _: Exception => (), NearMatchFinder logged through a java.util.logging logger gated behind Level.FINE with no configuration (so its output went nowhere), and SymbolResolver reported "not found" without saying what it had tried.

The logger

log4cats-core plus a StderrLogger implementing Logger[IO]. Deliberately not slf4j-backed: slf4j-nop is on the CLI classpath to silence coursier, and a real backend would both un-silence it and add native-image weight.

  • -v/--verbose — progress and warnings
  • --debug — detail plus stack traces
  • CELLAR_LOG=verbose|debug — same, without a flag, so an installed binary can be debugged without a rebuild. An explicit flag wins.

Available on get, get-external, get-source, list, list-external, search, search-external. Output goes to stderr only, so stdout stays clean enough to pipe into a prompt. The logger is threaded as a defaulted parameter (StderrLogger.off), so no existing call site or test changed.

Resolution diagnostics (#56)

ResolutionTrace records which strategies SymbolResolver tried. The lookups are synchronous and cannot write to a Logger[IO] as they go, so the trace is drained and logged by resolve. A miss is what people actually debug, so the trail is reported at verbose on failure and kept at debug on success:

$ cellar get-external org.typelevel:cats-core_3:2.10.0 cats.DoesNotExist99 --verbose
[cellar] could not resolve cats.DoesNotExist99
[cellar]   findStaticClass(cats.DoesNotExist99): miss
[cellar]   findStaticModuleClass(cats.DoesNotExist99): miss
[cellar]   findStaticTerm(cats.DoesNotExist99): miss
[cellar]   findStaticType(cats.DoesNotExist99): miss
[cellar]   findPackage(cats.DoesNotExist99): miss
[cellar]   nested lookup over 2 segments
[cellar]   prefix 'cats.DoesNotExist99': miss
[cellar]   no top-level root matched any prefix

--debug adds the resolved classpath, build-tool detection, cache hits, per-entry scan failures, and stack traces.

Two silent behaviours now surfaced

  • ContextResource.readClasspathRobust discarded classpath entries that trip a tasty-query MatchError, with no report. Dropping one can turn a present symbol into a "not found", so it is now logged at warn.
  • DocstringExtractor now reports which .tasty entry it inspected, whether the inspector callback ever fired, and any failure. It uses .attempt, which catches Throwable rather than Exception — a compiler that cannot start under native-image fails with an Error, which the old catch let escape the diagnostic entirely.

Behaviour preserved

findStaticTerm/findStaticType/findPackage use a non-recording variant of the trace helper, mirroring tryOrNone: their failures must not promote a NotFound into a LookupFailed. Routing them through the recording variant would have been a behaviour change smuggled in under a logging patch.

Why this exists now

Docstrings appear on the JVM but not in the released v0.1.0-M10 native binary, even though DocstringExtractor is byte-identical between M10 and HEAD. Building cli.assembly and running it reproduces the working behaviour, which rules out fat-jar classpath merging and leaves native-image as the suspect. This PR is the instrument needed to see the actual failure instead of a swallowed one; diagnosing it is follow-up work.

Testing

./mill lib.test 372/372, ./mill cli.test 236/236. Verified via ./mill cli.run and the cli.assembly jar, on both the success and not-found paths.

🤖 Generated with Claude Code

rochala and others added 3 commits August 22, 2026 12:55
Closes #63.

There was no way to see what cellar was doing. DocstringExtractor in
particular swallowed every failure with `catch case _: Exception => ()`,
so a TASTy inspection that failed was indistinguishable from a symbol
that genuinely has no docstring.

Adds log4cats-core and a StderrLogger implementing Logger[IO]. It is
deliberately not slf4j-backed: slf4j-nop is on the CLI classpath to
silence coursier, and a real backend would un-silence it and add
native-image weight.

Diagnostics go to stderr only, so stdout stays clean enough to pipe into
a prompt. CELLAR_LOG=verbose|debug sets the level without a flag, which
allows debugging an installed binary; an explicit flag wins over it.

DocstringExtractor.extract now returns IO and reports which .tasty entry
it inspected, whether the inspector callback ever fired, and any failure.
It catches Throwable rather than Exception — a compiler that cannot start
under native-image fails with an Error, which the old catch let escape
the diagnostic entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #56.

The previous commit added the logging mechanism but pointed it only at
the docstring path. #56's motivating case is the opposite one — a symbol
reported as not found despite being in the JAR — which was still silent.

Adds a ResolutionTrace that records which lookup strategies SymbolResolver
tried. The lookups are synchronous and cannot write to a Logger[IO] as
they go, so the trace is drained and logged by `resolve`. A miss is what
people actually debug, so the trail is reported at verbose on failure and
kept at debug on success.

Also logs, at debug: the resolved classpath, build-tool detection, and
classpath cache hits. And at warn: classpath entries dropped by
readClasspathRobust, which silently discarded entries — dropping one can
turn a present symbol into a "not found".

NearMatchFinder used a java.util.logging logger gated behind Level.FINE
with no configuration, so its output went nowhere; it now uses the same
Logger[IO]. Its skipped-entry reports are summarised at verbose and
detailed at debug, since the JRE alone contributes dozens every run and
the per-entry detail would drown the trail.

The flags now apply to get, get-external, get-source, list, list-external,
search and search-external.

findStaticTerm/findStaticType/findPackage keep using a non-recording
variant of the trace helper, mirroring tryOrNone: their failures must not
promote a NotFound into a LookupFailed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A resolution failure previously printed only "Could not resolve
'<coord>'. Check that the group ID, artifact ID, and version are
correct." Coursier names every location it consulted in its own message,
and CoordinateNotFound was already holding that cause and discarding it.

The first question on a resolution failure is whether cellar even looked
at the repository you expected, so this belongs in the error rather than
behind a flag. The leading line of coursier's message is dropped since it
restates the coordinate we already printed.

Also attaches coursier's progress bars to stderr when diagnostics are on,
so a slow fetch shows which URL it is waiting on. coursierapi.Logger
exposes only nop() and progressBars() — no overridable callbacks — so
this is the only route through the interface API. The bars emit ANSI
cursor control unconditionally, which corrupts a redirected stream, so
they are attached only when stderr is a terminal.

--verbose/--debug now also apply to deps and meta, which resolve
coordinates and so hit the same failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rochala
rochala force-pushed the feat/log4cats-verbose-debug branch from a1a43c5 to 6def988 Compare August 22, 2026 11:05
Catch inside the blocking thunk rather than via `.attempt`. The failure
this diagnostic exists for — a compiler that cannot start under
native-image — arrives as a LinkageError, which is not NonFatal.
cats-effect routes those to onFatalFailure and tears down the runtime
without ever reaching `.attempt`, so the previous code would have died
silently on exactly the case it was written to report. Confirmed by
running both shapes: `.attempt` never fires, the in-thunk catch does.

Delete the coursier progress bars. They were gated on
`System.console() != null`, which reflects stdin/stdout and never
stderr, and which returns non-null even when redirected on JDK 22+ (the
native image targets GraalVM 23). Redirecting stderr to collect a
diagnostics file would therefore have written ANSI cursor control into
that file. Java exposes no way to test whether stderr is a terminal, so
this cannot be gated correctly; the unconditional "Tried:" list already
names the URLs, which was the point.

Delete the temp file when the copy out of the jar throws. Only the
caller's `guarantee` deleted it, and that never runs if the copy fails.

Scope the docs to what is actually instrumented: the lookup-strategy
trail comes from SymbolResolver, which list and search do not use — they
resolve through SymbolLister. They still get classpath and fetch
diagnostics at --debug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rochala
rochala merged commit de4b550 into main Aug 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate to log4cats Add --verbose / --debug flag for diagnostic output

1 participant