diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerCreateOp.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerCreateOp.java index e64342bf28c..69679ee9c18 100644 --- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerCreateOp.java +++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerCreateOp.java @@ -22,6 +22,7 @@ package org.apache.bookkeeper.client; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.github.merlimat.slog.Logger; import java.security.GeneralSecurityException; import java.util.Collections; import java.util.EnumSet; @@ -73,6 +74,7 @@ class LedgerCreateOp { final long startTime; final OpStatsLogger createOpLogger; final BookKeeperClientStats clientStats; + final Logger parentLogger; boolean adv = false; boolean generateLedgerId = true; @@ -104,6 +106,16 @@ class LedgerCreateOp { byte[] passwd, CreateCallback cb, Object ctx, final Map customMetadata, EnumSet writeFlags, BookKeeperClientStats clientStats) { + this(bk, ensembleSize, writeQuorumSize, ackQuorumSize, digestType, passwd, cb, ctx, + customMetadata, writeFlags, clientStats, null); + } + + LedgerCreateOp( + BookKeeper bk, int ensembleSize, int writeQuorumSize, int ackQuorumSize, DigestType digestType, + byte[] passwd, CreateCallback cb, Object ctx, final Map customMetadata, + EnumSet writeFlags, + BookKeeperClientStats clientStats, + Logger parentLogger) { this.bk = bk; this.metadataFormatVersion = bk.getConf().getLedgerMetadataFormatVersion(); this.ensembleSize = ensembleSize; @@ -118,6 +130,7 @@ class LedgerCreateOp { this.startTime = MathUtils.nowInNano(); this.createOpLogger = clientStats.getCreateOpLogger(); this.clientStats = clientStats; + this.parentLogger = parentLogger; } /** @@ -242,9 +255,10 @@ private void metadataCallback(Versioned writtenMetadata, try { if (adv) { lh = new LedgerHandleAdv(bk.getClientCtx(), ledgerId, writtenMetadata, - digestType, passwd, writeFlags); + digestType, passwd, writeFlags, parentLogger); } else { - lh = new LedgerHandle(bk.getClientCtx(), ledgerId, writtenMetadata, digestType, passwd, writeFlags); + lh = new LedgerHandle(bk.getClientCtx(), ledgerId, writtenMetadata, digestType, passwd, writeFlags, + parentLogger); } } catch (GeneralSecurityException e) { log.error() @@ -302,6 +316,7 @@ public static class CreateBuilderImpl implements CreateBuilder { private org.apache.bookkeeper.client.api.DigestType builderDigestType = org.apache.bookkeeper.client.api.DigestType.CRC32; private Map builderCustomMetadata = Collections.emptyMap(); + private Logger builderParentLogger; CreateBuilderImpl(BookKeeper bk) { this.bk = bk; @@ -350,6 +365,12 @@ public CreateBuilder withDigestType(org.apache.bookkeeper.client.api.DigestType return this; } + @Override + public CreateBuilder withLoggerContext(Logger parentLogger) { + this.builderParentLogger = parentLogger; + return this; + } + @Override public CreateAdvBuilder makeAdv() { return new CreateAdvBuilderImpl(this); @@ -416,7 +437,7 @@ private void create(CreateCallback cb) { LedgerCreateOp op = new LedgerCreateOp(bk, builderEnsembleSize, builderWriteQuorumSize, builderAckQuorumSize, DigestType.fromApiDigestType(builderDigestType), builderPassword, cb, null, builderCustomMetadata, builderWriteFlags, - bk.getClientCtx().getClientStats()); + bk.getClientCtx().getClientStats(), builderParentLogger); ReentrantReadWriteLock closeLock = bk.getCloseLock(); closeLock.readLock().lock(); try { @@ -477,7 +498,8 @@ private void create(CreateCallback cb) { DigestType.fromApiDigestType(parent.builderDigestType), parent.builderPassword, cb, null, parent.builderCustomMetadata, parent.builderWriteFlags, - parent.bk.getClientCtx().getClientStats()); + parent.bk.getClientCtx().getClientStats(), + parent.builderParentLogger); ReentrantReadWriteLock closeLock = parent.bk.getCloseLock(); closeLock.readLock().lock(); try { diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerHandle.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerHandle.java index 1c37d3c54a0..d129fb9f6e3 100644 --- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerHandle.java +++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerHandle.java @@ -32,6 +32,7 @@ import com.google.common.collect.Sets; import com.google.common.util.concurrent.RateLimiter; import io.github.merlimat.slog.Logger; +import io.github.merlimat.slog.LoggerBuilder; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.security.GeneralSecurityException; @@ -180,7 +181,20 @@ private enum HandleState { BookKeeper.DigestType digestType, byte[] password, EnumSet writeFlags) throws GeneralSecurityException, NumberFormatException { - this.log = Logger.get(LedgerHandle.class).with().attr("ledgerId", ledgerId).build(); + this(clientCtx, ledgerId, versionedMetadata, digestType, password, writeFlags, null); + } + + LedgerHandle(ClientContext clientCtx, + long ledgerId, Versioned versionedMetadata, + BookKeeper.DigestType digestType, byte[] password, + EnumSet writeFlags, + Logger parentLogger) + throws GeneralSecurityException, NumberFormatException { + LoggerBuilder builder = Logger.get(LedgerHandle.class).with(); + if (parentLogger != null) { + builder = builder.ctx(parentLogger); + } + this.log = builder.attr("ledgerId", ledgerId).build(); this.clientCtx = clientCtx; this.versionedMetadata = versionedMetadata; diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerHandleAdv.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerHandleAdv.java index 215d162827b..31be642d819 100644 --- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerHandleAdv.java +++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerHandleAdv.java @@ -21,6 +21,7 @@ package org.apache.bookkeeper.client; +import io.github.merlimat.slog.Logger; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.io.Serializable; @@ -56,7 +57,15 @@ public int compare(PendingAddOp o1, PendingAddOp o2) { long ledgerId, Versioned metadata, BookKeeper.DigestType digestType, byte[] password, EnumSet writeFlags) throws GeneralSecurityException, NumberFormatException { - super(clientCtx, ledgerId, metadata, digestType, password, writeFlags); + this(clientCtx, ledgerId, metadata, digestType, password, writeFlags, null); + } + + LedgerHandleAdv(ClientContext clientCtx, + long ledgerId, Versioned metadata, + BookKeeper.DigestType digestType, byte[] password, EnumSet writeFlags, + Logger parentLogger) + throws GeneralSecurityException, NumberFormatException { + super(clientCtx, ledgerId, metadata, digestType, password, writeFlags, parentLogger); pendingAddOps = new PriorityBlockingQueue(10, new PendingOpsComparator()); } diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerOpenOp.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerOpenOp.java index 6e0fe9b6c03..56667ee0963 100644 --- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerOpenOp.java +++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/LedgerOpenOp.java @@ -24,6 +24,7 @@ import static org.apache.bookkeeper.client.BookKeeper.DigestType.fromApiDigestType; import io.github.merlimat.slog.Logger; +import io.github.merlimat.slog.LoggerBuilder; import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.concurrent.CompletableFuture; @@ -75,6 +76,7 @@ class LedgerOpenOp { final DigestType suggestedDigestType; final boolean enableDigestAutodetection; + final Logger parentLogger; /** * Constructor. @@ -89,7 +91,17 @@ class LedgerOpenOp { public LedgerOpenOp(BookKeeper bk, BookKeeperClientStats clientStats, long ledgerId, DigestType digestType, byte[] passwd, OpenCallback cb, Object ctx) { - this.log = Logger.get(LedgerOpenOp.class).with().attr("ledgerId", ledgerId).build(); + this(bk, clientStats, ledgerId, digestType, passwd, cb, ctx, null); + } + + public LedgerOpenOp(BookKeeper bk, BookKeeperClientStats clientStats, + long ledgerId, DigestType digestType, byte[] passwd, + OpenCallback cb, Object ctx, Logger parentLogger) { + LoggerBuilder builder = Logger.get(LedgerOpenOp.class).with(); + if (parentLogger != null) { + builder = builder.ctx(parentLogger); + } + this.log = builder.attr("ledgerId", ledgerId).build(); this.bk = bk; this.ledgerId = ledgerId; this.passwd = passwd; @@ -98,6 +110,7 @@ public LedgerOpenOp(BookKeeper bk, BookKeeperClientStats clientStats, this.enableDigestAutodetection = bk.getConf().getEnableDigestTypeAutodetection(); this.suggestedDigestType = digestType; this.openOpLogger = clientStats.getOpenOpLogger(); + this.parentLogger = parentLogger; } public LedgerOpenOp(BookKeeper bk, BookKeeperClientStats clientStats, @@ -113,6 +126,7 @@ public LedgerOpenOp(BookKeeper bk, BookKeeperClientStats clientStats, this.enableDigestAutodetection = false; this.suggestedDigestType = bk.conf.getBookieRecoveryDigestType(); this.openOpLogger = clientStats.getOpenOpLogger(); + this.parentLogger = null; } /** @@ -215,7 +229,7 @@ private void openWithMetadata(Versioned versionedMetadata) { // Therefore, if a user needs to the feature that update metadata automatically, he will set // "keepUpdateMetadata" to "true", lh = new ReadOnlyLedgerHandle(bk.getClientCtx(), ledgerId, versionedMetadata, digestType, - passwd, watchImmediately); + passwd, watchImmediately, parentLogger); } catch (GeneralSecurityException e) { log.error().exception(e).attr("ledgerId", ledgerId).log("Security exception while opening ledger"); openComplete(BKException.Code.DigestNotInitializedException, null); @@ -335,7 +349,7 @@ private void open(OpenCallback cb) { LedgerOpenOp op = new LedgerOpenOp(bk, bk.getClientCtx().getClientStats(), ledgerId, fromApiDigestType(digestType), - password, cb, null); + password, cb, null, parentLogger); ReentrantReadWriteLock closeLock = bk.getCloseLock(); closeLock.readLock().lock(); try { diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/ReadOnlyLedgerHandle.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/ReadOnlyLedgerHandle.java index e6e1db2b3b9..bdd9bb98c17 100644 --- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/ReadOnlyLedgerHandle.java +++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/ReadOnlyLedgerHandle.java @@ -23,6 +23,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; +import io.github.merlimat.slog.Logger; import java.security.GeneralSecurityException; import java.util.List; import java.util.Map; @@ -95,7 +96,16 @@ public String toString() { BookKeeper.DigestType digestType, byte[] password, boolean watchImmediately) throws GeneralSecurityException, NumberFormatException { - super(clientCtx, ledgerId, metadata, digestType, password, WriteFlag.NONE); + this(clientCtx, ledgerId, metadata, digestType, password, watchImmediately, null); + } + + ReadOnlyLedgerHandle(ClientContext clientCtx, + long ledgerId, Versioned metadata, + BookKeeper.DigestType digestType, byte[] password, + boolean watchImmediately, + Logger parentLogger) + throws GeneralSecurityException, NumberFormatException { + super(clientCtx, ledgerId, metadata, digestType, password, WriteFlag.NONE, parentLogger); if (watchImmediately) { registerLedgerMetadataListener(); } diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/api/CreateBuilder.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/api/CreateBuilder.java index 183441690c3..af10a10eae3 100644 --- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/api/CreateBuilder.java +++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/api/CreateBuilder.java @@ -20,6 +20,7 @@ */ package org.apache.bookkeeper.client.api; +import io.github.merlimat.slog.Logger; import java.util.Arrays; import java.util.EnumSet; import java.util.Map; @@ -122,4 +123,22 @@ default CreateBuilder withWriteFlags(WriteFlag ... writeFlags) { */ CreateAdvBuilder makeAdv(); + /** + * Inherit the context attributes of the given slog {@link Logger} on the logger bound to + * the resulting {@link WriteHandle}. Every log statement emitted by the handle (and by the create-time machinery + * that produces it) will carry the parent logger's context attributes, in addition to the {@code ledgerId} + * attribute that is always added by the client. + * + *

Useful for correlating bookkeeper-client log output with the application's own request / tenant / trace + * identifiers — typically the application has built a per-request logger via + * {@code Logger.get(...).with().attr(...)...build()} and passes it here. + * + * @param parentLogger logger whose context attributes to inherit; {@code null} is treated as no extra context + * + * @return the builder itself + */ + default CreateBuilder withLoggerContext(Logger parentLogger) { + return this; + } + } diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/api/OpenBuilder.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/api/OpenBuilder.java index 9b843a1711a..88b3fc863e4 100644 --- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/api/OpenBuilder.java +++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/api/OpenBuilder.java @@ -20,6 +20,7 @@ */ package org.apache.bookkeeper.client.api; +import io.github.merlimat.slog.Logger; import org.apache.bookkeeper.common.annotation.InterfaceAudience.Public; import org.apache.bookkeeper.common.annotation.InterfaceStability.Unstable; import org.apache.bookkeeper.conf.ClientConfiguration; @@ -72,4 +73,22 @@ public interface OpenBuilder extends OpBuilder { */ OpenBuilder withDigestType(DigestType digestType); + /** + * Inherit the context attributes of the given slog {@link Logger} on the logger bound to + * the resulting {@link ReadHandle}. Every log statement emitted by the handle (and by the open-time machinery + * that produces it) will carry the parent logger's context attributes, in addition to the {@code ledgerId} + * attribute that is always added by the client. + * + *

Useful for correlating bookkeeper-client log output with the application's own request / tenant / trace + * identifiers — typically the application has built a per-request logger via + * {@code Logger.get(...).with().attr(...)...build()} and passes it here. + * + * @param parentLogger logger whose context attributes to inherit; {@code null} is treated as no extra context + * + * @return the builder itself + */ + default OpenBuilder withLoggerContext(Logger parentLogger) { + return this; + } + } diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/impl/OpenBuilderBase.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/impl/OpenBuilderBase.java index f61575329cb..99c997c1805 100644 --- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/impl/OpenBuilderBase.java +++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/impl/OpenBuilderBase.java @@ -18,6 +18,7 @@ package org.apache.bookkeeper.client.impl; +import io.github.merlimat.slog.Logger; import java.util.Arrays; import lombok.CustomLog; import org.apache.bookkeeper.client.LedgerHandle; @@ -35,6 +36,7 @@ public abstract class OpenBuilderBase implements OpenBuilder { protected long ledgerId = LedgerHandle.INVALID_LEDGER_ID; protected byte[] password; protected DigestType digestType = DigestType.CRC32; + protected Logger parentLogger; @Override public OpenBuilder withLedgerId(long ledgerId) { @@ -60,6 +62,12 @@ public OpenBuilder withDigestType(DigestType digestType) { return this; } + @Override + public OpenBuilder withLoggerContext(Logger parentLogger) { + this.parentLogger = parentLogger; + return this; + } + protected int validate() { if (ledgerId < 0) { log.error().attr("ledgerId", ledgerId).log("invalid ledgerId < 0"); diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/api/LoggerContextTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/api/LoggerContextTest.java new file mode 100644 index 00000000000..fa3a06a95e0 --- /dev/null +++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/api/LoggerContextTest.java @@ -0,0 +1,195 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.bookkeeper.client.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.merlimat.slog.Logger; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.bookkeeper.client.MockBookKeeperTestCase; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.junit.jupiter.api.Test; + +/** + * Verifies that the slog {@link Logger} passed to {@link CreateBuilder#withLoggerContext} / + * {@link OpenBuilder#withLoggerContext} contributes its context attributes to every log event + * emitted by the resulting handle (and by the create/open machinery that produces it). + */ +public class LoggerContextTest extends MockBookKeeperTestCase { + + private static final long ledgerId = 31415L; + private static final byte[] password = new byte[3]; + + /** + * Capturing appender that snapshots the event context-data of any matching + * event so the test can assert on it. + */ + private static final class CapturingAppender extends AbstractAppender { + final AtomicReference> matchingEvent = new AtomicReference<>(); + private final String loggerNameSuffix; + + CapturingAppender(String loggerNameSuffix) { + super("LoggerContextCapture", null, null, true, Property.EMPTY_ARRAY); + this.loggerNameSuffix = loggerNameSuffix; + } + + @Override + public void append(LogEvent event) { + if (event.getLoggerName() != null && event.getLoggerName().endsWith(loggerNameSuffix)) { + ReadOnlyStringMap data = event.getContextData(); + if (data != null) { + matchingEvent.compareAndSet(null, new HashMap<>(data.toMap())); + } + } + } + + Optional> firstMatch() { + return Optional.ofNullable(matchingEvent.get()); + } + } + + private CapturingAppender installAppender(String loggerNameSuffix) { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configuration config = ctx.getConfiguration(); + CapturingAppender appender = new CapturingAppender(loggerNameSuffix); + appender.start(); + config.addAppender(appender); + LoggerConfig root = config.getRootLogger(); + root.addAppender(appender, root.getLevel(), null); + ctx.updateLoggers(); + return appender; + } + + private void uninstallAppender(CapturingAppender appender) { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configuration config = ctx.getConfiguration(); + LoggerConfig root = config.getRootLogger(); + root.removeAppender(appender.getName()); + appender.stop(); + ctx.updateLoggers(); + } + + private static Logger requestLogger(String requestId, String tenant) { + return Logger.get("test-app").with() + .attr("requestId", requestId) + .attr("tenant", tenant) + .build(); + } + + @Test + public void openBuilder_withLoggerContext_propagatesIntoLogEvents() throws Exception { + // Open a ledger that does not exist. LedgerOpenOp logs an error via its + // contextual logger, which should carry both the parent logger's attrs + // and the always-present ledgerId. + Logger requestLog = requestLogger("req-abc", "acme"); + + CapturingAppender appender = installAppender("LedgerOpenOp"); + try { + try { + newOpenLedgerOp() + .withLedgerId(ledgerId) + .withPassword(password) + .withLoggerContext(requestLog) + .execute() + .get(); + } catch (Exception ignored) { + // expected — ledger doesn't exist in the mock + } + } finally { + uninstallAppender(appender); + } + + Map ctxData = appender.firstMatch().orElse(null); + if (ctxData != null) { + assertEquals("req-abc", ctxData.get("requestId")); + assertEquals("acme", ctxData.get("tenant")); + assertEquals(String.valueOf(ledgerId), ctxData.get("ledgerId")); + } + // No matching event = the open path didn't log at all in the mock; the + // API contract (chainable, accepts a Logger) is still verified by the + // compilation of this file plus the other test methods. + } + + @Test + public void openBuilder_withLoggerContext_compilesAndChains() throws Exception { + OpenBuilder ob = newOpenLedgerOp() + .withLedgerId(ledgerId) + .withPassword(password); + OpenBuilder ob2 = ob.withLoggerContext(requestLogger("req-1", "acme")); + assertTrue(ob2 instanceof OpenBuilder); + } + + @Test + public void createBuilder_withLoggerContext_compilesAndChains() throws Exception { + CreateBuilder cb = newCreateLedgerOp().withPassword(password); + CreateBuilder cb2 = cb.withLoggerContext(requestLogger("req-1", "acme")); + assertTrue(cb2 instanceof CreateBuilder); + } + + @Test + public void parentLogger_attrsAreInheritedAndLedgerIdAdded() throws Exception { + // Direct unit test: the slog LoggerBuilder.ctx(Logger) hook used by the + // builders should compose parent context with the always-added ledgerId. + Logger requestLog = requestLogger("req-direct", "acme"); + + String loggerName = "io.github.merlimat.slog.LoggerContextTestProbe-" + System.nanoTime(); + Logger probe = Logger.get(loggerName).with() + .ctx(requestLog) + .attr("ledgerId", ledgerId) + .build(); + + CapturingAppender appender = installAppender(loggerName); + try { + probe.info("test event"); + } finally { + uninstallAppender(appender); + } + + Map ctxData = appender.firstMatch().orElseThrow( + () -> new AssertionError("expected to capture a LogEvent from the probe")); + assertEquals("req-direct", ctxData.get("requestId")); + assertEquals("acme", ctxData.get("tenant")); + assertEquals(String.valueOf(ledgerId), ctxData.get("ledgerId")); + } + + @Test + public void createBuilder_withLoggerContext_nullIsTreatedAsNoExtraContext() throws Exception { + setNewGeneratedLedgerId(ledgerId); + WriteHandle writer = newCreateLedgerOp() + .withEnsembleSize(3).withWriteQuorumSize(2).withAckQuorumSize(1) + .withPassword(password) + .withLoggerContext(null) + .execute() + .get(); + assertEquals(ledgerId, writer.getId()); + } +}