You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2021/07/09 13:38:09 UTC
[logging-log4j2] 05/05: LOG4J2-3067 Add CounterResolver.
This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 137a8e00b3fee80724af7f6a36b425a4c920f4c7
Author: Volkan Yazici <vo...@gmail.com>
AuthorDate: Wed Jul 7 15:04:10 2021 +0200
LOG4J2-3067 Add CounterResolver.
---
.../json/resolver/CaseConverterResolver.java | 1 -
.../template/json/resolver/CounterResolver.java | 247 +++++++++++++++++++++
.../json/resolver/CounterResolverFactory.java | 50 +++++
.../json/resolver/CounterResolverTest.java | 158 +++++++++++++
src/changes/changes.xml | 3 +
.../asciidoc/manual/json-template-layout.adoc.vm | 64 ++++++
6 files changed, 522 insertions(+), 1 deletion(-)
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolver.java
index 559cba1..bbc4946 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolver.java
@@ -44,7 +44,6 @@ import java.util.function.Function;
* "replace"
* )
* replacement = "replacement" -> JSON
- *
* </pre>
*
* {@code input} can be any available template value; e.g., a JSON literal,
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
new file mode 100644
index 0000000..aa4a139
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
@@ -0,0 +1,247 @@
+/*
+ * 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.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
+import org.apache.logging.log4j.layout.template.json.util.Recycler;
+
+import java.math.BigInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Consumer;
+
+/**
+ * Resolves a number from an internal counter.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = [ start ] , [ overflowing ] , [ stringified ]
+ * start = "start" -> number
+ * overflowing = "overflowing" -> boolean
+ * stringified = "stringified" -> boolean
+ * </pre>
+ *
+ * Unless provided, <tt>start</tt> and <tt>overflowing</tt> are respectively
+ * set to zero and <tt>true</tt> by default.
+ * <p>
+ * When <tt>overflowing</tt> is set to <tt>true</tt>, the internal counter
+ * is created using a <tt>long</tt>, which is subject to overflow while
+ * incrementing, though garbage-free. Otherwise, a {@link BigInteger} is used,
+ * which does not overflow, but incurs allocation costs.
+ * <p>
+ * When <tt>stringified</tt> is enabled, which is set to <tt>false</tt> by
+ * default, the resolved number will be converted to a string.
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolves a sequence of numbers starting from 0. Once {@link Long#MAX_VALUE}
+ * is reached, counter overflows to {@link Long#MIN_VALUE}.
+ *
+ * <pre>
+ * {
+ * "$resolver": "counter"
+ * }
+ * </pre>
+ *
+ * Resolves a sequence of numbers starting from 1000. Once {@link Long#MAX_VALUE}
+ * is reached, counter overflows to {@link Long#MIN_VALUE}.
+ *
+ * <pre>
+ * {
+ * "$resolver": "counter",
+ * "start": 1000
+ * }
+ * </pre>
+ *
+ * Resolves a sequence of numbers starting from 0 and keeps on doing as long as
+ * JVM heap allows.
+ *
+ * <pre>
+ * {
+ * "$resolver": "counter",
+ * "overflowing": false
+ * }
+ * </pre>
+ */
+public class CounterResolver implements EventResolver {
+
+ private final Consumer<JsonWriter> delegate;
+
+ public CounterResolver(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ this.delegate = createDelegate(context, config);
+ }
+
+ private static Consumer<JsonWriter> createDelegate(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ final BigInteger start = readStart(config);
+ final boolean overflowing = config.getBoolean("overflowing", true);
+ final boolean stringified = config.getBoolean("stringified", false);
+ if (stringified) {
+ final Recycler<StringBuilder> stringBuilderRecycler =
+ createStringBuilderRecycler(context);
+ return overflowing
+ ? createStringifiedLongResolver(start, stringBuilderRecycler)
+ : createStringifiedBigIntegerResolver(start, stringBuilderRecycler);
+ } else {
+ return overflowing
+ ? createLongResolver(start)
+ : createBigIntegerResolver(start);
+ }
+ }
+
+ private static BigInteger readStart(final TemplateResolverConfig config) {
+ final Object start = config.getObject("start", Object.class);
+ if (start == null) {
+ return BigInteger.ZERO;
+ } else if (start instanceof Short || start instanceof Integer || start instanceof Long) {
+ return BigInteger.valueOf(((Number) start).longValue());
+ } else if (start instanceof BigInteger) {
+ return (BigInteger) start;
+ } else {
+ final Class<?> clazz = start.getClass();
+ final String message = String.format(
+ "could not read start of type %s: %s", clazz, config);
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ private static Consumer<JsonWriter> createLongResolver(final BigInteger start) {
+ final long effectiveStart = start.longValue();
+ final AtomicLong counter = new AtomicLong(effectiveStart);
+ return (jsonWriter) -> {
+ final long number = counter.getAndIncrement();
+ jsonWriter.writeNumber(number);
+ };
+ }
+
+ private static Consumer<JsonWriter> createBigIntegerResolver(final BigInteger start) {
+ final AtomicBigInteger counter = new AtomicBigInteger(start);
+ return jsonWriter -> {
+ final BigInteger number = counter.getAndIncrement();
+ jsonWriter.writeNumber(number);
+ };
+ }
+
+ private static Recycler<StringBuilder> createStringBuilderRecycler(
+ final EventResolverContext context) {
+ return context
+ .getRecyclerFactory()
+ .create(
+ StringBuilder::new,
+ stringBuilder -> {
+ final int maxLength =
+ context.getJsonWriter().getMaxStringLength();
+ trimStringBuilder(stringBuilder, maxLength);
+ });
+ }
+
+ private static void trimStringBuilder(
+ final StringBuilder stringBuilder,
+ final int maxLength) {
+ if (stringBuilder.length() > maxLength) {
+ stringBuilder.setLength(maxLength);
+ stringBuilder.trimToSize();
+ }
+ stringBuilder.setLength(0);
+ }
+
+ private static Consumer<JsonWriter> createStringifiedLongResolver(
+ final BigInteger start,
+ final Recycler<StringBuilder> stringBuilderRecycler) {
+ final long effectiveStart = start.longValue();
+ final AtomicLong counter = new AtomicLong(effectiveStart);
+ return (jsonWriter) -> {
+ final long number = counter.getAndIncrement();
+ final StringBuilder stringBuilder = stringBuilderRecycler.acquire();
+ try {
+ stringBuilder.append(number);
+ jsonWriter.writeString(stringBuilder);
+ } finally {
+ stringBuilderRecycler.release(stringBuilder);
+ }
+ };
+ }
+
+ private static Consumer<JsonWriter> createStringifiedBigIntegerResolver(
+ final BigInteger start,
+ final Recycler<StringBuilder> stringBuilderRecycler) {
+ final AtomicBigInteger counter = new AtomicBigInteger(start);
+ return jsonWriter -> {
+ final BigInteger number = counter.getAndIncrement();
+ final StringBuilder stringBuilder = stringBuilderRecycler.acquire();
+ try {
+ stringBuilder.append(number);
+ jsonWriter.writeString(stringBuilder);
+ } finally {
+ stringBuilderRecycler.release(stringBuilder);
+ }
+ };
+ }
+
+ private static final class AtomicBigInteger {
+
+ private final AtomicReference<BigInteger> lastNumber;
+
+ private AtomicBigInteger(final BigInteger start) {
+ this.lastNumber = new AtomicReference<>(start);
+ }
+
+ private BigInteger getAndIncrement() {
+ BigInteger prevNumber;
+ BigInteger nextNumber;
+ do {
+ prevNumber = lastNumber.get();
+ nextNumber = prevNumber.add(BigInteger.ONE);
+ } while (!compareAndSetWithBackOff(prevNumber, nextNumber));
+ return prevNumber;
+ }
+
+ /**
+ * {@link AtomicReference#compareAndSet(Object, Object)} shortcut with a
+ * constant back off. This technique was originally described in
+ * <a href="https://arxiv.org/abs/1305.5800">Lightweight Contention
+ * Management for Efficient Compare-and-Swap Operations</a> and showed
+ * great results in benchmarks.
+ */
+ private boolean compareAndSetWithBackOff(
+ final BigInteger prevNumber,
+ final BigInteger nextNumber) {
+ if (lastNumber.compareAndSet(prevNumber, nextNumber)) {
+ return true;
+ }
+ LockSupport.parkNanos(1); // back-off
+ return false;
+ }
+
+ }
+
+ static String getName() {
+ return "counter";
+ }
+
+ @Override
+ public void resolve(final LogEvent ignored, final JsonWriter jsonWriter) {
+ delegate.accept(jsonWriter);
+ }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolverFactory.java
new file mode 100644
index 0000000..60217ab
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolverFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginFactory;
+
+/**
+ * {@link CounterResolver} factory.
+ */
+@Plugin(name = "CounterResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class CounterResolverFactory implements EventResolverFactory {
+
+ private static final CounterResolverFactory INSTANCE =
+ new CounterResolverFactory();
+
+ private CounterResolverFactory() {}
+
+ @PluginFactory
+ public static CounterResolverFactory getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public String getName() {
+ return CounterResolver.getName();
+ }
+
+ @Override
+ public CounterResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new CounterResolver(context, config);
+ }
+
+}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolverTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolverTest.java
new file mode 100644
index 0000000..d229fcf
--- /dev/null
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolverTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+import org.apache.logging.log4j.layout.template.json.util.JsonReader;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CounterResolverTest {
+
+ @Test
+ void no_arg_setup_should_start_from_zero() {
+ final String eventTemplate = writeJson(asMap("$resolver", "counter"));
+ verify(eventTemplate, 0, 1);
+ }
+
+ @Test
+ void positive_start_should_work() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", 3));
+ verify(eventTemplate, 3, 4);
+ }
+
+ @Test
+ void positive_start_should_work_when_stringified() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", 3,
+ "stringified", true));
+ verify(eventTemplate, "3", "4");
+ }
+
+ @Test
+ void negative_start_should_work() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", -3));
+ verify(eventTemplate, -3, -2);
+ }
+
+ @Test
+ void negative_start_should_work_when_stringified() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", -3,
+ "stringified", true));
+ verify(eventTemplate, "-3", "-2");
+ }
+
+ @Test
+ void min_long_should_work_when_overflow_enabled() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", Long.MIN_VALUE));
+ verify(eventTemplate, Long.MIN_VALUE, Long.MIN_VALUE + 1L);
+ }
+
+ @Test
+ void min_long_should_work_when_overflow_enabled_and_stringified() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", Long.MIN_VALUE,
+ "stringified", true));
+ verify(eventTemplate, "" + Long.MIN_VALUE, "" + (Long.MIN_VALUE + 1L));
+ }
+
+ @Test
+ void max_long_should_work_when_overflowing() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", Long.MAX_VALUE));
+ verify(eventTemplate, Long.MAX_VALUE, Long.MIN_VALUE);
+ }
+
+ @Test
+ void max_long_should_work_when_overflowing_and_stringified() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", Long.MAX_VALUE,
+ "stringified", true));
+ verify(eventTemplate, "" + Long.MAX_VALUE, "" + Long.MIN_VALUE);
+ }
+
+ @Test
+ void max_long_should_work_when_not_overflowing() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", Long.MAX_VALUE,
+ "overflowing", false));
+ verify(
+ eventTemplate,
+ Long.MAX_VALUE,
+ BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE));
+ }
+
+ @Test
+ void max_long_should_work_when_not_overflowing_and_stringified() {
+ final String eventTemplate = writeJson(asMap(
+ "$resolver", "counter",
+ "start", Long.MAX_VALUE,
+ "overflowing", false,
+ "stringified", true));
+ verify(
+ eventTemplate,
+ "" + Long.MAX_VALUE,
+ "" + BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE));
+ }
+
+ private static void verify(
+ final String eventTemplate,
+ final Object expectedNumber1,
+ final Object expectedNumber2) {
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Create the log event.
+ final LogEvent logEvent = Log4jLogEvent.newBuilder().build();
+
+ // Check the 1st serialized event.
+ final String serializedLogEvent1 = layout.toSerializable(logEvent);
+ final Object deserializedLogEvent1 = JsonReader.read(serializedLogEvent1);
+ assertThat(deserializedLogEvent1).isEqualTo(expectedNumber1);
+
+ // Check the 2nd serialized event.
+ final String serializedLogEvent2 = layout.toSerializable(logEvent);
+ final Object deserializedLogEvent2 = JsonReader.read(serializedLogEvent2);
+ assertThat(deserializedLogEvent2).isEqualTo(expectedNumber2);
+
+ }
+
+}
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 3222b34..00f3a5e 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -170,6 +170,9 @@
</release>
<release version="2.15.0" date="2021-MM-DD" description="GA Release 2.15.0">
<!-- ADDS -->
+ <action issue="LOG4J2-3067" dev="vy" type="add">
+ Add CounterResolver to JsonTemplateLayout.
+ </action>
<action issue="LOG4J2-3074" dev="vy" type="add">
Add replacement parameter to ReadOnlyStringMapResolver.
</action>
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc.vm b/src/site/asciidoc/manual/json-template-layout.adoc.vm
index d3204f6..80f706a 100644
--- a/src/site/asciidoc/manual/json-template-layout.adoc.vm
+++ b/src/site/asciidoc/manual/json-template-layout.adoc.vm
@@ -454,6 +454,67 @@ similar to the following:
The complete list of available event template resolvers are provided below in
detail.
+[#event-template-resolver-counter]
+===== `counter`
+
+[source]
+----
+config = [ start ] , [ overflowing ] , [ stringified ]
+start = "start" -> number
+overflowing = "overflowing" -> boolean
+stringified = "stringified" -> boolean
+----
+
+Resolves a number from an internal counter.
+
+Unless provided, `start` and `overflowing` are respectively set to zero and
+`true` by default.
+
+When `stringified` is enabled, which is set to `false by default, the resolved
+number will be converted to a string.
+
+[WARNING]
+====
+When `overflowing` is set to `true`, the internal counter is created using a
+`long`, which is subject to overflow while incrementing, though garbage-free.
+Otherwise, a `BigInteger` is used, which does not overflow, but incurs
+allocation costs.
+====
+
+====== Examples
+
+Resolves a sequence of numbers starting from 0. Once `Long.MAX_VALUE` is
+reached, counter overflows to `Long.MIN_VALUE`.
+
+[source,json]
+----
+{
+ "$resolver": "counter"
+}
+----
+
+Resolves a sequence of numbers starting from 1000. Once `Long.MAX_VALUE` is
+reached, counter overflows to `Long.MIN_VALUE`.
+
+[source,json]
+----
+{
+ "$resolver": "counter",
+ "start": 1000
+}
+----
+
+Resolves a sequence of numbers starting from 0 and keeps on doing as long as
+JVM heap allows.
+
+[source,json]
+----
+{
+ "$resolver": "counter",
+ "overflowing": false
+}
+----
+
[#event-template-resolver-caseConverter]
===== `caseConverter`
@@ -501,8 +562,11 @@ is always expected to be of type string, using non-string ``replacement``s or
`pass` in `errorHandlingStrategy` might result in type incompatibility issues at
the storage level.
+[WARNING]
+====
Unless the input value is ``pass``ed intact or ``replace``d, case conversion is
not garbage-free.
+====
====== Examples