feat(introspection): auto-log explain/trace via System.Logger + render refresh#216
Conversation
…r refresh Logging (ADR-0014): Mapper and ForwardMapper log their own introspection through java.base's System.Logger — zero dependency, no module-info change, routes to the app's backend (JUL / Logback / Log4j2 …): - DEBUG → explain() structure, once at construction (skipped for trail-less lifted shells). - TRACE → trace(input) value column, per forward(), built from the already-computed result (never re-running forward, which would recurse through trace). Both use the lazy Supplier overload, so nothing renders unless the level is enabled — a single isLoggable check when off. Always-on, level-gated: the log level is the per-mapper opt-in, controllable at runtime from config with no redeploy. Type-pair logger names (io.github.eschizoid.telescope.mapper.<Source>.<Target>) so one mapper — or the whole library by prefix — can be enabled from the app's logging config. Verified via a JUL-captured test (logs at TRACE, silent when off). Navigation auto-logging is deferred (a Telescope has no single construction point and doesn't retain Class<S> for a name). Render refresh: OpticReport.toString now renders Mapped → Skipped → Transformations → Unused sources with a blank line between sections, and a dropped field reads (ignored) (in explain and trace). Same content, matches the original design mockup; golden tests updated.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #216 +/- ##
=========================================
Coverage 79.59% 79.59%
- Complexity 1831 1841 +10
=========================================
Files 80 80
Lines 5762 5783 +21
Branches 1210 1215 +5
=========================================
+ Hits 4586 4603 +17
- Misses 656 660 +4
Partials 520 520 ☔ View full report in Codecov by Harness. |
There was a problem hiding this comment.
Pull request overview
This PR updates Telescope’s introspection rendering to better match the original intended format and adds always-on (level-gated) introspection auto-logging for Mapper / ForwardMapper via java.lang.System.Logger, with ADR documentation and tests.
Changes:
- Refresh
OpticReport.toString()section ordering/spacing and rename dropped-field label from(dropped)to(ignored)(tests updated). - Add
System.Logger-based DEBUG/TRACE auto-logging inMapperandForwardMapper(DEBUG=explain-on-construction, TRACE=value-trace-per-forward). - Add a JUL-captured contract test for the auto-logging behavior and document the decision in ADR-0014.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/adr/0014-system-logger-auto-trace.md | Adds ADR documenting the System.Logger auto-tracing decision and rationale. |
| core/src/test/java/io/github/eschizoid/telescope/introspection/OpticReportTest.java | Updates golden expectations for new section ordering/blank lines and (ignored) label. |
| core/src/test/java/io/github/eschizoid/telescope/introspection/MapperTraceTest.java | Updates trace wording/assertions to (ignored) for dropped fields. |
| core/src/test/java/io/github/eschizoid/telescope/introspection/MapperLoggingTest.java | Adds contract tests validating DEBUG/TRACE auto-logging via JUL-backed System.Logger. |
| core/src/main/java/io/github/eschizoid/telescope/introspection/OpticReport.java | Reorders render sections, adds blank lines between sections, renames DROPPED label to ignored. |
| core/src/main/java/io/github/eschizoid/telescope/conversion/MappingTraces.java | Renames DROPPED label to ignored in value-trace rendering. |
| core/src/main/java/io/github/eschizoid/telescope/conversion/Mapper.java | Introduces System.Logger and emits DEBUG explain + TRACE value-trace logs. |
| core/src/main/java/io/github/eschizoid/telescope/conversion/ForwardMapper.java | Introduces System.Logger and emits DEBUG explain + TRACE value-trace logs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…e Supplier Copilot: the log(Level, Supplier) overload skips invoking the supplier when the level is off, but the capturing lambda is still allocated on every forward(). Gate both the DEBUG (construction) and TRACE (forward) logs with an explicit isLoggable check — plus a non-empty-trail check — BEFORE the Supplier is built, so the hot path is fully allocation-free when off and trail-less shells never log an (empty optic) record. ADR updated to describe the guard precisely.
…eport Reword per review: a report holds either mapping sections or navigation hops (never both), so the last section's trailing blank is the final whitespace that stripTrailing() removes.
… test - OpticReport.toString now builds each non-empty part as a block and joins them with a blank line, instead of appending a trailing blank per section. Copilot: a report CAN hold both mapping rows and hops (a mapping telescope further navigated, e.g. map(A,B).field(B::x)), where the old approach stranded a non-trailing blank between the section and the hops. Joining spaces every case correctly with no stray blank. Pinned by a mixed-report golden test. - MapperLoggingTest disables useParentHandlers on the captured JUL logger (and restores it) so records don't propagate to the root handlers — keeps the contract test isolated and CI logs quiet.
…bump README to 1.1.0 - ADR-0013 "What it looks like" examples updated to the shipped render (Mapped → Skipped → Transformations order, blank lines between sections, (ignored) wording). - ADR-0014: dropped the cosmetic "render refresh" consequence — it's not an architectural decision; the render lives in code + the synced 0013 examples. Kept separate from 0013 (extends it) per the point-in-time-record convention. - README install snippets bumped 1.0.5 → 1.1.0 (latest release). migration-feedback.md's 1.0.5 references are historical (which version a bug was fixed/reported in) — left as-is.
…port, coverage Consolidates the round of fixes from the reviewer fleet plus the aligned-report polish: - Trace render is now total: a field whose toString() throws degrades to the (n/a) sentinel instead of escaping forward(). Enabling TRACE can no longer fail the conversion it only meant to observe (MappingTraces.render). - OpticReport render aligns the left column across every section — the widest cell sets one shared width, so every marker, field, and the following arrow / reason paren land in the same column across Mapped, Skipped, Transformations, and Unused sources rather than four independently-padded blocks. - Coverage: ForwardMapper logging, DEBUG-vs-TRACE level gating (structure at DEBUG, values only at TRACE), trail-less lifted shells silent even at ALL, and a throwing-toString conversion surviving TRACE. Four-section render golden. - README install snippets bumped to 1.1.1; ADR-0013 render examples synced to the aligned layout.
…oot's Logback backend Proves ADR-0014's promise end-to-end, not just against the JDK's default JUL sink: in the org-chart Spring Boot demo, a mapper's explain/trace auto-logging travels System.Logger → java.util.logging → jul-to-slf4j → SLF4J → Logback, and a Logback ListAppender on the type-pair logger captures it. Surfaces the real bridge behaviour honestly: jul-to-slf4j maps System.Logger TRACE (JUL FINER) onto SLF4J DEBUG, so both lines render at DEBUG through the default Spring Boot setup. The config threshold still separates them — DEBUG fires structure only, TRACE additionally fires the per-forward values — which is the knob adopters turn. The two tests pin that toggle through the real backend; ADR-0014 gains an adopter note documenting the bridge level mapping.
…mple-name logger naming - mappingRender collapses alignment padding and asserts the mapped row in-order (✓ firstName → givenName) rather than as independent substrings. - ADR-0014 spells out that logger names use simple class names and the same-simple-name-across-packages collision trade-off.
…gging Documents the mapper introspection surface as a first-class MapStruct differentiator: explain() (static structure, aligned render, typed OpticReport slices for assertions), trace(input) (value column), and the System.Logger auto-logging (DEBUG=explain at build, TRACE=trace per forward; type-pair logger names; the jul-to-slf4j level-render nuance).
…docs Two threads in one pass: Repo-wide convention sweep (describe behaviour in its own terms, not by citation): - Removed every "ADR-00NN" / "Enh N" reference from comments, javadoc, and @DisplayName across core, internal, codegen, lombok, benchmarks, and the org-chart demo — zero ADR references remain in any .java. Each site was reworded to keep its technical explanation intact. - Removed the lone "Bug 2" reference in LambdaIntrospectionTest, describing the propertyOf null-guard fix in its own terms. Copilot review follow-ups: - Bumped the codegen / lombok / quarkus / spring-boot-starter README install snippets from 1.0.5 to 1.1.1 (were stale against the root README). - Replaced the invalid `logger = LEVEL` config snippets in the README and ADR-0014 with valid, copy-pasteable logback.xml and Spring Boot application.properties forms. - Documented inline (Mapper / ForwardMapper) that the type-pair logger names use simple class names by design — the same-simple-name-across-packages collision is an accepted trade-off for readable, greppable names.
Follow-up to the ADR-citation sweep — removes the "Phase A/B/C/D" jargon that only made sense as pointers into the ADR-0006 rollout plan, describing each concept in its own terms instead. - Renamed the holder-path test fixtures and their tests: PhaseCSrc/PhaseCDst → HolderIsoSrc/HolderIsoDst, PhaseCPlainSrc/PhaseCPlainDst → HolderIsoPlainSrc/HolderIsoPlainDst, DeepMapPhaseCHolderTest → DeepMapHolderIsoReadTest, DeepMapPhaseDConstructTest → DeepMapHolderIsoConstructTest (the codegen holders rename with them). - Reworded the remaining "Phase B/C/D" prose in comments, javadoc, @DisplayName, and assert messages across core, internal, benchmarks, and lombok — "Phase B" → the holder probe, "Phase C" → the structural-iso holder path, etc. - Removed the docs/migration-feedback.md references from MigrationRegressionTest, describing the regressions as adopter-reported mapping bugs. Zero ADR / Phase / feedback-doc / Bug-number tokens remain in any .java. Full build green.
…st RuntimeException MappingTraces.render() and readDotted() are debug aids that must never fail the conversion they observe. They caught RuntimeException but let an Error escape — a cyclic toString() (StackOverflowError) or a toString()/getter that throws AssertionError would still break forward() at TRACE. Broaden both to Throwable, rethrowing only OutOfMemoryError (a dying JVM, not the value's fault). The throwing-toString test now throws an Error to pin the stronger guarantee.
Two things, one PR (per request).
1. explain() render refresh
OpticReport.toStringnow renders Mapped → Skipped → Transformations → Unused sources, with a blank line between sections, and a dropped field reads(ignored)(in bothexplainandtrace). Same content, closer to the original design mockup. Golden tests updated. No API change.2. Auto-log explain/trace via
System.Logger(ADR-0014)MapperandForwardMapperlog their own introspection throughjava.lang.System.Logger— zero dependency (java.base), nomodule-infochange, routes to whatever backend the app runs (JUL default / Logback / Log4j2 / JBoss Logging via aLoggerFinder):explain()structure, once at construction (skipped for trail-less lifted shells).trace(input)value column, perforward(), built from the already-computed result (never re-runningforward, which would recurse throughtrace).Both use the lazy
log(Level, Supplier<String>)overload, so the render is never computed unless the level is enabled — a singleisLoggablecheck on the hot path when off.Always-on, level-gated (not a flag, not an opt-in view): the log level is the switch, controllable per-mapper at runtime from config, no redeploy. Considered and rejected a
tracing=truefactory flag (compile-time, redundant with the level, loses runtime control) and amapper.logged()view (extra public surface). Adds zero new public API — just an internal logger.Type-pair logger names —
io.github.eschizoid.telescope.mapper.<Source>.<Target>:Navigation (
Telescope) auto-logging is deferred: it's built fluently (no single construction point for a DEBUGexplain) and doesn't retainClass<S>for a logger name.Telescope.explain()/trace()stay explicit-only.Verified by a JUL-captured test (
MapperLoggingTest): logs the explain structure at DEBUG + the value trace at TRACE, and is completely silent (nothing rendered) when the level is off.Design decisions flagged for review
.mapper.root.