diff --git a/bookkeeper-slogger/api/build.gradle b/bookkeeper-slogger/api/build.gradle
new file mode 100644
index 00000000000..0264913e640
--- /dev/null
+++ b/bookkeeper-slogger/api/build.gradle
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+plugins {
+ id 'java'
+}
+
+dependencies {
+ testImplementation depLibs.hamcrest
+ testImplementation depLibs.junit
+}
+
+jar.archiveBaseName = 'bookkeeper-slogger-api'
+
+jar {
+ dependsOn tasks.named("writeClasspath")
+}
+
diff --git a/bookkeeper-slogger/api/pom.xml b/bookkeeper-slogger/api/pom.xml
new file mode 100644
index 00000000000..9850f0d193a
--- /dev/null
+++ b/bookkeeper-slogger/api/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ 4.0.0
+
+ bookkeeper-slogger-parent
+ org.apache.bookkeeper
+ 4.16.0-SNAPSHOT
+ ..
+
+ org.apache.bookkeeper
+ bookkeeper-slogger-api
+ Apache BookKeeper :: Structured Logger :: API
+
diff --git a/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/AbstractSlogger.java b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/AbstractSlogger.java
new file mode 100644
index 00000000000..55ca52d38bc
--- /dev/null
+++ b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/AbstractSlogger.java
@@ -0,0 +1,275 @@
+/*
+ * 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.slogger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+
+/**
+ * Abstract implementation of slogger. Keeps track of key value pairs.
+ */
+public abstract class AbstractSlogger implements Slogger, Iterable {
+ /**
+ * Levels at which slogger can slog.
+ */
+ public enum Level {
+ INFO,
+ WARN,
+ ERROR
+ }
+
+ private static final int MAX_DEPTH = 3;
+ private List parentCtx;
+
+ private ThreadLocal> kvs = new ThreadLocal>() {
+ @Override
+ protected List initialValue() {
+ return new ArrayList<>();
+ }
+ };
+ private ThreadLocal> flattenedTls = ThreadLocal.withInitial(ArrayList::new);
+
+ protected AbstractSlogger(Iterable parentCtx) {
+ List flattened = new ArrayList<>();
+ flattenKeyValues(parentCtx.iterator(), (k, v) -> {
+ flattened.add(k);
+ flattened.add(v);
+ });
+ this.parentCtx = Collections.unmodifiableList(flattened);
+ }
+
+ protected abstract Slogger newSlogger(Optional> clazz, Iterable parent);
+ protected abstract void doLog(Level level, Enum> event, String message,
+ Throwable throwable, List keyValues);
+
+ private void flattenAndLog(Level level, Enum> event, String message,
+ Throwable throwable) {
+ List flattened = flattenedTls.get();
+ flattened.clear();
+
+ flattenKeyValues(this::addToFlattened);
+ doLog(level, event, message, throwable, flattened);
+ }
+
+ @Override
+ public void info(String message) {
+ flattenAndLog(Level.INFO, null, message, null);
+ }
+
+ @Override
+ public void info(String message, Throwable cause) {
+ flattenAndLog(Level.INFO, null, message, cause);
+ }
+
+ @Override
+ public void info(Enum> event) {
+ flattenAndLog(Level.INFO, event, null, null);
+ }
+
+ @Override
+ public void info(Enum> event, Throwable cause) {
+ flattenAndLog(Level.INFO, event, null, cause);
+ }
+
+ @Override
+ public void warn(String message) {
+ flattenAndLog(Level.WARN, null, message, null);
+ }
+
+ @Override
+ public void warn(String message, Throwable cause) {
+ flattenAndLog(Level.WARN, null, message, cause);
+ }
+
+ @Override
+ public void warn(Enum> event) {
+ flattenAndLog(Level.WARN, event, null, null);
+ }
+
+ @Override
+ public void warn(Enum> event, Throwable cause) {
+ flattenAndLog(Level.WARN, event, null, cause);
+ }
+
+ @Override
+ public void error(String message) {
+ flattenAndLog(Level.ERROR, null, message, null);
+ }
+
+ @Override
+ public void error(String message, Throwable cause) {
+ flattenAndLog(Level.ERROR, null, message, cause);
+ }
+
+ @Override
+ public void error(Enum> event) {
+ flattenAndLog(Level.ERROR, event, null, null);
+ }
+
+ @Override
+ public void error(Enum> event, Throwable cause) {
+ flattenAndLog(Level.ERROR, event, null, cause);
+ }
+
+ @Override
+ public Slogger ctx() {
+ try {
+ return newSlogger(Optional.empty(), this);
+ } finally {
+ kvs.get().clear();
+ }
+ }
+
+ @Override
+ public Slogger ctx(Class> clazz) {
+ try {
+ return newSlogger(Optional.of(clazz), this);
+ } finally {
+ kvs.get().clear();
+ }
+ }
+
+ @Override
+ public Iterator iterator() {
+ CtxIterator iterator = this.iterator.get();
+ iterator.reset();
+ return iterator;
+ }
+
+ protected void clearCurrentCtx() {
+ kvs.get().clear();
+ }
+
+ private void addToFlattened(String key, String value) {
+ flattenedTls.get().add(key);
+ flattenedTls.get().add(value);
+ }
+
+ protected void flattenKeyValues(BiConsumer consumer) {
+ Iterator iter = iterator();
+ try {
+ flattenKeyValues(iter, consumer);
+ } finally {
+ kvs.get().clear();
+ }
+ }
+
+ public static void flattenKeyValues(Iterator iter,
+ BiConsumer consumer) {
+ while (iter.hasNext()) {
+ String key = iter.next().toString();
+ if (!iter.hasNext()) {
+ return; // key without value
+ }
+ Object value = iter.next();
+
+ if (value instanceof Sloggable) {
+ addWithPrefix(key, (Sloggable) value, consumer, 0);
+ } else if (value.getClass().isArray()) {
+ consumer.accept(key, arrayToString(value));
+ } else {
+ consumer.accept(key, value.toString());
+ }
+ }
+ }
+
+ @Override
+ public Slogger kv(Object key, Object value) {
+ kvs.get().add(key);
+ kvs.get().add(value);
+ return this;
+ }
+
+ private static void addWithPrefix(String prefix, Sloggable value,
+ BiConsumer consumer, int depth) {
+ value.log(new SloggableAccumulator() {
+ @Override
+ public SloggableAccumulator kv(Object key, Object value) {
+ if (value instanceof Sloggable && depth < MAX_DEPTH) {
+ addWithPrefix(prefix + "." + key.toString(),
+ (Sloggable) value, consumer, depth + 1);
+ } else if (value.getClass().isArray()) {
+ consumer.accept(prefix + "." + key.toString(), arrayToString(value));
+ } else {
+ consumer.accept(prefix + "." + key.toString(), value.toString());
+ }
+ return this;
+ }
+ });
+ }
+
+ private static String arrayToString(Object o) {
+ if (o instanceof long[]) {
+ return Arrays.toString((long[]) o);
+ } else if (o instanceof int[]) {
+ return Arrays.toString((int[]) o);
+ } else if (o instanceof short[]) {
+ return Arrays.toString((short[]) o);
+ } else if (o instanceof char[]) {
+ return Arrays.toString((char[]) o);
+ } else if (o instanceof byte[]) {
+ return Arrays.toString((byte[]) o);
+ } else if (o instanceof boolean[]) {
+ return Arrays.toString((boolean[]) o);
+ } else if (o instanceof float[]) {
+ return Arrays.toString((float[]) o);
+ } else if (o instanceof double[]) {
+ return Arrays.toString((double[]) o);
+ } else if (o instanceof Object[]) {
+ return Arrays.toString((Object[]) o);
+ } else {
+ return o.toString();
+ }
+ }
+
+ private final ThreadLocal iterator = new ThreadLocal() {
+ @Override
+ protected CtxIterator initialValue() {
+ return new CtxIterator();
+ }
+ };
+ class CtxIterator implements Iterator {
+ int index = 0;
+
+ private void reset() {
+ index = 0;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return index < (parentCtx.size() + kvs.get().size());
+ }
+
+ @Override
+ public Object next() {
+ int i = index++;
+ if (i < parentCtx.size()) {
+ return parentCtx.get(i);
+ } else {
+ i -= parentCtx.size();
+ return kvs.get().get(i);
+ }
+ }
+ }
+}
diff --git a/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/ConsoleSlogger.java b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/ConsoleSlogger.java
new file mode 100644
index 00000000000..31d647816fc
--- /dev/null
+++ b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/ConsoleSlogger.java
@@ -0,0 +1,130 @@
+/*
+ * 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.slogger;
+
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Simple slogger implementation which writes json to console.
+ */
+public class ConsoleSlogger extends AbstractSlogger {
+ private static final int MAX_STACKTRACE_ELEMENTS = 20;
+ private static final int MAX_CAUSES = 10;
+ private final Class> clazz;
+
+ ConsoleSlogger() {
+ this(ConsoleSlogger.class);
+ }
+
+ ConsoleSlogger(Class> clazz) {
+ this(clazz, Collections.emptyList());
+ }
+
+ ConsoleSlogger(Class> clazz, Iterable parent) {
+ super(parent);
+ this.clazz = clazz;
+ }
+
+ @Override
+ protected Slogger newSlogger(Optional> clazz, Iterable parent) {
+ return new ConsoleSlogger(clazz.orElse(ConsoleSlogger.class), parent);
+ }
+
+ @Override
+ protected void doLog(Level level, Enum> event, String message,
+ Throwable throwable, List keyValues) {
+ String nowAsISO = ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("{");
+ keyValue(builder, "date", nowAsISO);
+ builder.append(",");
+ keyValue(builder, "level", level.toString());
+ if (event != null) {
+ builder.append(",");
+ keyValue(builder, "event", event.toString());
+ }
+ if (message != null) {
+ builder.append(",");
+ keyValue(builder, "message", message);
+ }
+
+ for (int i = 0; i < keyValues.size(); i += 2) {
+ builder.append(",");
+ keyValue(builder, keyValues.get(i).toString(), keyValues.get(i + 1).toString());
+ }
+ if (throwable != null) {
+ builder.append(",");
+ Throwable cause = throwable;
+ StringBuilder stacktrace = new StringBuilder();
+ int causes = 0;
+ while (cause != null) {
+ stacktrace.append("[").append(cause.getMessage()).append("] at ");
+ int i = 0;
+ for (StackTraceElement element : cause.getStackTrace()) {
+ if (i++ > MAX_STACKTRACE_ELEMENTS) {
+ stacktrace.append("<|[frames omitted]");
+ }
+ stacktrace.append("<|").append(element.toString());
+ }
+ cause = cause.getCause();
+ if (cause != null) {
+ if (causes++ > MAX_CAUSES) {
+ stacktrace.append(" [max causes exceeded] ");
+ break;
+ } else {
+ stacktrace.append(" caused by ");
+ }
+ }
+ }
+ keyValue(builder, "exception", stacktrace.toString());
+ }
+ builder.append("}");
+
+ System.out.println(builder.toString());
+ }
+
+ private static void keyValue(StringBuilder sb, String key, String value) {
+ quotedAppend(sb, key);
+ sb.append(":");
+ quotedAppend(sb, value);
+ }
+
+ private static void quotedAppend(StringBuilder sb, String str) {
+ sb.append('"');
+ for (int i = 0; i < str.length(); i++) {
+ char c = str.charAt(i);
+ if (c == '\\') {
+ sb.append("\\\\");
+ } else if (c == '"') {
+ sb.append("\\\"");
+ } else if (c < ' ') {
+ sb.append(String.format("\\u%04X", (int) c));
+ } else {
+ sb.append(c);
+ }
+ }
+ sb.append('"');
+ }
+}
diff --git a/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/NullSlogger.java b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/NullSlogger.java
new file mode 100644
index 00000000000..2c5302f80ab
--- /dev/null
+++ b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/NullSlogger.java
@@ -0,0 +1,63 @@
+/*
+ * 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.slogger;
+
+class NullSlogger implements Slogger {
+ @Override
+ public Slogger kv(Object key, Object value) {
+ return this;
+ }
+
+ @Override
+ public Slogger ctx() {
+ return this;
+ }
+
+ @Override
+ public Slogger ctx(Class> clazz) {
+ return this;
+ }
+
+ @Override
+ public void info(String message) {}
+ @Override
+ public void info(String message, Throwable cause) {}
+ @Override
+ public void info(Enum> event) {}
+ @Override
+ public void info(Enum> event, Throwable cause) {}
+
+ @Override
+ public void warn(String message) {}
+ @Override
+ public void warn(String message, Throwable cause) {}
+ @Override
+ public void warn(Enum> event) {}
+ @Override
+ public void warn(Enum> event, Throwable cause) {}
+
+ @Override
+ public void error(String message) {}
+ @Override
+ public void error(String message, Throwable cause) {}
+ @Override
+ public void error(Enum> event) {}
+ @Override
+ public void error(Enum> event, Throwable cause) {}
+}
diff --git a/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/Sloggable.java b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/Sloggable.java
new file mode 100644
index 00000000000..165dff5d682
--- /dev/null
+++ b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/Sloggable.java
@@ -0,0 +1,27 @@
+/*
+ * 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.slogger;
+
+/**
+ * Interface to be implemented by classes that want more control
+ * over how they are added to a structured log.
+ */
+public interface Sloggable {
+ SloggableAccumulator log(SloggableAccumulator accumulator);
+}
diff --git a/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/SloggableAccumulator.java b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/SloggableAccumulator.java
new file mode 100644
index 00000000000..10f484b0b07
--- /dev/null
+++ b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/SloggableAccumulator.java
@@ -0,0 +1,27 @@
+/*
+ * 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.slogger;
+
+/**
+ * Interface passed to Sloggable instances, with which they
+ * can add their own key/value pairs to the logged event.
+ */
+public interface SloggableAccumulator {
+ SloggableAccumulator kv(Object key, Object value);
+}
diff --git a/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/Slogger.java b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/Slogger.java
new file mode 100644
index 00000000000..f91b2b8f449
--- /dev/null
+++ b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/Slogger.java
@@ -0,0 +1,47 @@
+/*
+ * 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.slogger;
+
+/**
+ * Event logging interface will support for key value pairs and reusable context.
+ */
+public interface Slogger {
+ Slogger kv(Object key, Object value);
+
+ Slogger ctx();
+ Slogger ctx(Class> clazz); // <- should this be class or Logger? Logger requires some generics
+
+ void info(String message);
+ void info(String message, Throwable cause);
+ void info(Enum> event);
+ void info(Enum> event, Throwable cause);
+
+ void warn(String message);
+ void warn(String message, Throwable cause);
+ void warn(Enum> event);
+ void warn(Enum> event, Throwable cause);
+
+ void error(String message);
+ void error(String message, Throwable cause);
+ void error(Enum> event);
+ void error(Enum> event, Throwable cause);
+
+ Slogger NULL = new NullSlogger();
+ Slogger CONSOLE = new ConsoleSlogger();
+}
diff --git a/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/package-info.java b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/package-info.java
new file mode 100644
index 00000000000..f132eafa8d2
--- /dev/null
+++ b/bookkeeper-slogger/api/src/main/java/org/apache/bookkeeper/slogger/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Structured logging.
+ */
+package org.apache.bookkeeper.slogger;
diff --git a/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/ConcurrencyTest.java b/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/ConcurrencyTest.java
new file mode 100644
index 00000000000..07c28c37bfa
--- /dev/null
+++ b/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/ConcurrencyTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.slogger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+/**
+ * Test concurrent access to slogger.
+ */
+public class ConcurrencyTest {
+ enum Events {
+ FOOBAR
+ }
+
+ @Test
+ public void testConcurrentFlattening() throws Exception {
+ final int numThreads = 100;
+ final int numIterations = 10000;
+
+ Slogger slog = new AbstractSlogger(Collections.emptyList()) {
+ @Override
+ public Slogger newSlogger(Optional> clazz, Iterable parent) {
+ return this;
+ }
+ @Override
+ public void doLog(Level level, Enum> event, String message,
+ Throwable throwable, List keyValues) {
+ for (int i = 0; i < keyValues.size(); i += 2) {
+ if (!keyValues.get(i).equals(keyValues.get(i + 1))) {
+
+ throw new RuntimeException("Concurrency error");
+ }
+ }
+ }
+ };
+
+ ExecutorService executor = Executors.newFixedThreadPool(numThreads);
+ List> futures = new ArrayList<>();
+ for (int i = 0; i < numThreads; i++) {
+ futures.add(executor.submit(() -> {
+ for (int j = 0; j < numIterations; j++) {
+ String value = "kv" + Thread.currentThread().getId() + "-" + j;
+
+ slog.kv(value, value).info(Events.FOOBAR);
+ }
+ }));
+ }
+
+ for (Future> f : futures) {
+ f.get(60, TimeUnit.SECONDS);
+ }
+ }
+}
diff --git a/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/ConsoleSloggerTest.java b/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/ConsoleSloggerTest.java
new file mode 100644
index 00000000000..0ca3612346c
--- /dev/null
+++ b/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/ConsoleSloggerTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.slogger;
+
+import org.junit.Test;
+
+/**
+ * Test console slogger.
+ * Doesn't actually assert anything, but can be used to eyeball
+ */
+public class ConsoleSloggerTest {
+ enum Events {
+ FOOBAR,
+ BARFOO
+ };
+
+ @Test
+ public void testBasic() throws Exception {
+ ConsoleSlogger root = new ConsoleSlogger();
+ root.kv("fo\"o", "ba\r \\").info(Events.FOOBAR);
+ }
+}
+
diff --git a/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/MockSlogger.java b/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/MockSlogger.java
new file mode 100644
index 00000000000..82a6a4a94a4
--- /dev/null
+++ b/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/MockSlogger.java
@@ -0,0 +1,88 @@
+/*
+ * 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.slogger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Mock Slogger.
+ */
+public class MockSlogger extends AbstractSlogger {
+ List events = new ArrayList<>();
+
+ public MockSlogger() {
+ super(new ArrayList<>());
+ }
+
+ private MockSlogger(Iterable parentCtx) {
+ super(parentCtx);
+ }
+
+ @Override
+ protected Slogger newSlogger(Optional> clazz, Iterable parentCtx) {
+ return new MockSlogger(parentCtx);
+ }
+
+ @Override
+ protected void doLog(Level level, Enum> event, String message, Throwable throwable,
+ List keyValues) {
+ Map tmpKvs = new HashMap<>();
+ for (int i = 0; i < keyValues.size(); i += 2) {
+ tmpKvs.put(keyValues.get(i).toString(), keyValues.get(i + 1));
+ }
+ events.add(new MockEvent(level, event, message, tmpKvs, throwable));
+ }
+
+ static class MockEvent {
+ private final Level level;
+ private final Enum> event;
+ private final String message;
+ private final Map kvs;
+ private final Throwable throwable;
+
+ MockEvent(Level level, Enum> event, String message,
+ Map kvs, Throwable throwable) {
+ this.level = level;
+ this.event = event;
+ this.message = message;
+ this.kvs = kvs;
+ this.throwable = throwable;
+ }
+
+ Level getLevel() {
+ return level;
+ }
+ Enum> getEvent() {
+ return event;
+ }
+ String getMessage() {
+ return message;
+ }
+ Map getKeyValues() {
+ return kvs;
+ }
+ Throwable getThrowable() {
+ return throwable;
+ }
+ }
+}
diff --git a/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/SloggerTest.java b/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/SloggerTest.java
new file mode 100644
index 00000000000..ffb96d81d1f
--- /dev/null
+++ b/bookkeeper-slogger/api/src/test/java/org/apache/bookkeeper/slogger/SloggerTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.slogger;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import org.junit.Test;
+
+/**
+ * Test Slogger.
+ */
+public class SloggerTest {
+ enum Events {
+ FOOBAR,
+ BARFOO
+ };
+
+ @Test
+ public void testBasic() throws Exception {
+ MockSlogger root = new MockSlogger();
+ root.kv("foo", 2324).kv("bar", 2342).info(Events.FOOBAR);
+ assertThat(root.events, hasSize(1));
+ assertThat(root.events.get(0).getLevel(), is(MockSlogger.Level.INFO));
+ assertThat(root.events.get(0).getEvent(), is(Events.FOOBAR));
+ assertThat(root.events.get(0).getKeyValues(),
+ allOf(hasEntry("foo", "2324"),
+ hasEntry("bar", "2342")));
+ }
+
+ @Test
+ public void testSloggable() throws Exception {
+ MockSlogger root = new MockSlogger();
+ root.kv("fancy", new FancyClass(0, 2)).info(Events.FOOBAR);
+ assertThat(root.events, hasSize(1));
+ assertThat(root.events.get(0).getLevel(), is(MockSlogger.Level.INFO));
+ assertThat(root.events.get(0).getEvent(), is(Events.FOOBAR));
+ assertThat(root.events.get(0).getKeyValues(),
+ allOf(hasEntry("fancy.foo", "0"),
+ hasEntry("fancy.bar", "2"),
+ hasEntry("fancy.baz.baz", "123")));
+ }
+
+ @Test
+ public void testList() throws Exception {
+ MockSlogger root = new MockSlogger();
+ List list = new ArrayList<>();
+ list.add(1);
+ list.add(2);
+ root.kv("list", list).info(Events.FOOBAR);
+
+ assertThat(root.events, hasSize(1));
+ assertThat(root.events.get(0).getLevel(), is(MockSlogger.Level.INFO));
+ assertThat(root.events.get(0).getEvent(), is(Events.FOOBAR));
+ assertThat(root.events.get(0).getKeyValues(), hasEntry("list", "[1, 2]"));
+ }
+
+ @Test
+ public void testMap() throws Exception {
+ MockSlogger root = new MockSlogger();
+ HashMap map = new HashMap<>();
+ map.put(1, 3);
+ map.put(2, 4);
+ root.kv("map", map).info(Events.FOOBAR);
+
+ assertThat(root.events, hasSize(1));
+ assertThat(root.events.get(0).getLevel(), is(MockSlogger.Level.INFO));
+ assertThat(root.events.get(0).getEvent(), is(Events.FOOBAR));
+ assertThat(root.events.get(0).getKeyValues(), hasEntry("map", "{1=3, 2=4}"));
+ }
+
+ @Test
+ public void testArray() throws Exception {
+ MockSlogger root = new MockSlogger();
+ String[] array = {"foo", "bar"};
+ root.kv("array", array).info(Events.FOOBAR);
+
+ assertThat(root.events, hasSize(1));
+ assertThat(root.events.get(0).getLevel(), is(MockSlogger.Level.INFO));
+ assertThat(root.events.get(0).getEvent(), is(Events.FOOBAR));
+ assertThat(root.events.get(0).getKeyValues(), hasEntry("array", "[foo, bar]"));
+ }
+
+ @Test
+ public void testNestingLimit() throws Exception {
+ }
+
+ @Test
+ public void testCtx() throws Exception {
+ MockSlogger root = new MockSlogger();
+ MockSlogger withCtx = (MockSlogger) root.kv("ctx1", 1234).kv("ctx2", 4321).ctx();
+
+ withCtx.kv("someMore", 2345).info(Events.FOOBAR);
+
+ assertThat(withCtx.events, hasSize(1));
+ assertThat(withCtx.events.get(0).getLevel(), is(MockSlogger.Level.INFO));
+ assertThat(withCtx.events.get(0).getEvent(), is(Events.FOOBAR));
+ System.out.println("kvs " + withCtx.events.get(0).getKeyValues());
+ assertThat(withCtx.events.get(0).getKeyValues(),
+ allOf(hasEntry("ctx1", "1234"),
+ hasEntry("ctx2", "4321"),
+ hasEntry("someMore", "2345")));
+ }
+
+ @Test
+ public void textCtxImmutableAfterCreation() throws Exception {
+ }
+
+ static class FancyClass implements Sloggable {
+ int foo;
+ int bar;
+ OtherFancyClass baz;
+
+ FancyClass(int foo, int bar) {
+ this.foo = foo;
+ this.bar = bar;
+ this.baz = new OtherFancyClass(123);
+ }
+
+ @Override
+ public SloggableAccumulator log(SloggableAccumulator slogger) {
+ return slogger.kv("foo", foo)
+ .kv("bar", bar)
+ .kv("baz", baz);
+ }
+ }
+
+ static class OtherFancyClass implements Sloggable {
+ int baz;
+
+ OtherFancyClass(int baz) {
+ this.baz = baz;
+ }
+
+ @Override
+ public SloggableAccumulator log(SloggableAccumulator slogger) {
+ return slogger.kv("baz", baz);
+ }
+ }
+}
diff --git a/bookkeeper-slogger/pom.xml b/bookkeeper-slogger/pom.xml
new file mode 100644
index 00000000000..cd59d929caa
--- /dev/null
+++ b/bookkeeper-slogger/pom.xml
@@ -0,0 +1,50 @@
+
+
+
+ 4.0.0
+
+ org.apache.bookkeeper
+ bookkeeper
+ 4.16.0-SNAPSHOT
+ ..
+
+ pom
+ org.apache.bookkeeper
+ bookkeeper-slogger-parent
+ Apache BookKeeper :: Structured Logger :: Parent
+
+
+ api
+ slf4j
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+
+
diff --git a/bookkeeper-slogger/slf4j/build.gradle b/bookkeeper-slogger/slf4j/build.gradle
new file mode 100644
index 00000000000..85653428b0d
--- /dev/null
+++ b/bookkeeper-slogger/slf4j/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+plugins {
+ id 'java'
+}
+
+dependencies {
+ implementation project(':bookkeeper-slogger:api')
+ implementation depLibs.slf4j
+ testImplementation depLibs.slf4jSimple
+ testImplementation depLibs.junit
+}
+
+jar.archiveBaseName = 'bookkeeper-slogger-slf4j'
+
+jar {
+ dependsOn tasks.named("writeClasspath")
+}
+
diff --git a/bookkeeper-slogger/slf4j/pom.xml b/bookkeeper-slogger/slf4j/pom.xml
new file mode 100644
index 00000000000..b442035175b
--- /dev/null
+++ b/bookkeeper-slogger/slf4j/pom.xml
@@ -0,0 +1,38 @@
+
+
+
+ 4.0.0
+
+ bookkeeper-slogger-parent
+ org.apache.bookkeeper
+ 4.16.0-SNAPSHOT
+ ..
+
+ org.apache.bookkeeper
+ bookkeeper-slogger-slf4j
+ Apache BookKeeper :: Structured Logger :: SLF4J Implementation
+
+
+ org.apache.bookkeeper
+ bookkeeper-slogger-api
+ ${project.parent.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
diff --git a/bookkeeper-slogger/slf4j/src/main/java/org/apache/bookkeeper/slogger/slf4j/Slf4jSlogger.java b/bookkeeper-slogger/slf4j/src/main/java/org/apache/bookkeeper/slogger/slf4j/Slf4jSlogger.java
new file mode 100644
index 00000000000..81400227ab3
--- /dev/null
+++ b/bookkeeper-slogger/slf4j/src/main/java/org/apache/bookkeeper/slogger/slf4j/Slf4jSlogger.java
@@ -0,0 +1,106 @@
+/*
+ * 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.slogger.slf4j;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.apache.bookkeeper.slogger.AbstractSlogger;
+import org.apache.bookkeeper.slogger.Slogger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+/**
+ * Slf4j implementation of slogger.
+ */
+public class Slf4jSlogger extends AbstractSlogger {
+ private ThreadLocal> mdcKeysTls = new ThreadLocal>() {
+ @Override
+ protected List initialValue() {
+ return new ArrayList<>();
+ }
+ };
+
+ private final Logger log;
+
+ public Slf4jSlogger(Class> clazz) {
+ this(clazz, Collections.emptyList());
+ }
+
+ Slf4jSlogger() {
+ this(Slf4jSlogger.class);
+ }
+
+ Slf4jSlogger(Class> clazz, Iterable parent) {
+ super(parent);
+ this.log = LoggerFactory.getLogger(clazz);
+ }
+
+ @Override
+ protected Slogger newSlogger(Optional> clazz, Iterable parent) {
+ return new Slf4jSlogger(clazz.orElse(Slf4jSlogger.class), parent);
+ }
+
+ @Override
+ protected void doLog(Level level, Enum> event, String message,
+ Throwable throwable, List keyValues) {
+ List mdcKeys = mdcKeysTls.get();
+ mdcKeys.clear();
+ try {
+ if (event != null) {
+ MDC.put("event", event.toString());
+ mdcKeys.add("event");
+ }
+
+ for (int i = 0; i < keyValues.size(); i += 2) {
+ MDC.put(keyValues.get(i).toString(), keyValues.get(i + 1).toString());
+ mdcKeys.add(keyValues.get(i).toString());
+ }
+
+ String msg = message == null ? event.toString() : message;
+ switch (level) {
+ case INFO:
+ log.info(msg);
+ break;
+ case WARN:
+ if (throwable != null) {
+ log.warn(msg, throwable);
+ } else {
+ log.warn(msg);
+ }
+ break;
+ default:
+ case ERROR:
+ if (throwable != null) {
+ log.error(msg, throwable);
+ } else {
+ log.error(msg);
+ }
+ break;
+ }
+ } finally {
+ for (String key : mdcKeys) {
+ MDC.remove(key);
+ }
+ mdcKeys.clear();
+ }
+ }
+}
diff --git a/bookkeeper-slogger/slf4j/src/main/java/org/apache/bookkeeper/slogger/slf4j/package-info.java b/bookkeeper-slogger/slf4j/src/main/java/org/apache/bookkeeper/slogger/slf4j/package-info.java
new file mode 100644
index 00000000000..69ff1ed3eb9
--- /dev/null
+++ b/bookkeeper-slogger/slf4j/src/main/java/org/apache/bookkeeper/slogger/slf4j/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Structured logging (slf4j implementation).
+ */
+package org.apache.bookkeeper.slogger.slf4j;
diff --git a/bookkeeper-slogger/slf4j/src/test/java/org/apache/bookkeeper/slogger/slf4j/Slf4jTest.java b/bookkeeper-slogger/slf4j/src/test/java/org/apache/bookkeeper/slogger/slf4j/Slf4jTest.java
new file mode 100644
index 00000000000..848a05cfacf
--- /dev/null
+++ b/bookkeeper-slogger/slf4j/src/test/java/org/apache/bookkeeper/slogger/slf4j/Slf4jTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.slogger.slf4j;
+
+import org.apache.bookkeeper.slogger.Slogger;
+import org.junit.Test;
+
+/**
+ * Test to eyeball slf4j output.
+ * Contains no asserts.
+ */
+public class Slf4jTest {
+ enum Events {
+ FOOBAR
+ }
+ @Test
+ public void testBasic() throws Exception {
+ Slogger slogger = new Slf4jSlogger(Slf4jTest.class);
+ slogger.kv("foo", 123).kv("bar", 432).info(Events.FOOBAR);
+ }
+}
diff --git a/pom.xml b/pom.xml
index 6b63673124c..dd9cb22b01c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,7 @@
bookkeeper-dist
shaded
microbenchmarks
+ bookkeeper-slogger
tests
native-io