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/07 13:04:24 UTC

[logging-log4j2] branch release-2.x updated: LOG4J2-3067 Add CounterResolver.

This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/release-2.x by this push:
     new d9e7ed4  LOG4J2-3067 Add CounterResolver.
d9e7ed4 is described below

commit d9e7ed452130895a615cf50e0904b05495d941a1
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    | 169 +++++++++++++++++++++
 .../json/resolver/CounterResolverFactory.java      |  50 ++++++
 .../json/resolver/CounterResolverTest.java         | 109 +++++++++++++
 src/changes/changes.xml                            |   3 +
 .../asciidoc/manual/json-template-layout.adoc.vm   |  59 +++++++
 6 files changed, 390 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 51372e8..0ac69f7 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..9071277
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
@@ -0,0 +1,169 @@
+/*
+ * 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 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 ] , [ overflow ]
+ * start    = "start" -> number
+ * overflow = "overflow" -> boolean
+ * </pre>
+ *
+ * Unless provided, <tt>start</tt> and <tt>overflow</tt> are respectively set to
+ * zero and <tt>true</tt> by default.
+ * <p>
+ * When <tt>overflow</tt> is enabled, 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.
+ *
+ * <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",
+ *   "overflow": false
+ * }
+ * </pre>
+ */
+public class CounterResolver implements EventResolver {
+
+    private final Consumer<JsonWriter> delegate;
+
+    public CounterResolver(final TemplateResolverConfig config) {
+        final BigInteger start = readStart(config);
+        final boolean overflow = config.getBoolean("overflow", true);
+        this.delegate = overflow
+                ? 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 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..eb1daec
--- /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.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.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 ignored,
+            final TemplateResolverConfig config) {
+        return new CounterResolver(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..25f02f1
--- /dev/null
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolverTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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 negative_start_should_work() {
+        final String eventTemplate = writeJson(asMap(
+                "$resolver", "counter",
+                "start", -3));
+        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 max_long_should_work_when_overflow_enabled() {
+        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_overflow_disabled() {
+        final String eventTemplate = writeJson(asMap(
+                "$resolver", "counter",
+                "start", Long.MAX_VALUE,
+                "overflow", false));
+        verify(
+                eventTemplate,
+                Long.MAX_VALUE,
+                BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE));
+    }
+
+    private static void verify(
+            final String eventTemplate,
+            final Number expectedNumber1,
+            final Number 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 bbddae2..dd1ecf0 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -31,6 +31,9 @@
     -->
     <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 768e222..77c2747 100644
--- a/src/site/asciidoc/manual/json-template-layout.adoc.vm
+++ b/src/site/asciidoc/manual/json-template-layout.adoc.vm
@@ -454,6 +454,62 @@ 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 ] , [ overflow ]
+start    = "start" -> number
+overflow = "overflow" -> boolean
+----
+
+Resolves a number from an internal counter.
+
+Unless provided, `start` and `overflow` are respectively set to zero and `true`
+by default.
+
+[WARNING]
+====
+When `overflow` is enabled, 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",
+  "overflow": false
+}
+----
+
 [#event-template-resolver-caseConverter]
 ===== `caseConverter`
 
@@ -501,8 +557,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