# Logging Framework — decision log

The Phase 5 answer key. Every entry is a question an interviewer actually asks and the answer
that earns the mark: what change the seam anticipates, and what the change would cost without it.

Three classes in `reference/src` beyond `Demo`: `Logger`, `Registration`, and nothing else.
`Logger` holds no rule that a requirement could reach — every rule that changes for a reason
(which destinations exist, how each one filters, how each one renders) lives in the data handed
to `addAppender`, not in a branch inside `log`.

| File | One reason to change |
|---|---|
| `Logger` | the *sequence* of one call to `log` changes |
| `Registration` | what a destination is made of changes |
| `Demo` | what we show when we run it changes |

---

## Seam 1 · `Formatter` — the shelf label and the till, again

**Anticipates:** any new way to render a record — JSON, a one-line summary, a verbose dump with
the thread name, a format some destinations want and others don't.

The two axes named in the contract — *which records a destination accepts* and *what a record
looks like once it does* — are independent on purpose. Reading a record's own fields to decide
its format (say, switching on `level` inside a hypothetical `ConsoleAppender`) would work for
exactly one destination's opinion about formatting. The moment a second destination wants a
second format — and every real log setup has at least two — that logic has to be duplicated or
generalised, and generalising it *is* `Formatter`.

**Cost without it.** Curveball 2 (collapsing repeated messages) reads the record's message to
decide whether to suppress it — if formatting also lived inline in `log`, that one method would
be doing filtering, deduplication and rendering all at once. With the seam, deduplication is a
few lines in `log` and rendering is untouched.

## Seam 2 · `Appender` — one method, and nothing about *how* it writes

**Anticipates:** every destination this problem's curveballs and any real system would add:
console, file, a rolling file, a network sink, a queue, a test double. All of them are "write
this line somewhere" with a different somewhere.

**The decision worth defending: `Appender` receives a finished `String`, never a `LogRecord`.**
Formatting happens once, in `Logger.offer`, *before* the destination is asked to do anything.
The alternative — handing the raw record to the appender and letting it format for itself —
would mean every destination re-implements "look up my formatter and call it," and would make
`Formatter` a suggestion rather than a rule. Doing it once, in one place, is also what makes
Seam 1's independence a fact about the code rather than a convention destinations have to honour.

**Cost without it.** Curveball 3 (a rolling file that switches destination at a size threshold)
needs a completely different notion of "where the bytes end up" — and touches nothing in
`Logger`, because `Appender` never promised anything about *how* a line gets written, only that
one arrives. Measured at **0 lines**.

## Seam 3 · The registry is data (`List<Registration>`), not four fields

**Anticipates:** any change to what happens between "a record was created" and "a destination
received it" — a new axis of filtering, an async destination, a limit on how many destinations
exist. All of these are operations on the list, not new fields on `Logger`.

The obvious alternative is three parallel lists (`appenders`, `thresholds`, `formatters`) or a
`Map<Appender, Level>` that cannot express "the same appender twice under different formats" —
which the base suite requires directly (`oneDestinationTwoFormats`). `Registration` is the
one-line answer: a value holding exactly the three things one destination needs, so "how many
kinds of destination exist" is answered by "how many `Registration`s are in the list," including
more than one for the same `Appender`.

## Seam 4 · `Clock` — injected, and read exactly once per call

Same pattern as the rate limiter's `Clock`, and the same three reasons apply: a timestamp
assertable by the grader requires a clock it controls; one record must see one instant, not a
re-read per destination; and reading it in exactly one place (the top of `log`) means the
question "which instant does destination B see, versus destination A" never has two answers.
`Entry.create()` (zero-argument) wraps `Instant::now` as the production default; every hidden
test uses `Entry.create(Clock)` with a `ManualClock` so a timestamp is a known value instead of
"sometime during the test run."

## The commit discipline · one record, one pass, every destination sees the same thing

`log` does exactly two things in order: build one `LogRecord` from the clock, the level, the
message and the calling thread; then walk the registrations once, offering that *same* record to
each one that clears its threshold. `offer` does *not* mutate anything shared — no scratch
buffer, no counter, no field reused between destinations or between calls. That absence is the
entire reason the concurrency suite is satisfiable: a `String` handed to `Appender.write` was
built fresh, from a local variable, for this call, for this destination, and nothing else in the
system can have touched it first.

**This is the one design decision the stress suite exists to check**, and it is deliberately
easy to get wrong in a way that looks like a harmless optimisation: reuse one `StringBuilder`
field across calls instead of allocating a fresh line each time. See
`tests_concurrency/LoggerStressTest.java`'s class javadoc and `problem.json`'s
`concurrency.proven_against` for the measured proof that this suite catches exactly that mistake.

## Errors · a destination throwing is not the caller's problem

**The decision with no single obviously-correct answer, so it is made and stated rather than
discovered.** `Logger.offer` catches `RuntimeException` from both `Formatter.format` and
`Appender.write`, and swallows it: that one destination is skipped for that one record, every
other destination still runs, and `log` never throws because of a destination.

The alternative — let a destination's exception propagate out of `log` — would mean one
misconfigured file handle takes down every caller's ability to log anything, including whatever
error message might have explained the file handle problem. A logging framework whose own
`log` call can fail is worse than one that occasionally drops a line to a broken destination,
and dropping a line is exactly what already happens when a destination's threshold does not
clear — this is the same kind of "quietly does not deliver," not a new kind of failure.

**Why `IllegalArgumentException`, not a checked exception or a result type, for a null level.**
There is exactly one way to call `log` wrongly enough to reject outright — `null` is not a fifth
severity — and it is a programming error at the call site, not a runtime condition a caller
should be routing around with a try/catch. A checked exception would force every call site
(including the five convenience methods, which cannot fail this way) to declare it; an unchecked
one matches what it actually is.

## `CopyOnWriteArrayList` — E4, and why not a `synchronized` list instead

The registry is read on *every single call* to `log` — once per registration, per call — and
written only when something calls `addAppender`, which in any real system happens a handful of
times at start-up and rarely afterwards. That access pattern is the textbook case for
`CopyOnWriteArrayList`: reads never block, never allocate, and never see a half-finished write,
because a write replaces the whole backing array rather than mutating it in place. A
`Collections.synchronizedList(new ArrayList<>())` would serialise the read path — the operation
that happens constantly — to protect the write path, which almost never runs. It would also still
require the *iterating* thread to hold the lock for the whole loop to avoid `ConcurrentModificationException`,
which a plain synchronized wrapper does not do for you; `tests_concurrency` proves this directly
by catching exactly that exception against an unguarded `ArrayList`.

## Identity and values

`LogRecord` is a value: built once per call, handed unchanged to every destination, and never
compared by anything but its four fields (which `record` gives for free). `Registration` is the
same kind of thing one level down — three references that together mean "one destination" and
are never mutated after construction.

## Collections never escape

Nothing in `LoggerApi` hands back the registrations, and nothing needs to: there is no query
that would require it, and exposing the list would let a caller iterate, or worse hold, structure
that `Logger` needs to control the safety of. This is C5 as much as it is A7 — the surface is
narrow because there was nothing to leave off, not because something useful was cut for tidiness.

## Time is read once, and only from the seam

Worth saying out loud rather than assumed: nothing in this design calls `Instant.now()` or
`System.currentTimeMillis()` anywhere except inside the lambda `Entry.create()` hands to
`Entry.create(Clock)` for the zero-argument convenience path. Every test-observable timestamp
comes from the injected `Clock`.

## What was deliberately *not* built

- **No global minimum level on the logger itself.** The contract's threshold lives per
  destination, and a candidate might reasonably ask "shouldn't there also be one knob for the
  whole logger?" There deliberately isn't one: it would be redundant with per-destination
  thresholds (a destination that should see nothing below `WARN` already says so at
  registration) and would reopen the exact coupling the problem is built to avoid — a single
  "current level" implies one true severity for the whole logger, which is precisely the wrong
  mental model once two destinations disagree about what they want to see.
- **No `Formatters` utility class with a built-in default format.** Tempting, because most real
  frameworks ship one. Left out because every base and curveball test supplies its own formatter
  anyway, and a default nobody is required to use is exactly the kind of speculative surface C5
  argues against — it would be one more contract type existing only to be optional.
- **No log level below `DEBUG` or above `FATAL`, and no way to add one.** The five levels are a
  closed enum for the same reason `Coin` was closed in the vending machine problem: there is no
  such thing as an "unrecognised" level, so there is no failure mode to design for.
- **No structured/JSON output, no MDC, no correlation ids, no sampling, no log rotation as a base
  requirement.** Stated out of scope up front; curveball 3 is where rotation-shaped behaviour
  shows up, entirely inside a destination.

## Rejected alternative: `Logger` as a singleton

**Several public write-ups of this exact problem make the logger a singleton — a single static
`getInstance()` everyone shares.** Rejected here, deliberately, for three concrete reasons rather
than a style preference:

1. **It cannot be constructed twice with different collaborators.** This grader creates a fresh
   logger per test, injects a `ManualClock` it controls, and needs two independent loggers in the
   same JVM run (nothing here shares static state across tests) never to interfere with each
   other. A singleton has exactly one clock, ever, for the life of the process.
2. **It hides a real dependency instead of naming it.** `Entry.create(Clock)` states in the type
   signature that a logger needs a clock; `Logger.getInstance()` states nothing, and the clock
   would have to be smuggled in through a static setter — which is the dependency-inversion
   violation C3 exists to name, not a stylistic nit.
3. **It is untestable in exactly the way this problem's stress suite needs to test it.** Proving
   the reference against a *deliberately broken variant* (see `tests_concurrency`) requires
   swapping in a different `Logger` implementation per run. A process-wide singleton cannot be
   swapped without a classloader trick that would test the trick, not the logger.

None of this means global, convenient access is a bad idea in a real application — it usually
sits one layer up, as a single `Entry.create()` call made once at start-up and the reference
passed down, which is composition, not a singleton enforced by the class itself.
