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