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 <[email protected]> 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
