This is an automated email from the ASF dual-hosted git repository.

merlimat pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git


The following commit(s) were added to refs/heads/master by this push:
     new fe5b8062cf BP-69: Adopt slog for structured logging (#4751)
fe5b8062cf is described below

commit fe5b8062cffe260edb076f912cedd3afab390ca9
Author: Matteo Merli <[email protected]>
AuthorDate: Thu Apr 23 07:04:23 2026 -0700

    BP-69: Adopt slog for structured logging (#4751)
    
    Proposal to migrate BookKeeper from SLF4J to the slog structured-logging
    library (mirroring Pulsar PIP-467), and to add a public client API method
    withLoggerContext(Map<String, Object>) on CreateBuilder/OpenBuilder/
    DeleteBuilder so callers can attach their own logging context attributes to
    a ledger Handle. Retires the existing bookkeeper-slogger module (5 internal
    call sites, no external adoption).
    
    Tracking issue: #4750
---
 .../src/pages/bps/BP-69-slog-structured-logging.md | 278 +++++++++++++++++++++
 .../src/pages/community/bookkeeper-proposals.md    |   3 +-
 2 files changed, 280 insertions(+), 1 deletion(-)

diff --git a/site3/website/src/pages/bps/BP-69-slog-structured-logging.md 
b/site3/website/src/pages/bps/BP-69-slog-structured-logging.md
new file mode 100644
index 0000000000..528a2ad900
--- /dev/null
+++ b/site3/website/src/pages/bps/BP-69-slog-structured-logging.md
@@ -0,0 +1,278 @@
+# BP-69: Adopt slog for structured logging
+
+- Issue: [#4750](https://github.com/apache/bookkeeper/issues/4750)
+- State: Under Discussion
+- Release: N/A
+
+### Motivation
+
+BookKeeper today uses SLF4J across the codebase. Logs are unstructured lines 
with
+positional `{}` placeholders, and the same identity attributes (ledger id, 
bookie
+id, entry id, journal id, etc.) are repeated inline in every message string. 
This
+has three concrete consequences:
+
+1. **Poor queryability in log backends.** Modern log pipelines (Loki, Elastic,
+   Splunk, OpenSearch, GCP Logging, CloudWatch) are indexed on structured
+   key/value attributes. Grepping `"ledgerId=12345"` out of free-form messages 
is
+   slow, lossy, and breaks whenever a log message is reworded.
+2. **Boilerplate and inconsistency.** Every log statement restates the same
+   identity context ("ledger 12345, entry 67, bookie 10.0.0.1:3181 ..."), with
+   no consistent attribute naming. Two log sites that mean the same thing get
+   formatted differently.
+3. **`isDebugEnabled()` boilerplate.** To avoid `String.format`/toString cost 
on
+   hot paths with debug disabled, the code is sprinkled with explicit level
+   guards (hundreds of them), which obscure the actual logic.
+
+A similar analysis was done for Apache Pulsar in
+[PIP-467: Adopt slog for structured logging across Pulsar][pip-467]. PIP-467
+reached the same conclusion, selected the `slog` library, was accepted, and the
+migration of the entire Pulsar codebase is complete. Because Pulsar is
+BookKeeper's largest user, a shared structured logging substrate lets
+cross-project debugging chains stay structured and correlated end-to-end (more
+on this in the "Cross-boundary logger context" section below, which is the main
+API change this BP introduces for BookKeeper).
+
+BookKeeper itself has a prior attempt at structured logging, the
+`bookkeeper-slogger` module (`Slogger` API with `kv(key, value)` and `ctx()`).
+It was introduced alongside `DbLedgerStorage`/`DirectEntryLogger` but was never
+adopted broadly: only **5 main-source files** in `bookkeeper-server` use it 
today
+(`DbLedgerStorage`, `DirectEntryLogger`, `DirectCompactionEntryLog`,
+`DirectWriter`, `EntryLogIdsImpl`). The API is clunky (no debug/trace levels,
+verbose `.kv().kv().info(...)` chains, no zero-overhead disabled levels), and
+the SLF4J bridge is the only handler implementation. It is effectively dead
+code. We propose to retire it.
+
+### Public Interfaces
+
+**New public interfaces** (`@Public`/`@Unstable`):
+
+1. A `Map<String, Object>` parameter on the client-side builder API so the
+   caller (Pulsar, a user application, etc.) can attach its own logging
+   context attributes to a ledger `Handle`:
+   - `CreateBuilder withLoggerContext(Map<String, Object> attrs)`
+   - `OpenBuilder withLoggerContext(Map<String, Object> attrs)`
+   - `DeleteBuilder withLoggerContext(Map<String, Object> attrs)`
+
+   When set, the resulting `WriteHandle`/`ReadHandle` binds those attributes
+   onto its internal logger. All BookKeeper client log lines emitted on behalf
+   of that handle automatically carry the caller's attributes — e.g. Pulsar's
+   `managedLedger=<topic>` and `cursor=<subscription>` — without BookKeeper
+   knowing anything about Pulsar concepts. Note that the public API does
+   **not** expose slog types: callers pass a plain `Map`, so they are not
+   required to take a slog dependency to use the feature.
+
+2. A new compile-scope dependency: `io.github.merlimat.slog:slog` (currently
+   `0.9.7`).
+
+**Changed public interfaces**: none. SLF4J remains on the classpath during and
+after the migration as a transitive dependency, because slog ships an SLF4J
+handler (`Slf4jHandler`) that sends structured records to any existing SLF4J
+backend (Logback, Log4j2). Applications that currently configure BookKeeper
+logging via `logback.xml` / `log4j2.xml` continue to work unchanged.
+
+**Removed public interfaces**:
+
+- The `bookkeeper-slogger` module (`org.apache.bookkeeper.slogger.Slogger`,
+  `AbstractSlogger`, `Sloggable`, `ConsoleSlogger`, `NullSlogger`, and the
+  `bookkeeper-slogger-slf4j` bridge). It is `@Unstable` and has no adoption
+  outside the 5 internal files listed above.
+
+**Not changed**:
+
+- The BookKeeper wire protocol, binary formats (journal, entry log, ledger
+  index), metadata formats, metrics, and CLI are untouched.
+- MDC behavior (`MdcUtils`, MDC propagation in `OrderedExecutor`) is preserved;
+  slog's SLF4J handler forwards structured attributes as MDC to the underlying
+  SLF4J backend so that existing log appenders keep rendering them.
+
+### Proposed Changes
+
+#### 1. Add `slog` as a core dependency
+
+- Add `io.github.merlimat.slog:slog:0.9.7` to root `pom.xml`
+  `<dependencyManagement>` and to the global compile classpath, alongside the
+  existing SLF4J API.
+- Add `lombok.config` at the repo root so `@CustomLog` generates a slog
+  `Logger`:
+  ```
+  lombok.log.custom.declaration = io.github.merlimat.slog.Logger \
+      io.github.merlimat.slog.Logger.get(TYPE)
+  ```
+- Continue to ship the SLF4J handler at runtime so existing log configs 
(Logback
+  / Log4j2 XML) keep working.
+
+#### 2. Migrate SLF4J call sites to slog, one module at a time
+
+Phased conversion. Each phase is a single commit; the codebase must compile at
+the end of each phase.
+
+| Phase | Scope                                                       |
+|-------|-------------------------------------------------------------|
+| 1     | `bookkeeper-common`                                         |
+| 2     | `stats`, `bookkeeper-common-allocator`                      |
+| 3     | `bookkeeper-server` (core, largest phase)                   |
+| 4     | `bookkeeper-http`, `tools`                                  |
+| 5     | `stream/distributedlog`                                     |
+| 6     | `metadata-drivers`, `benchmark`, peripheral tests           |
+| 7     | Cleanup: remove `bookkeeper-slogger`, remove `@Slf4j` usage |
+
+Conversion rules:
+
+- `@Slf4j` → `@CustomLog`; `LoggerFactory.getLogger(X.class)` →
+  `Logger.get(X.class)`; naming: static = `LOG`, instance-bound = `log`.
+- `log.info("text {} {}", a, b)` is **not** mechanically rewritten to 
`logf(...)`
+  — instead convert parameters to structured attributes:
+  `log.info().attr("a", a).attr("b", b).log("text")`.
+- Classes with identity (ledger id, bookie id, stream name, ...) build an
+  instance-bound logger in the constructor and drop those attributes from every
+  call site:
+  ```java
+  this.log = Logger.get(PerChannelBookieClient.class)
+          .with().attr("bookieId", bookieId).build();
+  ```
+- `if (log.isDebugEnabled())` guards are removed; slog returns a no-op `Event`
+  for disabled levels so the `.attr()` chain is zero-overhead.
+- When a `.attr()` value is itself expensive to compute (e.g.
+  `Iterables.size(iter)`, `Joiner.on(...).join(...)`,
+  `Arrays.toString(...)`), use slog's lambda form so the lambda is only invoked
+  when the level is enabled:
+  ```java
+  log.debug(e -> e.attr("count", Iterables.size(newLocations))
+                  .log("Update locations"));
+  ```
+
+#### 3. Cross-boundary logger context through the client API
+
+This is the main API change. Today a caller (Pulsar, a user app, a CLI tool)
+that wants to correlate its own log lines with BookKeeper's log lines has no
+way to do so — BookKeeper's client code logs with its own loggers, and the
+caller's context (topic name, cursor name, request id, trace id) never makes
+it into those lines.
+
+We add an optional `withLoggerContext(Map<String, Object>)` method on the
+ledger builder API. The caller provides a plain map of attribute names to
+values; slog does not appear in the public API surface:
+
+```java
+// Pulsar ManagedLedgerImpl
+Map<String, Object> logCtx = Map.of(
+        "managedLedger", name,
+        "namespace", namespace);
+
+CompletableFuture<WriteHandle> f = bookKeeper.newCreateLedgerOp()
+        .withEnsembleSize(3)
+        .withWriteQuorumSize(2)
+        .withAckQuorumSize(2)
+        .withLoggerContext(logCtx)    // <-- new
+        .execute();
+```
+
+Inside BookKeeper, the `LedgerHandle` constructor binds those attributes onto
+its internal logger:
+
+```java
+var builder = Logger.get(LedgerHandle.class).with();
+if (callerAttrs != null) {
+    callerAttrs.forEach(builder::attr);
+}
+this.log = builder.attr("ledgerId", ledgerId).build();
+```
+
+The net effect: a failed write on a bookie emits a log line like
+
+```
+level=ERROR ledgerId=12345 entryId=67 bookieId=bk-3:3181 \
+    managedLedger=persistent://public/default/my-topic \
+    "Write to bookie failed"
+```
+
+without any BookKeeper code referencing the `managedLedger` concept. The
+propagation is one-way (caller context flows in; BookKeeper-only attributes do
+not leak out), and `withLoggerContext` is optional — if unset, BookKeeper uses
+its own `Logger.get(...)` as before and behavior is unchanged.
+
+Scope for this BP: `CreateBuilder`, `OpenBuilder`, `DeleteBuilder`. The same
+mechanism can be extended to `newListLedgersOp` / admin APIs in follow-up
+changes.
+
+#### 4. Retire `bookkeeper-slogger`
+
+In the cleanup phase:
+
+- Convert the 5 remaining `Slogger` call sites in
+  `bookkeeper-server` to the new slog API (they are already structured-event
+  loggers; the translation is mechanical).
+- Delete the `bookkeeper-slogger/` module (API + SLF4J bridge + tests).
+- Remove it from the root `pom.xml` reactor.
+
+### Compatibility, Deprecation, and Migration Plan
+
+- **End-user impact for the log output format**: with the default SLF4J
+  handler, the rendered log lines continue to come from the user's existing
+  Logback/Log4j2 pattern. Structured attributes are forwarded as MDC, so a
+  layout like `%X{ledgerId} %msg` picks them up automatically. No log config
+  change is required to upgrade.
+- **End-user impact for the client API**: additive only. 
`withLoggerContext(...)`
+  is a new method on `CreateBuilder` / `OpenBuilder` / `DeleteBuilder`; 
existing
+  callers that don't use it see no behavioral change. Its parameter is a
+  standard JDK `Map<String, Object>`, so callers are not required to take a
+  slog dependency to use the feature.
+- **Dependency impact**: applications get `io.github.merlimat.slog:slog` as a
+  new transitive dependency. slog has no transitive dependencies of its own
+  beyond an optional SLF4J handler (runtime scope).
+- **`bookkeeper-slogger` removal**: `@Unstable`, 5 internal call sites, zero
+  external adoption observed. We will deprecate in the release that begins the
+  migration and remove after Phase 7 lands. Users who happen to depend on it
+  can migrate to the published slog API, which is a superset.
+- **SLF4J deprecation**: **not** part of this BP. SLF4J stays for the
+  foreseeable future as the rendering backend behind slog.
+- **Rollout**: each phase is independently mergeable and independently
+  revertable. There is no flag day.
+
+### Test Plan
+
+- Each phase preserves the existing unit/integration test suite unchanged;
+  "the tests still pass" is the baseline success criterion per phase.
+- For the new `withLoggerContext(Map<String, Object>)` API, add unit tests
+  that:
+  1. Verify the log events emitted by a `LedgerHandle` carry the attributes
+     supplied via the map (use a capturing slog handler in tests).
+  2. Verify the default (no `withLoggerContext` call) behavior is identical
+     to pre-BP, including logger name.
+- For log-format compatibility, a small integration check in
+  `bookkeeper-server` that wires the slog SLF4J handler to Logback and asserts
+  that MDC-styled patterns render structured attributes.
+- No wire/format/metric tests change — none of those are touched.
+
+### Rejected Alternatives
+
+1. **Stay on SLF4J, adopt a convention.** Rejected: SLF4J's `{}` format has no
+   attribute names, no composable context (beyond MDC, which is thread-local
+   and brittle across async/executor boundaries), and no zero-overhead disabled
+   levels. Cross-project correlation with Pulsar would not be possible in a
+   structured way.
+2. **Expand `bookkeeper-slogger` to cover the whole codebase.** Rejected: the
+   API surface is small and missing critical features (`debug`/`trace`,
+   lambda-deferred attribute evaluation, per-instance context via
+   `with().build()`). It has no external users. Hardening it would mean
+   reinventing slog while duplicating work with Pulsar's choice. Cheaper to
+   retire it.
+3. **Use Log4j2's `ThreadContext` / structured API directly.** Rejected: binds
+   BookKeeper to a specific backend (we currently support Logback, Log4j2, and
+   others via SLF4J). slog keeps the rendering backend pluggable.
+4. **OpenTelemetry Logs Bridge API.** Rejected for this BP: OTel logs are still
+   stabilizing, and binding the in-process logging API to an OTel SDK is a much
+   larger commitment (and dependency footprint) than what is needed to solve
+   the stated problem. Nothing in this BP precludes later adding an OTel
+   handler to slog.
+5. **Expose a slog `Logger` parameter on the builder (e.g.
+   `withLogger(io.github.merlimat.slog.Logger)`) instead of a
+   `Map<String, Object>`.** Rejected: it would force every BookKeeper client
+   that wants to propagate logging context to take a direct compile dependency
+   on slog and to construct a slog `Logger` before calling the client API. The
+   map-of-attributes form keeps slog out of the public API surface while giving
+   the caller everything they actually need (a bag of key/value pairs to bind
+   to the handle's logger). Nothing prevents a caller that already uses slog
+   from deriving the map from an existing logger's attributes.
+
+[pip-467]: https://github.com/apache/pulsar/blob/master/pip/pip-467.md
diff --git a/site3/website/src/pages/community/bookkeeper-proposals.md 
b/site3/website/src/pages/community/bookkeeper-proposals.md
index 320f6dd8ec..c01574a1b8 100644
--- a/site3/website/src/pages/community/bookkeeper-proposals.md
+++ b/site3/website/src/pages/community/bookkeeper-proposals.md
@@ -83,7 +83,7 @@ using Google Doc.
 
 This section lists all the _bookkeeper proposals_ made to BookKeeper.
 
-*Next Proposal Number: 43*
+*Next Proposal Number: 70*
 
 ### Inprogress
 
@@ -110,6 +110,7 @@ Proposal | State
 [BP-41: Separate BookieId from Separate BookieId from Bookie Network 
Address](../bps/BP-41-bookieid) | Accepted
 [BP-42: New Client API - list ledgers](../bps/BP-42-new-api-list-ledgers) | 
Accepted
 [BP-43: Migration to gradle](../bps/BP-43-gradle-migration) | Draft
+[BP-69: Adopt slog for structured 
logging](../bps/BP-69-slog-structured-logging) | Under Discussion
 
 
 ### Adopted

Reply via email to