From d9eb350406f8a90f071f43cfff96f694e81dd94c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Fri, 18 Oct 2024 11:28:33 +0200
Subject: [PATCH 01/21] Move `InstantFormatter` to `log4j-core`
---
.../internal/InstantNumberFormatterTest.java | 70 ++++
.../InstantPatternDynamicFormatterTest.java | 184 +++++++++
...PatternThreadLocalCachedFormatterTest.java | 292 ++++++++++++++
.../core/pattern/DatePatternConverter.java | 379 ++++++------------
.../log4j/core/util/datetime/DatePrinter.java | 2 +
.../core/util/datetime/FastDateFormat.java | 2 +
.../core/util/datetime/FastDatePrinter.java | 2 +
.../core/util/datetime/FixedDateFormat.java | 55 +--
.../log4j/core/util/datetime/Format.java | 2 +
.../log4j/core/util/datetime/FormatCache.java | 2 +
.../core/util/datetime/package-info.java | 10 +-
.../core/util/internal/InstantFormatter.java | 32 +-
.../util/internal/InstantNumberFormatter.java | 122 ++++++
.../InstantPatternDynamicFormatter.java | 335 ++++++++++++++++
.../internal/InstantPatternFormatter.java | 124 ++++++
...tantPatternThreadLocalCachedFormatter.java | 136 +++++++
.../layout/template/json/LogEventFixture.java | 27 +-
.../layout/template/json/GelfLayoutTest.java | 22 +-
.../json/util/InstantFormatterTest.java | 114 ------
.../json/resolver/TimestampResolver.java | 269 ++++---------
.../template/json/util/InstantFormatter.java | 3 +
.../perf/jmh/DateTimeFormatBenchmark.java | 149 ++++---
.../jmh/DateTimeFormatImpactBenchmark.java | 156 +++++++
23 files changed, 1824 insertions(+), 665 deletions(-)
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatterTest.java
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatterTest.java
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatterTest.java
rename log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java => log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantFormatter.java (53%)
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatter.java
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatter.java
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternFormatter.java
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatter.java
delete mode 100644 log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/InstantFormatterTest.java
create mode 100644 log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatImpactBenchmark.java
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatterTest.java
new file mode 100644
index 00000000000..f3c11e3e1aa
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatterTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.core.util.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class InstantNumberFormatterTest {
+
+ @ParameterizedTest
+ @MethodSource("testCases")
+ void should_produce_expected_output(
+ final InstantFormatter formatter, final Instant instant, final String expectedOutput) {
+ final String actualOutput = formatter.format(instant);
+ assertThat(actualOutput).isEqualTo(expectedOutput);
+ }
+
+ static Stream
*
* @since Apache Commons Lang 3.2
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
*/
+@Deprecated
public interface DatePrinter {
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
index 8cd06c45708..c14a296da0f 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
@@ -69,7 +69,9 @@
*
*
* @since Apache Commons Lang 2.0
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
*/
+@Deprecated
public class FastDateFormat extends Format implements DatePrinter {
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDatePrinter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDatePrinter.java
index 96541cd57cd..6345f485499 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDatePrinter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDatePrinter.java
@@ -78,7 +78,9 @@
*
*
* @since Apache Commons Lang 3.2
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
*/
+@Deprecated
public class FastDatePrinter implements DatePrinter, Serializable {
// A lot of the speed in this class comes from caching, but some comes
// from the special int to StringBuffer conversion.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
index 9a0d7e2cd64..aba0baaf2ee 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
@@ -16,8 +16,10 @@
*/
package org.apache.logging.log4j.core.util.datetime;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.Arrays;
-import java.util.Calendar;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
@@ -27,11 +29,10 @@
/**
* Custom time formatter that trades flexibility for performance. This formatter only supports the date patterns defined
* in {@link FixedFormat}. For any other date patterns use {@link FastDateFormat}.
- *
- * Related benchmarks: /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and
- * /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
- *
+ *
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
*/
+@Deprecated
@ProviderType
public class FixedDateFormat {
@@ -48,13 +49,13 @@ public enum FixedFormat {
*/
ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1, 3, null),
/**
- * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,nnnnnn"}.
+ * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,SSSSSS"}.
*/
- ABSOLUTE_MICROS("HH:mm:ss,nnnnnn", null, 0, ':', 1, ',', 1, 6, null),
+ ABSOLUTE_MICROS("HH:mm:ss,SSSSSS", null, 0, ':', 1, ',', 1, 6, null),
/**
- * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,nnnnnnnnn"}.
+ * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,SSSSSSSSS"}.
*/
- ABSOLUTE_NANOS("HH:mm:ss,nnnnnnnnn", null, 0, ':', 1, ',', 1, 9, null),
+ ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS", null, 0, ':', 1, ',', 1, 9, null),
/**
* ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}.
@@ -81,13 +82,13 @@ public enum FixedFormat {
*/
DEFAULT("yyyy-MM-dd HH:mm:ss,SSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 3, null),
/**
- * DEFAULT time format with microsecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnn"}.
+ * DEFAULT time format with microsecond precision: {@code "yyyy-MM-dd HH:mm:ss,SSSSSS"}.
*/
- DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,nnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 6, null),
+ DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 6, null),
/**
- * DEFAULT time format with nanosecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"}.
+ * DEFAULT time format with nanosecond precision: {@code "yyyy-MM-dd HH:mm:ss,SSSSSSSSS"}.
*/
- DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,nnnnnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 9, null),
+ DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 9, null),
/**
* DEFAULT time format variation with period separator: {@code "yyyy-MM-dd HH:mm:ss.SSS"}.
@@ -142,9 +143,9 @@ public enum FixedFormat {
ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 3, null),
/**
- * ISO8601 time format with support for microsecond precision: {@code "yyyy-MM-dd'T'HH:mm:ss.nnnnnn"}.
+ * ISO8601 time format with support for microsecond precision: {@code "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"}.
*/
- ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.nnnnnn", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 6, null),
+ ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 6, null),
/**
* American date/time format with 2-digit year: {@code "dd/MM/yy HH:mm:ss.SSS"}.
@@ -158,7 +159,7 @@ public enum FixedFormat {
private static final String DEFAULT_SECOND_FRACTION_PATTERN = "SSS";
private static final int MILLI_FRACTION_DIGITS = DEFAULT_SECOND_FRACTION_PATTERN.length();
- private static final char SECOND_FRACTION_PATTERN = 'n';
+ private static final char SECOND_FRACTION_PATTERN = 'S';
private final String pattern;
private final String datePattern;
@@ -572,22 +573,22 @@ private void updateMidnightMillis(final long now) {
}
private long calcMidnightMillis(final long time, final int addDays) {
- final Calendar cal = Calendar.getInstance(timeZone);
- cal.setTimeInMillis(time);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- cal.add(Calendar.DATE, addDays);
- return cal.getTimeInMillis();
+ final ZoneId zoneId = timeZone.toZoneId();
+ final ZonedDateTime zonedInstant = java.time.Instant.ofEpochMilli(time).atZone(zoneId);
+ return LocalDate.from(zonedInstant)
+ .atStartOfDay()
+ .plusDays(addDays)
+ .atZone(zoneId)
+ .toInstant()
+ .toEpochMilli();
}
private void updateDaylightSavingTime() {
Arrays.fill(dstOffsets, 0);
- final int ONE_HOUR = (int) TimeUnit.HOURS.toMillis(1);
- if (timeZone.getOffset(midnightToday) != timeZone.getOffset(midnightToday + 23 * ONE_HOUR)) {
+ final long oneHourMillis = TimeUnit.HOURS.toMillis(1);
+ if (timeZone.getOffset(midnightToday) != timeZone.getOffset(midnightToday + 23 * oneHourMillis)) {
for (int i = 0; i < dstOffsets.length; i++) {
- final long time = midnightToday + i * ONE_HOUR;
+ final long time = midnightToday + i * oneHourMillis;
dstOffsets[i] = timeZone.getOffset(time) - timeZone.getRawOffset();
}
if (dstOffsets[0] > dstOffsets[23]) { // clock is moved backwards.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
index b77cfc7adf6..c51bc58a921 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
@@ -20,7 +20,9 @@
/**
* The basic methods for performing date formatting.
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
*/
+@Deprecated
public abstract class Format {
public final String format(final Object obj) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FormatCache.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FormatCache.java
index 5434a3413cc..c179b605027 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FormatCache.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FormatCache.java
@@ -32,8 +32,10 @@
*
*
* @since Apache Commons Lang 3.0
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
*/
// TODO: Before making public move from getDateTimeInstance(Integer,...) to int; or some other approach.
+@Deprecated
abstract class FormatCache {
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
index dd5eea74619..cb21f6148dd 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
@@ -14,14 +14,16 @@
* See the license for the specific language governing permissions and
* limitations under the license.
*/
+
/**
- * Log4j 2 date formatting classes.
+ * Log4j date & time formatting classes.
+ *
+ * @deprecated Starting with version {@code 2.25.0}, these classes are assumed to be internal and planned to be moved to an internal package in the next major release.
*/
+@Deprecated
@Export
-@Version("2.21.1")
-@BaselineIgnore("2.22.0")
+@Version("2.21.2")
package org.apache.logging.log4j.core.util.datetime;
-import aQute.bnd.annotation.baseline.BaselineIgnore;
import org.osgi.annotation.bundle.Export;
import org.osgi.annotation.versioning.Version;
diff --git a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantFormatter.java
similarity index 53%
rename from log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantFormatter.java
index d4d9924283c..35c6c190dc9 100644
--- a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantFormatter.java
@@ -14,9 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@Export
-@Version("2.20.1")
-package org.apache.logging.log4j.layout.template.json;
+package org.apache.logging.log4j.core.util.internal;
-import org.osgi.annotation.bundle.Export;
-import org.osgi.annotation.versioning.Version;
+import static java.util.Objects.requireNonNull;
+
+import java.time.temporal.ChronoUnit;
+import org.apache.logging.log4j.core.time.Instant;
+
+/**
+ * Contract for formatting {@link Instant}s.
+ *
+ * @since 2.25.0
+ */
+public interface InstantFormatter {
+
+ /**
+ * @return the time precision of the formatted output
+ */
+ ChronoUnit getPrecision();
+
+ default String format(final Instant instant) {
+ requireNonNull(instant, "instant");
+ final StringBuilder buffer = new StringBuilder();
+ formatTo(buffer, instant);
+ return buffer.toString();
+ }
+
+ void formatTo(StringBuilder buffer, Instant instant);
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatter.java
new file mode 100644
index 00000000000..216cd0779fb
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatter.java
@@ -0,0 +1,122 @@
+/*
+ * 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.core.util.internal;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.temporal.ChronoUnit;
+import java.util.function.BiConsumer;
+import org.apache.logging.log4j.core.time.Instant;
+
+/**
+ * Formats an {@link Instant} numerically; e.g., formats its epoch1 seconds.
+ *
+ * 1 Epoch is a fixed instant on {@code 1970-01-01Z}.
+ *
+ *
+ * @since 2.25.0
+ */
+public enum InstantNumberFormatter implements InstantFormatter {
+
+ /**
+ * Formats nanoseconds since epoch; e.g., {@code 1581082727982123456}.
+ */
+ EPOCH_NANOS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long nanos = epochNanos(instant);
+ buffer.append(nanos);
+ }),
+
+ /**
+ * Formats milliseconds since epoch, including the nanosecond fraction; e.g., {@code 1581082727982.123456}.
+ * The nanosecond fraction will be skipped if it is zero.
+ */
+ EPOCH_MILLIS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long nanos = epochNanos(instant);
+ buffer.append(nanos);
+ buffer.insert(buffer.length() - 6, '.');
+ }),
+
+ /**
+ * Formats milliseconds since epoch, excluding the nanosecond fraction; e.g., {@code 1581082727982}.
+ */
+ EPOCH_MILLIS_ROUNDED(ChronoUnit.MILLIS, (instant, buffer) -> {
+ final long millis = instant.getEpochMillisecond();
+ buffer.append(millis);
+ }),
+
+ /**
+ * Formats the nanosecond fraction of milliseconds since epoch; e.g., {@code 123456}.
+ */
+ EPOCH_MILLIS_NANOS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long nanos = epochNanos(instant);
+ final long fraction = nanos % 1_000_000L;
+ buffer.append(fraction);
+ }),
+
+ /**
+ * Formats seconds since epoch, including the nanosecond fraction; e.g., {@code 1581082727.982123456}.
+ * The nanosecond fraction will be skipped if it is zero.
+ */
+ EPOCH_SECONDS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long nanos = epochNanos(instant);
+ buffer.append(nanos);
+ buffer.insert(buffer.length() - 9, '.');
+ }),
+
+ /**
+ * Formats seconds since epoch, excluding the nanosecond fraction; e.g., {@code 1581082727}.
+ * The nanosecond fraction will be skipped if it is zero.
+ */
+ EPOCH_SECONDS_ROUNDED(ChronoUnit.SECONDS, (instant, buffer) -> {
+ final long seconds = instant.getEpochSecond();
+ buffer.append(seconds);
+ }),
+
+ /**
+ * Formats the nanosecond fraction of seconds since epoch; e.g., {@code 982123456}.
+ */
+ EPOCH_SECONDS_NANOS(ChronoUnit.NANOS, (instant, buffer) -> {
+ final long secondsNanos = instant.getNanoOfSecond();
+ buffer.append(secondsNanos);
+ });
+
+ private static long epochNanos(final Instant instant) {
+ final long nanos = Math.multiplyExact(1_000_000_000L, instant.getEpochSecond());
+ return Math.addExact(nanos, instant.getNanoOfSecond());
+ }
+
+ private final ChronoUnit precision;
+
+ private final BiConsumer formatter;
+
+ InstantNumberFormatter(final ChronoUnit precision, final BiConsumer formatter) {
+ this.precision = precision;
+ this.formatter = formatter;
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return precision;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ requireNonNull(buffer, "buffer");
+ requireNonNull(instant, "instant");
+ formatter.accept(instant, buffer);
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatter.java
new file mode 100644
index 00000000000..79de1c9db63
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatter.java
@@ -0,0 +1,335 @@
+/*
+ * 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.core.util.internal;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.TimeZone;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.core.util.datetime.FastDateFormat;
+import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Formats an {@link Instant} using the specified date & time formatting pattern.
+ * This is a composite formatter trying to employ either {@link FixedDateFormat}, {@link FastDateFormat} or {@link DateTimeFormatter} in the given order due to performance reasons.
+ *
+ * If the given pattern happens to be supported by either {@link FixedDateFormat} or {@link FastDateFormat}, yet they produce a different result compared to {@link DateTimeFormatter}, {@link DateTimeFormatter} will be employed instead.
+ * Note that {@link FastDateFormat} supports at most millisecond precision.
+ *
+ *
+ * @since 2.25.0
+ */
+@NullMarked
+final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
+
+ private static final StatusLogger LOGGER = StatusLogger.getLogger();
+
+ /**
+ * The list of formatter factories in decreasing efficiency order.
+ */
+ private static final FormatterFactory[] FORMATTER_FACTORIES = {
+ new Log4jFixedFormatterFactory(), new Log4jFastFormatterFactory(), new JavaDateTimeFormatterFactory()
+ };
+
+ private final String pattern;
+
+ private final Locale locale;
+
+ private final TimeZone timeZone;
+
+ private final Formatter formatter;
+
+ InstantPatternDynamicFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
+ this.pattern = pattern;
+ this.locale = locale;
+ this.timeZone = timeZone;
+ this.formatter = Arrays.stream(FORMATTER_FACTORIES)
+ .map(formatterFactory -> {
+ try {
+ return formatterFactory.createIfSupported(pattern, locale, timeZone);
+ } catch (final Exception error) {
+ LOGGER.warn("skipping the failed formatter factory `{}`", formatterFactory, error);
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("could not find a matching formatter"));
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ requireNonNull(buffer, "buffer");
+ requireNonNull(instant, "instant");
+ formatter.formatTo(buffer, instant);
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return formatter.precision;
+ }
+
+ Class> getInternalImplementationClass() {
+ return formatter.getInternalImplementationClass();
+ }
+
+ @Override
+ public String getPattern() {
+ return pattern;
+ }
+
+ @Override
+ public Locale getLocale() {
+ return locale;
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(InstantPatternDynamicFormatter.class.getSimpleName()).append('{');
+ buffer.append("pattern=`").append(pattern).append('`');
+ if (!locale.equals(Locale.getDefault())) {
+ buffer.append(",locale=`").append(locale).append('`');
+ }
+ if (!timeZone.equals(TimeZone.getDefault())) {
+ buffer.append(",timeZone=`").append(timeZone.getID()).append('`');
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ private interface FormatterFactory {
+
+ @Nullable
+ Formatter createIfSupported(String pattern, Locale locale, TimeZone timeZone);
+ }
+
+ private abstract static class Formatter {
+
+ private final ChronoUnit precision;
+
+ Formatter(final ChronoUnit precision) {
+ this.precision = precision;
+ }
+
+ abstract Class> getInternalImplementationClass();
+
+ abstract void formatTo(StringBuilder buffer, Instant instant);
+ }
+
+ private static final class JavaDateTimeFormatterFactory implements FormatterFactory {
+
+ @Override
+ public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
+ return new JavaDateTimeFormatter(pattern, locale, timeZone);
+ }
+ }
+
+ private static final class JavaDateTimeFormatter extends Formatter {
+
+ private final DateTimeFormatter formatter;
+
+ private final MutableInstant mutableInstant;
+
+ private JavaDateTimeFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
+ super(patternPrecision(pattern));
+ this.formatter =
+ DateTimeFormatter.ofPattern(pattern).withLocale(locale).withZone(timeZone.toZoneId());
+ this.mutableInstant = new MutableInstant();
+ }
+
+ @Override
+ public Class> getInternalImplementationClass() {
+ return DateTimeFormatter.class;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ if (instant instanceof MutableInstant) {
+ formatMutableInstant((MutableInstant) instant, buffer);
+ } else {
+ formatInstant(instant, buffer);
+ }
+ }
+
+ private void formatMutableInstant(final MutableInstant instant, final StringBuilder buffer) {
+ formatter.formatTo(instant, buffer);
+ }
+
+ private void formatInstant(final Instant instant, final StringBuilder buffer) {
+ mutableInstant.initFrom(instant);
+ formatMutableInstant(mutableInstant, buffer);
+ }
+ }
+
+ private static final class Log4jFastFormatterFactory implements FormatterFactory {
+
+ @Override
+ public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
+ final Log4jFastFormatter formatter = new Log4jFastFormatter(pattern, locale, timeZone);
+ final boolean patternSupported = patternSupported(pattern, locale, timeZone, formatter);
+ return patternSupported ? formatter : null;
+ }
+ }
+
+ private static final class Log4jFastFormatter extends Formatter {
+
+ private final FastDateFormat formatter;
+
+ private final Supplier calendarSupplier;
+
+ private Log4jFastFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
+ super(effectivePatternPrecision(pattern));
+ this.formatter = FastDateFormat.getInstance(pattern, timeZone, locale);
+ this.calendarSupplier = memoryEfficientInstanceSupplier(() -> Calendar.getInstance(timeZone, locale));
+ }
+
+ @Override
+ public Class> getInternalImplementationClass() {
+ return FastDateFormat.class;
+ }
+
+ private static ChronoUnit effectivePatternPrecision(final String pattern) {
+ final ChronoUnit patternPrecision = patternPrecision(pattern);
+ // `FastDateFormat` doesn't support precision higher than millisecond
+ return ChronoUnit.MILLIS.compareTo(patternPrecision) > 0 ? ChronoUnit.MILLIS : patternPrecision;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder stringBuilder, final Instant instant) {
+ final Calendar calendar = calendarSupplier.get();
+ calendar.setTimeInMillis(instant.getEpochMillisecond());
+ formatter.format(calendar, stringBuilder);
+ }
+ }
+
+ private static final class Log4jFixedFormatterFactory implements FormatterFactory {
+
+ @Override
+ @Nullable
+ public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
+ final FixedDateFormat internalFormatter = FixedDateFormat.createIfSupported(pattern, timeZone.getID());
+ if (internalFormatter == null) {
+ return null;
+ }
+ final Log4jFixedFormatter formatter = new Log4jFixedFormatter(internalFormatter);
+ final boolean patternSupported = patternSupported(pattern, locale, timeZone, formatter);
+ return patternSupported ? formatter : null;
+ }
+ }
+
+ private static final class Log4jFixedFormatter extends Formatter {
+
+ private final FixedDateFormat formatter;
+
+ private final Supplier bufferSupplier;
+
+ private Log4jFixedFormatter(final FixedDateFormat formatter) {
+ super(patternPrecision(formatter.getFormat()));
+ this.formatter = formatter;
+ this.bufferSupplier = memoryEfficientInstanceSupplier(() -> {
+ // Double size for locales with lengthy `DateFormatSymbols`
+ return new char[formatter.getLength() << 1];
+ });
+ }
+
+ @Override
+ public Class> getInternalImplementationClass() {
+ return FixedDateFormat.class;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ final char[] charBuffer = bufferSupplier.get();
+ final int length = formatter.formatInstant(instant, charBuffer, 0);
+ buffer.append(charBuffer, 0, length);
+ }
+ }
+
+ private static Supplier memoryEfficientInstanceSupplier(final Supplier supplier) {
+ return Constants.ENABLE_THREADLOCALS ? ThreadLocal.withInitial(supplier)::get : supplier;
+ }
+
+ /**
+ * Checks if the provided formatter output matches with the one generated by {@link DateTimeFormatter}.
+ */
+ private static boolean patternSupported(
+ final String pattern, final Locale locale, final TimeZone timeZone, final Formatter formatter) {
+ final DateTimeFormatter javaFormatter =
+ DateTimeFormatter.ofPattern(pattern).withLocale(locale).withZone(timeZone.toZoneId());
+ final MutableInstant instant = new MutableInstant();
+ instant.initFromEpochSecond(
+ // 2021-05-17 21:41:10
+ 1_621_280_470,
+ // Using the highest nanosecond precision possible to
+ // differentiate formatters only supporting millisecond
+ // precision.
+ 123_456_789);
+ final String expectedFormat = javaFormatter.format(instant);
+ final StringBuilder buffer = new StringBuilder();
+ formatter.formatTo(buffer, instant);
+ final String actualFormat = buffer.toString();
+ return expectedFormat.equals(actualFormat);
+ }
+
+ /**
+ * @param pattern a date & time formatting pattern
+ * @return the time precision of the output when formatted using the specified {@code pattern}
+ */
+ static ChronoUnit patternPrecision(final String pattern) {
+ // Remove text blocks
+ final String trimmedPattern = pattern.replaceAll("'[^']*'", "");
+ // A single `S` (fraction-of-second) outputs nanosecond precision
+ if (trimmedPattern.matches(".*(? epochInstantExtractor;
+
+ private final ThreadLocal epochInstantAndBufferRef =
+ ThreadLocal.withInitial(InstantPatternThreadLocalCachedFormatter::createEpochInstantAndBuffer);
+
+ private Object[] lastEpochInstantAndBuffer = createEpochInstantAndBuffer();
+
+ private static Object[] createEpochInstantAndBuffer() {
+ return new Object[] {-1L, new StringBuilder()};
+ }
+
+ private final ChronoUnit precision;
+
+ private InstantPatternThreadLocalCachedFormatter(
+ final InstantPatternFormatter formatter,
+ final Function epochInstantExtractor,
+ final ChronoUnit precision) {
+ this.formatter = formatter;
+ this.epochInstantExtractor = epochInstantExtractor;
+ this.precision = precision;
+ }
+
+ static InstantPatternThreadLocalCachedFormatter ofMilliPrecision(final InstantPatternFormatter formatter) {
+ final ChronoUnit precision = effectivePrecision(formatter, ChronoUnit.MILLIS);
+ return new InstantPatternThreadLocalCachedFormatter(formatter, Instant::getEpochMillisecond, precision);
+ }
+
+ static InstantPatternThreadLocalCachedFormatter ofSecondPrecision(final InstantPatternFormatter formatter) {
+ final ChronoUnit precision = effectivePrecision(formatter, ChronoUnit.SECONDS);
+ return new InstantPatternThreadLocalCachedFormatter(formatter, Instant::getEpochSecond, precision);
+ }
+
+ private static ChronoUnit effectivePrecision(final InstantFormatter formatter, final ChronoUnit cachePrecision) {
+ final ChronoUnit formatterPrecision = formatter.getPrecision();
+ final int comparison = cachePrecision.compareTo(formatterPrecision);
+ if (comparison == 0) {
+ return formatterPrecision;
+ } else if (comparison > 0) {
+ final String message = String.format(
+ "instant formatter `%s` is of `%s` precision, whereas the requested cache precision is `%s`",
+ formatter, formatterPrecision, cachePrecision);
+ throw new IllegalArgumentException(message);
+ } else {
+ return cachePrecision;
+ }
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return precision;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ requireNonNull(buffer, "buffer");
+ requireNonNull(instant, "instant");
+ final Object[] prevEpochInstantAndBuffer = lastEpochInstantAndBuffer;
+ final long prevEpochInstant = (long) prevEpochInstantAndBuffer[0];
+ final StringBuilder prevBuffer = (StringBuilder) prevEpochInstantAndBuffer[1];
+ final long nextEpochInstant = epochInstantExtractor.apply(instant);
+ if (prevEpochInstant == nextEpochInstant) {
+ buffer.append(prevBuffer);
+ } else {
+
+ // We could have used `StringBuilders.trimToMaxSize()` on `prevBuffer`.
+ // That is, we wouldn't want exploded `StringBuilder`s in hundreds of `ThreadLocal`s.
+ // Though we are formatting instants and always expect to produce strings of more or less the same length.
+ // Hence, no need for truncation.
+
+ // Populate a new cache entry
+ final Object[] nextEpochInstantAndBuffer = epochInstantAndBufferRef.get();
+ nextEpochInstantAndBuffer[0] = nextEpochInstant;
+ final StringBuilder nextBuffer = (StringBuilder) nextEpochInstantAndBuffer[1];
+ nextBuffer.setLength(0);
+ formatter.formatTo(nextBuffer, instant);
+
+ // Update the effective cache entry
+ lastEpochInstantAndBuffer = nextEpochInstantAndBuffer;
+
+ // Help out the request
+ buffer.append(nextBuffer);
+ }
+ }
+
+ @Override
+ public String getPattern() {
+ return formatter.getPattern();
+ }
+
+ @Override
+ public Locale getLocale() {
+ return formatter.getLocale();
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return formatter.getTimeZone();
+ }
+}
diff --git a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/LogEventFixture.java b/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/LogEventFixture.java
index 21daa068190..bc9a36094da 100644
--- a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/LogEventFixture.java
+++ b/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/LogEventFixture.java
@@ -29,18 +29,24 @@
import org.apache.logging.log4j.spi.ThreadContextStack;
import org.apache.logging.log4j.util.StringMap;
-final class LogEventFixture {
+public final class LogEventFixture {
private LogEventFixture() {}
private static final int TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT = 10;
- static List createLiteLogEvents(final int logEventCount) {
+ public static List createLiteLogEvents(final int logEventCount) {
+ return createLiteLogEvents(logEventCount, TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT);
+ }
+
+ public static List createLiteLogEvents(
+ final int logEventCount, final int timeOverlappingConsecutiveEventCount) {
final List logEvents = new ArrayList<>(logEventCount);
final long startTimeMillis = System.currentTimeMillis();
for (int logEventIndex = 0; logEventIndex < logEventCount; logEventIndex++) {
final String logEventId = String.valueOf(logEventIndex);
- final long logEventTimeMillis = createLogEventTimeMillis(startTimeMillis, logEventIndex);
+ final long logEventTimeMillis =
+ createLogEventTimeMillis(startTimeMillis, logEventIndex, timeOverlappingConsecutiveEventCount);
final LogEvent logEvent = LogEventFixture.createLiteLogEvent(logEventId, logEventTimeMillis);
logEvents.add(logEvent);
}
@@ -63,24 +69,31 @@ private static LogEvent createLiteLogEvent(final String id, final long timeMilli
.build();
}
- static List createFullLogEvents(final int logEventCount) {
+ public static List createFullLogEvents(final int logEventCount) {
+ return createFullLogEvents(logEventCount, TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT);
+ }
+
+ public static List createFullLogEvents(
+ final int logEventCount, final int timeOverlappingConsecutiveEventCount) {
final List logEvents = new ArrayList<>(logEventCount);
final long startTimeMillis = System.currentTimeMillis();
for (int logEventIndex = 0; logEventIndex < logEventCount; logEventIndex++) {
final String logEventId = String.valueOf(logEventIndex);
- final long logEventTimeMillis = createLogEventTimeMillis(startTimeMillis, logEventIndex);
+ final long logEventTimeMillis =
+ createLogEventTimeMillis(startTimeMillis, logEventIndex, timeOverlappingConsecutiveEventCount);
final LogEvent logEvent = LogEventFixture.createFullLogEvent(logEventId, logEventTimeMillis);
logEvents.add(logEvent);
}
return logEvents;
}
- private static long createLogEventTimeMillis(final long startTimeMillis, final int logEventIndex) {
+ private static long createLogEventTimeMillis(
+ final long startTimeMillis, final int logEventIndex, final int timeOverlappingConsecutiveEventCount) {
// Create event time repeating every certain number of consecutive
// events. This is better aligned with the real-world use case and
// gives surface to timestamp formatter caches to perform their
// magic, which is implemented for almost all layouts.
- return startTimeMillis + logEventIndex / TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT;
+ return startTimeMillis + logEventIndex / timeOverlappingConsecutiveEventCount;
}
private static LogEvent createFullLogEvent(final String id, final long timeMillis) {
diff --git a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/GelfLayoutTest.java b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/GelfLayoutTest.java
index 17ceba6791a..6454c89546a 100644
--- a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/GelfLayoutTest.java
+++ b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/GelfLayoutTest.java
@@ -18,7 +18,6 @@
import static org.apache.logging.log4j.layout.template.json.TestHelpers.serializeUsingLayout;
-import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -94,16 +93,15 @@ private static void verifyTimestamp(
final Instant logEventInstant,
final Map jsonTemplateLayoutMap,
final Map gelfLayoutMap) {
- final BigDecimal jsonTemplateLayoutTimestamp = (BigDecimal) jsonTemplateLayoutMap.remove("timestamp");
- final BigDecimal gelfLayoutTimestamp = (BigDecimal) gelfLayoutMap.remove("timestamp");
- final String description = String.format(
- "instantEpochSecs=%d.%d, jsonTemplateLayoutTimestamp=%s, gelfLayoutTimestamp=%s",
- logEventInstant.getEpochSecond(),
- logEventInstant.getNanoOfSecond(),
- jsonTemplateLayoutTimestamp,
- gelfLayoutTimestamp);
- Assertions.assertThat(jsonTemplateLayoutTimestamp.compareTo(gelfLayoutTimestamp))
- .as(description)
- .isEqualTo(0);
+ final Number jsonTemplateLayoutTimestamp = (Number) jsonTemplateLayoutMap.remove("timestamp");
+ final Number gelfLayoutTimestamp = (Number) gelfLayoutMap.remove("timestamp");
+ Assertions.assertThat(jsonTemplateLayoutTimestamp.doubleValue())
+ .as(
+ "instantEpochSecs=%d.%d, jsonTemplateLayoutTimestamp=%s, gelfLayoutTimestamp=%s",
+ logEventInstant.getEpochSecond(),
+ logEventInstant.getNanoOfSecond(),
+ jsonTemplateLayoutTimestamp,
+ gelfLayoutTimestamp)
+ .isEqualTo(gelfLayoutTimestamp.doubleValue());
}
}
diff --git a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/InstantFormatterTest.java b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/InstantFormatterTest.java
deleted file mode 100644
index 87df6556988..00000000000
--- a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/InstantFormatterTest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.util;
-
-import java.util.Locale;
-import java.util.TimeZone;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.datetime.FastDateFormat;
-import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
-import org.apache.logging.log4j.test.ListStatusListener;
-import org.apache.logging.log4j.test.junit.UsingStatusListener;
-import org.assertj.core.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
-
-class InstantFormatterTest {
-
- @ParameterizedTest
- @CsvSource({
- "yyyy-MM-dd'T'HH:mm:ss.SSS" + ",FixedDateFormat",
- "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + ",FastDateFormat",
- "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'" + ",DateTimeFormatter"
- })
- void all_internal_implementations_should_be_used(final String pattern, final String className) {
- final InstantFormatter formatter =
- InstantFormatter.newBuilder().setPattern(pattern).build();
- Assertions.assertThat(formatter.getInternalImplementationClass())
- .asString()
- .describedAs("pattern=%s", pattern)
- .endsWith("." + className);
- }
-
- @Test
- void nanoseconds_should_be_formatted() {
- final InstantFormatter formatter = InstantFormatter.newBuilder()
- .setPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'")
- .setTimeZone(TimeZone.getTimeZone("UTC"))
- .build();
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(0, 123_456_789);
- Assertions.assertThat(formatter.format(instant)).isEqualTo("1970-01-01T00:00:00.123456789Z");
- }
-
- /**
- * Reproduces LOG4J2-3614.
- */
- @Test
- void FastDateFormat_failures_should_be_handled() {
-
- // Define a pattern causing `FastDateFormat` to fail.
- final String pattern = "ss.nnnnnnnnn";
- final TimeZone timeZone = TimeZone.getTimeZone("UTC");
- final Locale locale = Locale.US;
-
- // Assert that the pattern is not supported by `FixedDateFormat`.
- final FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(pattern, timeZone.getID());
- Assertions.assertThat(fixedDateFormat).isNull();
-
- // Assert that the pattern indeed causes a `FastDateFormat` failure.
- Assertions.assertThatThrownBy(() -> FastDateFormat.getInstance(pattern, timeZone, locale))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Illegal pattern component: nnnnnnnnn");
-
- // Assert that `InstantFormatter` falls back to `DateTimeFormatter`.
- final InstantFormatter formatter = InstantFormatter.newBuilder()
- .setPattern(pattern)
- .setTimeZone(timeZone)
- .build();
- Assertions.assertThat(formatter.getInternalImplementationClass())
- .asString()
- .endsWith(".DateTimeFormatter");
-
- // Assert that formatting works.
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(0, 123_456_789);
- Assertions.assertThat(formatter.format(instant)).isEqualTo("00.123456789");
- }
-
- @Test
- @UsingStatusListener
- void FixedFormatter_large_enough_buffer(ListStatusListener listener) {
- final String pattern = "yyyy-MM-dd'T'HH:mm:ss,SSSXXX";
- final TimeZone timeZone = TimeZone.getTimeZone("America/Chicago");
- final Locale locale = Locale.ENGLISH;
- final InstantFormatter formatter = InstantFormatter.newBuilder()
- .setPattern(pattern)
- .setTimeZone(timeZone)
- .setLocale(locale)
- .build();
-
- // On this pattern the FixedFormatter used a buffer shorter than necessary,
- // which caused exceptions and warnings.
- Assertions.assertThat(listener.findStatusData(Level.WARN)).hasSize(0);
- Assertions.assertThat(formatter.getInternalImplementationClass())
- .asString()
- .endsWith(".FixedDateFormat");
- }
-}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
index f023f603aa3..489bdda1fab 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
@@ -18,13 +18,12 @@
import java.util.Locale;
import java.util.TimeZone;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.time.Instant;
-import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.internal.InstantFormatter;
+import org.apache.logging.log4j.core.util.internal.InstantNumberFormatter;
+import org.apache.logging.log4j.core.util.internal.InstantPatternFormatter;
import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
-import org.apache.logging.log4j.layout.template.json.util.InstantFormatter;
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
/**
@@ -55,15 +54,14 @@
* rounded = "rounded" -> boolean
*
*
- * If no configuration options are provided, pattern-config is
- * employed. There {@link
- * JsonTemplateLayoutDefaults#getTimestampFormatPattern()}, {@link
- * JsonTemplateLayoutDefaults#getTimeZone()}, {@link
- * JsonTemplateLayoutDefaults#getLocale()} are used as defaults for
- * pattern, timeZone, and locale, respectively.
+ *
+ * If no configuration options are provided, pattern-config is employed.
+ * There {@link JsonTemplateLayoutDefaults#getTimestampFormatPattern()}, {@link JsonTemplateLayoutDefaults#getTimeZone()}, {@link JsonTemplateLayoutDefaults#getLocale()} are used as defaults for pattern, timeZone, and locale, respectively.
+ *
*
- * In epoch-config, millis.nanos, secs.nanos stand
- * for the fractional component in nanoseconds.
+ *
+ * In epoch-config, millis.nanos, secs.nanos stand for the fractional component in nanoseconds.
+ *
*
*
Examples
*
@@ -209,109 +207,53 @@ private static EventResolver createResolver(final TemplateResolverConfig config)
return epochProvided ? createEpochResolver(config) : createPatternResolver(config);
}
- private static final class PatternResolverContext {
-
- private final InstantFormatter formatter;
-
- private final StringBuilder lastFormattedInstantBuffer = new StringBuilder();
-
- private final MutableInstant lastFormattedInstant = new MutableInstant();
-
- private PatternResolverContext(final String pattern, final TimeZone timeZone, final Locale locale) {
- this.formatter = InstantFormatter.newBuilder()
- .setPattern(pattern)
- .setTimeZone(timeZone)
- .setLocale(locale)
- .build();
- lastFormattedInstant.initFromEpochSecond(-1, 0);
- }
+ private static EventResolver createPatternResolver(final TemplateResolverConfig config) {
+ final String pattern = readPattern(config);
+ final TimeZone timeZone = readTimeZone(config);
+ final Locale locale = config.getLocale(new String[] {"pattern", "locale"});
+ final InstantFormatter formatter = InstantPatternFormatter.newBuilder()
+ .setPattern(pattern)
+ .setTimeZone(timeZone)
+ .setLocale(locale)
+ .build();
+ return new PatternResolver(formatter);
+ }
- private static PatternResolverContext fromConfig(final TemplateResolverConfig config) {
- final String pattern = readPattern(config);
- final TimeZone timeZone = readTimeZone(config);
- final Locale locale = config.getLocale(new String[] {"pattern", "locale"});
- return new PatternResolverContext(pattern, timeZone, locale);
- }
+ private static String readPattern(final TemplateResolverConfig config) {
+ final String format = config.getString(new String[] {"pattern", "format"});
+ return format != null ? format : JsonTemplateLayoutDefaults.getTimestampFormatPattern();
+ }
- private static String readPattern(final TemplateResolverConfig config) {
- final String format = config.getString(new String[] {"pattern", "format"});
- return format != null ? format : JsonTemplateLayoutDefaults.getTimestampFormatPattern();
+ private static TimeZone readTimeZone(final TemplateResolverConfig config) {
+ final String timeZoneId = config.getString(new String[] {"pattern", "timeZone"});
+ if (timeZoneId == null) {
+ return JsonTemplateLayoutDefaults.getTimeZone();
}
-
- private static TimeZone readTimeZone(final TemplateResolverConfig config) {
- final String timeZoneId = config.getString(new String[] {"pattern", "timeZone"});
- if (timeZoneId == null) {
- return JsonTemplateLayoutDefaults.getTimeZone();
- }
- boolean found = false;
- for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
- if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
- found = true;
- break;
- }
- }
- if (!found) {
- throw new IllegalArgumentException("invalid timestamp time zone: " + config);
+ boolean found = false;
+ for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
+ if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
+ found = true;
+ break;
}
- return TimeZone.getTimeZone(timeZoneId);
}
+ if (!found) {
+ throw new IllegalArgumentException("invalid timestamp time zone: " + config);
+ }
+ return TimeZone.getTimeZone(timeZoneId);
}
private static final class PatternResolver implements EventResolver {
- private final Lock lock = new ReentrantLock();
-
- private final PatternResolverContext patternResolverContext;
+ private final InstantFormatter formatter;
- private PatternResolver(final PatternResolverContext patternResolverContext) {
- this.patternResolverContext = patternResolverContext;
+ private PatternResolver(final InstantFormatter formatter) {
+ this.formatter = formatter;
}
@Override
public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
- lock.lock();
- try {
- unsynchronizedResolve(logEvent, jsonWriter);
- } finally {
- lock.unlock();
- }
+ jsonWriter.writeString(formatter::formatTo, logEvent.getInstant());
}
-
- private void unsynchronizedResolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
-
- // Format timestamp if it doesn't match the last cached one.
- final boolean instantMatching = patternResolverContext.formatter.isInstantMatching(
- patternResolverContext.lastFormattedInstant, logEvent.getInstant());
- if (!instantMatching) {
-
- // Format the timestamp.
- patternResolverContext.lastFormattedInstantBuffer.setLength(0);
- patternResolverContext.lastFormattedInstant.initFrom(logEvent.getInstant());
- patternResolverContext.formatter.format(
- patternResolverContext.lastFormattedInstant, patternResolverContext.lastFormattedInstantBuffer);
-
- // Write the formatted timestamp.
- final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
- final int startIndex = jsonWriterStringBuilder.length();
- jsonWriter.writeString(patternResolverContext.lastFormattedInstantBuffer);
-
- // Cache the written value.
- patternResolverContext.lastFormattedInstantBuffer.setLength(0);
- patternResolverContext.lastFormattedInstantBuffer.append(
- jsonWriterStringBuilder, startIndex, jsonWriterStringBuilder.length());
-
- }
-
- // Write the cached formatted timestamp.
- else {
- jsonWriter.writeRawString(patternResolverContext.lastFormattedInstantBuffer);
- }
- }
- }
-
- private static EventResolver createPatternResolver(final TemplateResolverConfig config) {
- final PatternResolverContext patternResolverContext = PatternResolverContext.fromConfig(config);
- return new PatternResolver(patternResolverContext);
}
private static EventResolver createEpochResolver(final TemplateResolverConfig config) {
@@ -331,119 +273,48 @@ private static EventResolver createEpochResolver(final TemplateResolverConfig co
throw new IllegalArgumentException("invalid epoch configuration: " + config);
}
- private static final class EpochResolutionRecord {
-
- private static final int MAX_LONG_LENGTH =
- String.valueOf(Long.MAX_VALUE).length();
-
- private final MutableInstant instant = new MutableInstant();
-
- private final char[] resolution =
- new char[ /* integral: */MAX_LONG_LENGTH + /* dot: */ 1 + /* fractional: */ MAX_LONG_LENGTH];
-
- private int resolutionLength;
-
- private EpochResolutionRecord() {
- instant.initFromEpochSecond(-1, 0);
- }
- }
-
- private abstract static class EpochResolver implements EventResolver {
-
- private final Lock lock = new ReentrantLock();
-
- private final EpochResolutionRecord resolutionRecord = new EpochResolutionRecord();
-
- @Override
- public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
- lock.lock();
- try {
- unsynchronizedResolve(logEvent, jsonWriter);
- } finally {
- lock.unlock();
- }
- }
-
- private void unsynchronizedResolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
- final Instant logEventInstant = logEvent.getInstant();
- if (logEventInstant.equals(resolutionRecord.instant)) {
- jsonWriter.writeRawString(resolutionRecord.resolution, 0, resolutionRecord.resolutionLength);
- } else {
- resolutionRecord.instant.initFrom(logEventInstant);
- final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
- final int startIndex = stringBuilder.length();
- resolve(logEventInstant, jsonWriter);
- resolutionRecord.resolutionLength = stringBuilder.length() - startIndex;
- stringBuilder.getChars(startIndex, stringBuilder.length(), resolutionRecord.resolution, 0);
- }
- }
-
- abstract void resolve(Instant logEventInstant, JsonWriter jsonWriter);
- }
-
- private static final EventResolver EPOCH_NANOS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final long nanos = epochNanos(logEventInstant);
- jsonWriter.writeNumber(nanos);
- }
+ private static final EventResolver EPOCH_NANOS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_NANOS.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_MILLIS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
- final long nanos = epochNanos(logEventInstant);
- jsonWriterStringBuilder.append(nanos);
- jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 6, '.');
- }
+ private static final EventResolver EPOCH_MILLIS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_MILLIS.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_MILLIS_ROUNDED_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
- }
+ private static final EventResolver EPOCH_MILLIS_ROUNDED_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_MILLIS_ROUNDED.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_MILLIS_NANOS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final long nanos = epochNanos(logEventInstant);
- final long fraction = nanos % 1_000_000L;
- jsonWriter.writeNumber(fraction);
- }
+ private static final EventResolver EPOCH_MILLIS_NANOS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_MILLIS_NANOS.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_SECS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
- final long nanos = epochNanos(logEventInstant);
- jsonWriterStringBuilder.append(nanos);
- jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 9, '.');
- }
+ private static final EventResolver EPOCH_SECS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_SECONDS.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_SECS_ROUNDED_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getEpochSecond());
- }
+ private static final EventResolver EPOCH_SECS_ROUNDED_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_SECONDS_ROUNDED.formatTo(buffer, instant);
};
- private static final EventResolver EPOCH_SECS_NANOS_RESOLVER = new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
- }
+ private static final EventResolver EPOCH_SECS_NANOS_RESOLVER = (logEvent, jsonWriter) -> {
+ final StringBuilder buffer = jsonWriter.getStringBuilder();
+ final Instant instant = logEvent.getInstant();
+ InstantNumberFormatter.EPOCH_SECONDS_NANOS.formatTo(buffer, instant);
};
- private static long epochNanos(final Instant instant) {
- final long nanos = Math.multiplyExact(1_000_000_000L, instant.getEpochSecond());
- return Math.addExact(nanos, instant.getNanoOfSecond());
- }
-
static String getName() {
return "timestamp";
}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/InstantFormatter.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/InstantFormatter.java
index c9a47d1f2c1..4b385da2315 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/InstantFormatter.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/InstantFormatter.java
@@ -37,7 +37,10 @@
* Note that {@link FixedDateFormat} and {@link FastDateFormat} only support
* millisecond precision. If the pattern asks for a higher precision,
* {@link DateTimeFormatter} will be employed, which is significantly slower.
+ *
+ * @deprecated Starting with version {@code 2.25.0}, this class is planned to be removed in the next major release.
*/
+@Deprecated
public final class InstantFormatter {
private static final StatusLogger LOGGER = StatusLogger.getLogger();
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java
index 3ad0f039dbb..06b4818df83 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java
@@ -21,9 +21,11 @@
import java.util.Arrays;
import java.util.Calendar;
import java.util.Locale;
-import java.util.Objects;
import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
import java.util.stream.IntStream;
+import java.util.stream.LongStream;
import org.apache.logging.log4j.core.time.MutableInstant;
import org.apache.logging.log4j.core.util.datetime.FastDatePrinter;
import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
@@ -33,84 +35,137 @@
import org.openjdk.jmh.infra.Blackhole;
/**
- * Compares {@link MutableInstant} formatting efficiency of
- * {@link FastDatePrinter}, {@link FixedDateFormat}, and {@link DateTimeFormatter}.
+ * Compares {@link MutableInstant} formatting efficiency of {@link FastDatePrinter}, {@link FixedDateFormat}, and {@link DateTimeFormatter}.
*
- * The major formatting efficiency is mostly provided by caching, i.e.,
- * reusing the earlier formatter output if timestamps match. We deliberately
- * exclude this optimization, since it is applicable to all formatters. This
- * benchmark rather focuses on only and only the formatting efficiency.
+ * The major formatting efficiency is mostly provided by caching, i.e., reusing the earlier formatter output if timestamps match.
+ * We deliberately exclude this optimization, since it is applicable to all formatters.
+ * This benchmark rather focuses on only and only the formatting efficiency.
+ *
+ *
+ * @see DateTimeFormatImpactBenchmark for the performance impact of different date & time formatters on a typical layout
*/
@State(Scope.Thread)
public class DateTimeFormatBenchmark {
- /**
- * The pattern to be tested.
- *
- * Note that neither {@link FastDatePrinter}, nor {@link FixedDateFormat}
- * supports nanosecond precision.
- */
- private static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";
+ static final Locale LOCALE = Locale.US;
+
+ static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
+
+ private static final MutableInstant[] INSTANTS = createInstants();
+
+ private static MutableInstant[] createInstants() {
+ final Instant initInstant = Instant.parse("2020-05-14T10:44:23.901Z");
+ MutableInstant[] instants = IntStream.range(0, 1_000)
+ .mapToObj((final int index) -> {
+ final MutableInstant instant = new MutableInstant();
+ instant.initFromEpochSecond(
+ Math.addExact(initInstant.getEpochSecond(), index),
+ Math.addExact(initInstant.getNano(), index));
+ return instant;
+ })
+ .toArray(MutableInstant[]::new);
+ validateEpochMillisForFixedDateFormatCache(
+ () -> Arrays.stream(instants).mapToLong(MutableInstant::getEpochMillisecond));
+ return instants;
+ }
+
+ @SuppressWarnings("OptionalGetWithoutIsPresent")
+ static void validateEpochMillisForFixedDateFormatCache(final Supplier millisStreamSupplier) {
+ final long minMillis = millisStreamSupplier.get().min().getAsLong();
+ final long maxMillis = millisStreamSupplier.get().max().getAsLong();
+ final long offMillis = maxMillis - minMillis;
+ if (TimeUnit.DAYS.toMillis(1) <= offMillis) {
+ throw new IllegalStateException(
+ "instant samples must be of the same day to exploit the `FixedDateTime` caching");
+ }
+ }
- private static final Locale LOCALE = Locale.US;
+ private static final Formatters DATE_TIME_FORMATTERS = new Formatters("yyyy-MM-dd'T'HH:mm:ss.SSS");
- private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
+ private static final Formatters TIME_FORMATTERS = new Formatters("HH:mm:ss.SSS");
- private static final Instant INIT_INSTANT = Instant.parse("2020-05-14T10:44:23.901Z");
+ static final class Formatters {
- private static final MutableInstant[] INSTANTS = IntStream.range(0, 1_000)
- .mapToObj((final int index) -> {
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(
- Math.addExact(INIT_INSTANT.getEpochSecond(), index),
- Math.addExact(INIT_INSTANT.getNano(), index));
- return instant;
- })
- .toArray(MutableInstant[]::new);
+ private final String pattern;
- private static final Calendar[] CALENDARS = Arrays.stream(INSTANTS)
- .map((final MutableInstant instant) -> {
- final Calendar calendar = Calendar.getInstance(TIME_ZONE, LOCALE);
- calendar.setTimeInMillis(instant.getEpochMillisecond());
- return calendar;
- })
- .toArray(Calendar[]::new);
+ final FastDatePrinter fastFormatter;
- private static final FastDatePrinter FAST_DATE_PRINTER = new FastDatePrinter(PATTERN, TIME_ZONE, LOCALE) {};
+ final FixedDateFormat fixedFormatter;
- private static final FixedDateFormat FIXED_DATE_FORMAT = Objects.requireNonNull(
- FixedDateFormat.createIfSupported(PATTERN, TIME_ZONE.getID()),
- "couldn't create FixedDateTime for pattern " + PATTERN + " and time zone " + TIME_ZONE.getID());
+ final DateTimeFormatter javaFormatter;
- private static final DateTimeFormatter DATE_TIME_FORMATTER =
- DateTimeFormatter.ofPattern(PATTERN).withZone(TIME_ZONE.toZoneId()).withLocale(LOCALE);
+ Formatters(final String pattern) {
+ this.pattern = pattern;
+ this.fastFormatter = new FastDatePrinter(pattern, TIME_ZONE, LOCALE) {};
+ this.fixedFormatter = FixedDateFormat.createIfSupported(pattern, TIME_ZONE.getID());
+ if (fixedFormatter == null) {
+ final String message = String.format(
+ "couldn't create `%s` for pattern `%s` and time zone `%s`",
+ FixedDateFormat.class.getSimpleName(), pattern, TIME_ZONE.getID());
+ }
+ this.javaFormatter = DateTimeFormatter.ofPattern(pattern)
+ .withZone(TIME_ZONE.toZoneId())
+ .withLocale(LOCALE);
+ }
+ }
- private final StringBuilder stringBuilder = new StringBuilder(PATTERN.length() * 2);
+ private final StringBuilder stringBuilder =
+ new StringBuilder(Math.max(DATE_TIME_FORMATTERS.pattern.length(), TIME_FORMATTERS.pattern.length()) * 2);
private final char[] charBuffer = new char[stringBuilder.capacity()];
+ private final Calendar calendar = Calendar.getInstance(TIME_ZONE, LOCALE);
+
+ @Benchmark
+ public void fastFormatter_dateTime(final Blackhole blackhole) {
+ fastFormatter(blackhole, DATE_TIME_FORMATTERS.fastFormatter);
+ }
+
@Benchmark
- public void fastDatePrinter(final Blackhole blackhole) {
- for (final Calendar calendar : CALENDARS) {
+ public void fastFormatter_time(final Blackhole blackhole) {
+ fastFormatter(blackhole, TIME_FORMATTERS.fastFormatter);
+ }
+
+ private void fastFormatter(final Blackhole blackhole, final FastDatePrinter formatter) {
+ for (final MutableInstant instant : INSTANTS) {
stringBuilder.setLength(0);
- FAST_DATE_PRINTER.format(calendar, stringBuilder);
+ calendar.setTimeInMillis(instant.getEpochMillisecond());
+ formatter.format(calendar, stringBuilder);
blackhole.consume(stringBuilder.length());
}
}
@Benchmark
- public void fixedDateFormat(final Blackhole blackhole) {
+ public void fixedFormatter_dateTime(final Blackhole blackhole) {
+ fixedFormatter(blackhole, DATE_TIME_FORMATTERS.fixedFormatter);
+ }
+
+ @Benchmark
+ public void fixedFormatter_time(final Blackhole blackhole) {
+ fixedFormatter(blackhole, DATE_TIME_FORMATTERS.fixedFormatter);
+ }
+
+ private void fixedFormatter(final Blackhole blackhole, final FixedDateFormat formatter) {
for (final MutableInstant instant : INSTANTS) {
- final int length = FIXED_DATE_FORMAT.formatInstant(instant, charBuffer, 0);
+ final int length = formatter.formatInstant(instant, charBuffer, 0);
blackhole.consume(length);
}
}
@Benchmark
- public void dateTimeFormatter(final Blackhole blackhole) {
+ public void javaFormatter_dateTime(final Blackhole blackhole) {
+ javaFormatter(blackhole, DATE_TIME_FORMATTERS.javaFormatter);
+ }
+
+ @Benchmark
+ public void javaFormatter_time(final Blackhole blackhole) {
+ javaFormatter(blackhole, TIME_FORMATTERS.javaFormatter);
+ }
+
+ private void javaFormatter(final Blackhole blackhole, final DateTimeFormatter formatter) {
for (final MutableInstant instant : INSTANTS) {
stringBuilder.setLength(0);
- DATE_TIME_FORMATTER.formatTo(instant, stringBuilder);
+ formatter.formatTo(instant, stringBuilder);
blackhole.consume(stringBuilder.length());
}
}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatImpactBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatImpactBenchmark.java
new file mode 100644
index 00000000000..2d91b04f969
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatImpactBenchmark.java
@@ -0,0 +1,156 @@
+/*
+ * 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.perf.jmh;
+
+import static org.apache.logging.log4j.perf.jmh.DateTimeFormatBenchmark.validateEpochMillisForFixedDateFormatCache;
+
+import java.time.format.DateTimeFormatter;
+import java.util.Calendar;
+import java.util.List;
+import java.util.function.BiFunction;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.NullConfiguration;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.datetime.FastDatePrinter;
+import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
+import org.apache.logging.log4j.layout.template.json.LogEventFixture;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Benchmarks the impact of different date & time formatters on a typical layout.
+ *
+ * @see DateTimeFormatBenchmark for isolated benchmarks of date & time formatters
+ */
+@State(Scope.Thread)
+public class DateTimeFormatImpactBenchmark {
+
+ private static final List LITE_LOG_EVENTS = createLogEvents(LogEventFixture::createLiteLogEvents);
+
+ private static final List FULL_LOG_EVENTS = createLogEvents(LogEventFixture::createFullLogEvents);
+
+ private static List createLogEvents(final BiFunction> supplier) {
+ final int logEventCount = 1_000;
+ final List logEvents = supplier.apply(
+ logEventCount,
+ // Avoid overlapping instants to ensure the impact of date & time formatting at event encoding:
+ 1);
+ validateEpochMillisForFixedDateFormatCache(() -> logEvents.stream().mapToLong(LogEvent::getTimeMillis));
+ return logEvents;
+ }
+
+ private static final PatternLayout LAYOUT = PatternLayout.newBuilder()
+ .withConfiguration(new NullConfiguration())
+ // Use a typical pattern *without* a date & time converter!
+ .withPattern("[%t] %p %-40.40c{1.} %notEmpty{%x }- %m%n")
+ .withAlwaysWriteExceptions(true)
+ .build();
+
+ private static final DateTimeFormatBenchmark.Formatters FORMATTERS =
+ new DateTimeFormatBenchmark.Formatters("yyyy-MM-dd'T'HH:mm:ss.SSS");
+
+ private final StringBuilder stringBuilder = new StringBuilder(1_1024 * 16);
+
+ private final char[] charBuffer = new char[stringBuilder.capacity()];
+
+ private final Calendar calendar =
+ Calendar.getInstance(DateTimeFormatBenchmark.TIME_ZONE, DateTimeFormatBenchmark.LOCALE);
+
+ @Benchmark
+ public void fastFormatter_lite(final Blackhole blackhole) {
+ fastFormatter(blackhole, LITE_LOG_EVENTS, FORMATTERS.fastFormatter);
+ }
+
+ @Benchmark
+ public void fastFormatter_full(final Blackhole blackhole) {
+ fastFormatter(blackhole, FULL_LOG_EVENTS, FORMATTERS.fastFormatter);
+ }
+
+ private void fastFormatter(
+ final Blackhole blackhole, final List logEvents, final FastDatePrinter formatter) {
+ // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int logEventIndex = 0; logEventIndex < logEvents.size(); logEventIndex++) {
+
+ // 1. Encode event
+ final LogEvent logEvent = logEvents.get(logEventIndex);
+ stringBuilder.setLength(0);
+ LAYOUT.serialize(logEvent, stringBuilder);
+
+ // 2. Encode date & time
+ calendar.setTimeInMillis(logEvent.getInstant().getEpochMillisecond());
+ formatter.format(calendar, stringBuilder);
+ blackhole.consume(stringBuilder.length());
+ }
+ }
+
+ @Benchmark
+ public void fixedFormatter_lite(final Blackhole blackhole) {
+ fixedFormatter(blackhole, LITE_LOG_EVENTS, FORMATTERS.fixedFormatter);
+ }
+
+ @Benchmark
+ public void fixedFormatter_full(final Blackhole blackhole) {
+ fixedFormatter(blackhole, FULL_LOG_EVENTS, FORMATTERS.fixedFormatter);
+ }
+
+ private void fixedFormatter(
+ final Blackhole blackhole, final List logEvents, final FixedDateFormat formatter) {
+ // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int logEventIndex = 0; logEventIndex < logEvents.size(); logEventIndex++) {
+
+ // 1. Encode event
+ final LogEvent logEvent = logEvents.get(logEventIndex);
+ stringBuilder.setLength(0);
+ LAYOUT.serialize(logEvent, stringBuilder);
+
+ // 2. Encode date & time
+ final MutableInstant instant = (MutableInstant) logEvent.getInstant();
+ final int length = formatter.formatInstant(instant, charBuffer, 0);
+ blackhole.consume(length);
+ }
+ }
+
+ @Benchmark
+ public void javaFormatter_lite(final Blackhole blackhole) {
+ javaFormatter(blackhole, LITE_LOG_EVENTS, FORMATTERS.javaFormatter);
+ }
+
+ @Benchmark
+ public void javaFormatter_full(final Blackhole blackhole) {
+ javaFormatter(blackhole, FULL_LOG_EVENTS, FORMATTERS.javaFormatter);
+ }
+
+ private void javaFormatter(
+ final Blackhole blackhole, final List logEvents, final DateTimeFormatter formatter) {
+ // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int logEventIndex = 0; logEventIndex < logEvents.size(); logEventIndex++) {
+
+ // 1. Encode event
+ final LogEvent logEvent = logEvents.get(logEventIndex);
+ stringBuilder.setLength(0);
+ LAYOUT.serialize(logEvent, stringBuilder);
+
+ // 2. Encode date & time
+ final MutableInstant instant = (MutableInstant) logEvent.getInstant();
+ formatter.formatTo(instant, stringBuilder);
+ blackhole.consume(stringBuilder.length());
+ }
+ }
+}
From 1cf4ac90ad305995ff038affc76274e3f3c7a3bf Mon Sep 17 00:00:00 2001
From: ASF Logging Services RM
Date: Mon, 14 Oct 2024 10:51:27 +0000
Subject: [PATCH 02/21] Update `com.github.jnr:jnr-ffi` to version `2.2.17`
(#3082)
---
log4j-cassandra/pom.xml | 2 +-
src/changelog/.2.x.x/update_com_github_jnr_jnr_ffi.xml | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
create mode 100644 src/changelog/.2.x.x/update_com_github_jnr_jnr_ffi.xml
diff --git a/log4j-cassandra/pom.xml b/log4j-cassandra/pom.xml
index eb914cf0e28..209da9734ca 100644
--- a/log4j-cassandra/pom.xml
+++ b/log4j-cassandra/pom.xml
@@ -46,7 +46,7 @@
25.1-jre
- 2.2.16
+ 2.2.171.1.10.7
diff --git a/src/changelog/.2.x.x/update_com_github_jnr_jnr_ffi.xml b/src/changelog/.2.x.x/update_com_github_jnr_jnr_ffi.xml
new file mode 100644
index 00000000000..0484a6ba498
--- /dev/null
+++ b/src/changelog/.2.x.x/update_com_github_jnr_jnr_ffi.xml
@@ -0,0 +1,8 @@
+
+
+
+ Update `com.github.jnr:jnr-ffi` to version `2.2.17`
+
From 062b945bc2cb852966b001c232051eb15456b9a4 Mon Sep 17 00:00:00 2001
From: martin-dorey-hv
Date: Tue, 15 Oct 2024 23:24:39 -0700
Subject: [PATCH 03/21] Correct example property syntax for PatternMatch under
ScriptPatternSelector. (#3092)
Fixes #3078
Co-authored-by: Martin Dorey
---
.../script-pattern-selector/log4j2.properties | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/site/antora/modules/ROOT/examples/manual/pattern-layout/script-pattern-selector/log4j2.properties b/src/site/antora/modules/ROOT/examples/manual/pattern-layout/script-pattern-selector/log4j2.properties
index ec7b7407ccf..67092d5599d 100644
--- a/src/site/antora/modules/ROOT/examples/manual/pattern-layout/script-pattern-selector/log4j2.properties
+++ b/src/site/antora/modules/ROOT/examples/manual/pattern-layout/script-pattern-selector/log4j2.properties
@@ -30,12 +30,12 @@ if (logEvent.getLoggerName().equals("NoLocation")) {\
} else {\
return null;\
}
-appender.0.layout.patternSelector.patternMatch.0.type = PatternMatch
-appender.0.layout.patternSelector.patternMatch.0.key = NoLocation
-appender.0.layout.patternSelector.patternMatch.0.pattern = [%-5level] %c{1.} %msg%n
-appender.0.layout.patternSelector.patternMatch.1.type = PatternMatch
-appender.0.layout.patternSelector.patternMatch.1.key = Flow
-appender.0.layout.patternSelector.patternMatch.1.pattern = [%-5level] %c{1.} ====== %C{1.}.%M:%L %msg ======%n
+appender.0.layout.patternSelector.0.type = PatternMatch
+appender.0.layout.patternSelector.0.key = NoLocation
+appender.0.layout.patternSelector.0.pattern = [%-5level] %c{1.} %msg%n
+appender.0.layout.patternSelector.1.type = PatternMatch
+appender.0.layout.patternSelector.1.key = Flow
+appender.0.layout.patternSelector.1.pattern = [%-5level] %c{1.} ====== %C{1.}.%M:%L %msg ======%n
rootLogger.level = WARN
rootLogger.appenderRef.0.ref = CONSOLE
From dfe0ada4a6f13478b66d4eb64f08ec43c45397a2 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Wed, 16 Oct 2024 08:26:08 +0200
Subject: [PATCH 04/21] Fixes property names in release notes (#3089)
Fixes the names of configuration properties introduced after `2.10.0` to always use the normalized form.
Fixes #3079
---
...cts_the_configured_log4j2_is_webapp_property.xml | 6 +++++-
...tus_logger_timestamp_format_is_now_configura.xml | 5 ++++-
...n_on_how_to_toggle_log4j2_debug_system_prope.xml | 6 +++++-
..._default_Require_log4j2_enableJndi_to_be_set.xml | 6 +++++-
src/changelog/2.17.0/.release-notes.adoc.ftl | 6 +++++-
...he_java_protocol_only_JNDI_will_remain_disab.xml | 13 +++++++++++--
...ow_uses_JndiManager_to_access_JNDI_resources.xml | 9 +++++++--
...cript_enableLanguages_to_be_specified_to_ena.xml | 6 +++++-
src/changelog/2.22.0/change_basic_auth_encoding.xml | 4 +++-
.../2.23.1/fix_StatusLogger_instant_formatting.xml | 4 +++-
src/changelog/2.24.0/.release-notes.adoc.ftl | 4 +++-
...63_add_logback_throwable_consuming_semantics.xml | 4 +++-
.../2.24.0/2374_refactor_initialization.xml | 6 +++++-
.../2.24.0/2379_fix_message_factory_properties.xml | 10 ++++++++--
.../2.24.0/2462_disable_jmx_by_default.xml | 9 +++++++--
src/changelog/2.24.0/2703_log4j_debug.xml | 5 ++++-
src/changelog/2.24.0/change-is-webapp.xml | 8 +++++++-
.../deprecate_log4j2_default_status_level.xml | 5 ++++-
.../modules/ROOT/pages/manual/systemproperties.adoc | 2 +-
19 files changed, 95 insertions(+), 23 deletions(-)
diff --git a/src/changelog/2.10.0/LOG4J2-2091_Log4j_respects_the_configured_log4j2_is_webapp_property.xml b/src/changelog/2.10.0/LOG4J2-2091_Log4j_respects_the_configured_log4j2_is_webapp_property.xml
index abbee25ab73..920abe1c410 100644
--- a/src/changelog/2.10.0/LOG4J2-2091_Log4j_respects_the_configured_log4j2_is_webapp_property.xml
+++ b/src/changelog/2.10.0/LOG4J2-2091_Log4j_respects_the_configured_log4j2_is_webapp_property.xml
@@ -4,5 +4,9 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="fixed">
- Log4j respects the configured "log4j2.is.webapp" property
+
+ Log4j respects the configured
+ xref:manual/systemproperties.adoc#log4j2.isWebapp[`log4j2.isWebapp`]
+ property.
+
diff --git a/src/changelog/2.11.0/LOG4J2-2250_The_internal_status_logger_timestamp_format_is_now_configura.xml b/src/changelog/2.11.0/LOG4J2-2250_The_internal_status_logger_timestamp_format_is_now_configura.xml
index 29e60533f7a..1bd0b1e15fb 100644
--- a/src/changelog/2.11.0/LOG4J2-2250_The_internal_status_logger_timestamp_format_is_now_configura.xml
+++ b/src/changelog/2.11.0/LOG4J2-2250_The_internal_status_logger_timestamp_format_is_now_configura.xml
@@ -4,5 +4,8 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="changed">
- The internal status logger timestamp format is now configurable with system property `log4j2.StatusLogger.DateFormat`.
+
+ The internal status logger timestamp format is now configurable with system property
+ xref:manual/status-logger.adoc#log4j2.statusLoggerDateFormat[`log4j2.statusLoggerDateFormat`].
+
diff --git a/src/changelog/2.15.0/LOG4J2-3160_Fix_documentation_on_how_to_toggle_log4j2_debug_system_prope.xml b/src/changelog/2.15.0/LOG4J2-3160_Fix_documentation_on_how_to_toggle_log4j2_debug_system_prope.xml
index 08a8b38ebf2..0091fb3bb71 100644
--- a/src/changelog/2.15.0/LOG4J2-3160_Fix_documentation_on_how_to_toggle_log4j2_debug_system_prope.xml
+++ b/src/changelog/2.15.0/LOG4J2-3160_Fix_documentation_on_how_to_toggle_log4j2_debug_system_prope.xml
@@ -4,5 +4,9 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="fixed">
- Fix documentation on how to toggle log4j2.debug system property.
+
+ Fix documentation on how to toggle
+ xref:manual/status-logger.adoc#log4j2.debug[`log4j2.debug`]
+ system property.
+
diff --git a/src/changelog/2.16.0/LOG4J2-3208_Disable_JNDI_by_default_Require_log4j2_enableJndi_to_be_set.xml b/src/changelog/2.16.0/LOG4J2-3208_Disable_JNDI_by_default_Require_log4j2_enableJndi_to_be_set.xml
index 13d7be46deb..3968288cf32 100644
--- a/src/changelog/2.16.0/LOG4J2-3208_Disable_JNDI_by_default_Require_log4j2_enableJndi_to_be_set.xml
+++ b/src/changelog/2.16.0/LOG4J2-3208_Disable_JNDI_by_default_Require_log4j2_enableJndi_to_be_set.xml
@@ -4,5 +4,9 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="fixed">
- Disable JNDI by default. Require log4j2.enableJndi to be set to true to allow JNDI.
+
+ Disable JNDI by default. Require
+ xref:manual/systemproperties.adoc#properties-jndi[`log4j2.enableJndi`]
+ to be set to true to allow JNDI.
+
diff --git a/src/changelog/2.17.0/.release-notes.adoc.ftl b/src/changelog/2.17.0/.release-notes.adoc.ftl
index 9d246be882e..c24e4f2fab1 100644
--- a/src/changelog/2.17.0/.release-notes.adoc.ftl
+++ b/src/changelog/2.17.0/.release-notes.adoc.ftl
@@ -45,7 +45,11 @@ Recursive evaluation is still allowed while generating the configuration.
* The `JndiLookup`, `JndiContextSelector`, and `JMSAppender` now require individual system properties to be enabled.
* Remove LDAP and LDAPS as supported protocols from JNDI.
-The single `log4j2.enableJndi` property introduced in Log4j 2.16.0 has been replaced with three individual properties; `log4j2.enableJndiContextSelector`, `log4j2.enableJndiJms`, and `log4j2.enableJndiLookup`.
+The single `log4j2.enableJndi` property introduced in Log4j 2.16.0 has been replaced with three individual properties:
+xref:manual/systemproperties.adoc#log4j2.enableJndiContextSelector[`log4j2.enableJndiContextSelector`],
+xref:manual/systemproperties.adoc#log4j2.enableJndiJms[`log4j2.enableJndiJms`],
+and
+ xref:manual/systemproperties.adoc#log4j2.enableJndiLookup[`log4j2.enableJndiLookup`].
The Log4j 2.17.0 API, as well as many core components, maintains binary compatibility with previous releases.
diff --git a/src/changelog/2.17.0/LOG4J2-3242_Limit_JNDI_to_the_java_protocol_only_JNDI_will_remain_disab.xml b/src/changelog/2.17.0/LOG4J2-3242_Limit_JNDI_to_the_java_protocol_only_JNDI_will_remain_disab.xml
index ad04a77cf6a..04b0ec7031c 100644
--- a/src/changelog/2.17.0/LOG4J2-3242_Limit_JNDI_to_the_java_protocol_only_JNDI_will_remain_disab.xml
+++ b/src/changelog/2.17.0/LOG4J2-3242_Limit_JNDI_to_the_java_protocol_only_JNDI_will_remain_disab.xml
@@ -4,6 +4,15 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="fixed">
- Limit JNDI to the java protocol only. JNDI will remain disabled by default. Rename JNDI enablement property from
- 'log4j2.enableJndi' to 'log4j2.enableJndiLookup', 'log4j2.enableJndiJms', and 'log4j2.enableJndiContextSelector'.
+
+ Limit JNDI to the java protocol only.
+ JNDI will remain disabled by default.
+ Rename JNDI enablement property from
+ xref:manual/systemproperties.adoc#properties-jndi[`log4j2.enableJndi`]
+ to
+ xref:manual/systemproperties.adoc#log4j2.enableJndiLookup[`log4j2.enableJndiLookup`],
+ xref:manual/systemproperties.adoc#log4j2.enableJndiJms[`log4j2.enableJndiJms`],
+ and
+ xref:manual/systemproperties.adoc#log4j2.enableJndiContextSelector[`log4j2.enableJndiContextSelector`].
+
diff --git a/src/changelog/2.17.1/LOG4J2-3293_JdbcAppender_now_uses_JndiManager_to_access_JNDI_resources.xml b/src/changelog/2.17.1/LOG4J2-3293_JdbcAppender_now_uses_JndiManager_to_access_JNDI_resources.xml
index 54ce27ac86d..56c7dd1ac35 100644
--- a/src/changelog/2.17.1/LOG4J2-3293_JdbcAppender_now_uses_JndiManager_to_access_JNDI_resources.xml
+++ b/src/changelog/2.17.1/LOG4J2-3293_JdbcAppender_now_uses_JndiManager_to_access_JNDI_resources.xml
@@ -4,6 +4,11 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="fixed">
- JdbcAppender now uses JndiManager to access JNDI resources. JNDI is only enabled when system property
- log4j2.enableJndiJdbc is set to true.
+
+ xref:manual/appenders/database.adoc#JdbcAppender[JDBC Appender]
+ now uses `JndiManager` to access JNDI resources.
+ JNDI is only enabled when the system property
+ xref:manual/systemproperties.adoc#log4j2.enableJndiJdbc[`log4j2.enableJndiJdbc`]
+ is set to `true`.
+
diff --git a/src/changelog/2.17.2/LOG4J2-2486_Require_log4j2_Script_enableLanguages_to_be_specified_to_ena.xml b/src/changelog/2.17.2/LOG4J2-2486_Require_log4j2_Script_enableLanguages_to_be_specified_to_ena.xml
index 17407d90f73..a73a33e938c 100644
--- a/src/changelog/2.17.2/LOG4J2-2486_Require_log4j2_Script_enableLanguages_to_be_specified_to_ena.xml
+++ b/src/changelog/2.17.2/LOG4J2-2486_Require_log4j2_Script_enableLanguages_to_be_specified_to_ena.xml
@@ -4,5 +4,9 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="added">
- Require log4j2.Script.enableLanguages to be specified to enable scripting for specific languages.
+
+ Require
+ xref:manual/systemproperties.adoc#log4j2.scriptEnableLanguages[`log4j2.scriptEnableLanguages`]
+ to be specified to enable scripting for specific languages.
+
diff --git a/src/changelog/2.22.0/change_basic_auth_encoding.xml b/src/changelog/2.22.0/change_basic_auth_encoding.xml
index f528caab8cc..ffe7a67ac11 100644
--- a/src/changelog/2.22.0/change_basic_auth_encoding.xml
+++ b/src/changelog/2.22.0/change_basic_auth_encoding.xml
@@ -5,6 +5,8 @@
type="changed">
- Change default encoding of HTTP Basic Authentication to UTF-8 and add `log4j2.configurationAuthorizationEncoding` property to overwrite it.
+ Change default encoding of HTTP Basic Authentication to UTF-8 and add
+ xref:manual/systemproperties.adoc#log4j2.configurationAuthorizationEncoding[`log4j2.configurationAuthorizationEncoding`]
+ property to overwrite it.
diff --git a/src/changelog/2.23.1/fix_StatusLogger_instant_formatting.xml b/src/changelog/2.23.1/fix_StatusLogger_instant_formatting.xml
index 2f2298baee2..4b965ca3c09 100644
--- a/src/changelog/2.23.1/fix_StatusLogger_instant_formatting.xml
+++ b/src/changelog/2.23.1/fix_StatusLogger_instant_formatting.xml
@@ -5,7 +5,9 @@
type="fixed">
- Add `log4j2.StatusLogger.dateFormatZone` system property to set the time-zone `StatusLogger` uses to format `java.time.Instant`.
+ Add
+ xref:manual/statusLogger.adoc#log4j2.statusLoggerDateFormatZone[`log4j2.statusLoggerDateFormatZone`]
+ system property to set the time-zone `StatusLogger` uses to format `java.time.Instant`.
Without this, formatting patterns accessing to time-zone-specific fields (e.g., year-of-era) cause failures.
diff --git a/src/changelog/2.24.0/.release-notes.adoc.ftl b/src/changelog/2.24.0/.release-notes.adoc.ftl
index 2d0ccab9d58..cdc6f5779b3 100644
--- a/src/changelog/2.24.0/.release-notes.adoc.ftl
+++ b/src/changelog/2.24.0/.release-notes.adoc.ftl
@@ -58,6 +58,8 @@ Please migrate to xref:components.adoc#log4j-mongodb[`log4j-mongodb`] (client ve
=== JMX changes
-Starting in version 2.24.0, JMX support is disabled by default and can be re-enabled via the `log4j2.disableJmx=false` system property.
+Starting in version 2.24.0, JMX support is disabled by default and can be re-enabled via the
+xref:manual/systemproperties.adoc#log4j2.disableJmx[`log4j2.disableJmx`]
+system property.
<#include "../.changelog.adoc.ftl">
diff --git a/src/changelog/2.24.0/2363_add_logback_throwable_consuming_semantics.xml b/src/changelog/2.24.0/2363_add_logback_throwable_consuming_semantics.xml
index 7d8332750d1..9e917978615 100644
--- a/src/changelog/2.24.0/2363_add_logback_throwable_consuming_semantics.xml
+++ b/src/changelog/2.24.0/2363_add_logback_throwable_consuming_semantics.xml
@@ -6,6 +6,8 @@
Add Logback throwable-consuming semantics as an option in `log4j-slf4j-impl` and `log4j-slf4j2-impl`.
- Users can enable it by setting the property `log4j2.messageFactory` to `org.apache.logging.slf4j.message.ThrowableConsumingMessageFactory`.
+ Users can enable it by setting the property
+ xref:manual/systemproperties.adoc#log4j2.messageFactory[`log4j2.messageFactory`]
+ to `org.apache.logging.slf4j.message.ThrowableConsumingMessageFactory`.
diff --git a/src/changelog/2.24.0/2374_refactor_initialization.xml b/src/changelog/2.24.0/2374_refactor_initialization.xml
index b80bb68867c..ed80858b32b 100644
--- a/src/changelog/2.24.0/2374_refactor_initialization.xml
+++ b/src/changelog/2.24.0/2374_refactor_initialization.xml
@@ -4,5 +4,9 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="changed">
- Centralize initialization in the `Provider` class and deprecate `log4j2.loggerContextFactory` property.
+
+ Centralize initialization in the `Provider` class and deprecate the
+ xref:manual/systemproperties.adoc#log4j2.loggerContextFactory[`log4j2.loggerContextFactory`]
+ property.
+
diff --git a/src/changelog/2.24.0/2379_fix_message_factory_properties.xml b/src/changelog/2.24.0/2379_fix_message_factory_properties.xml
index f86978422f1..6e414b11f54 100644
--- a/src/changelog/2.24.0/2379_fix_message_factory_properties.xml
+++ b/src/changelog/2.24.0/2379_fix_message_factory_properties.xml
@@ -3,6 +3,12 @@
xmlns="https://logging.apache.org/xml/ns"
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="fixed">
-
- Fix handling of `log4j2.messageFactory` and `log4j2.flowMessageFactory` properties
+
+
+ Fix handling of
+ xref:manual/systemproperties.adoc#log4j2.messageFactory[`log4j2.messageFactory`]
+ and
+ xref:manual/systemproperties.adoc#log4j2.flowMessageFactory[`log4j2.flowMessageFactory`]
+ properties.
+
diff --git a/src/changelog/2.24.0/2462_disable_jmx_by_default.xml b/src/changelog/2.24.0/2462_disable_jmx_by_default.xml
index 0adfd67cbad..3e3ca1b31f8 100644
--- a/src/changelog/2.24.0/2462_disable_jmx_by_default.xml
+++ b/src/changelog/2.24.0/2462_disable_jmx_by_default.xml
@@ -3,6 +3,11 @@
xmlns="https://logging.apache.org/xml/ns"
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="changed">
-
- Disable JMX support by default. Require `log4j2.disableJmx` to be set to `false` to enable JMX support.
+
+
+ Disable JMX support by default.
+ Requires
+ xref:manual/systemproperties.adoc#log4j2.disableJmx[`log4j2.disableJmx`]
+ to be set to `false` to enable JMX support.
+
diff --git a/src/changelog/2.24.0/2703_log4j_debug.xml b/src/changelog/2.24.0/2703_log4j_debug.xml
index 42a9fd1c534..f0ea523807d 100644
--- a/src/changelog/2.24.0/2703_log4j_debug.xml
+++ b/src/changelog/2.24.0/2703_log4j_debug.xml
@@ -4,5 +4,8 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="fixed">
- Fix handling of `log4j2.debug`.
+
+ Fix handling of
+ xref:manual/status-logger#log4j2.debug[`log4j2.debug`].
+
diff --git a/src/changelog/2.24.0/change-is-webapp.xml b/src/changelog/2.24.0/change-is-webapp.xml
index 105b114ccd6..2c0d9fd9abe 100644
--- a/src/changelog/2.24.0/change-is-webapp.xml
+++ b/src/changelog/2.24.0/change-is-webapp.xml
@@ -5,6 +5,12 @@
type="changed">
- Prioritize user-defined values of `log4j2.enableThreadLocals`, `log4j2.garbagefreeThreadContextMap` and `log4j2.shutdownHookEnabled` over the value of `log4j.isWebapp`.
+ Prioritize user-defined values of
+ xref:manual/systemproperties.adoc#log4j2.enableThreadlocals[`log4j2.enableThreadlocals`],
+ xref:manual/systemproperties.adoc#log4j2.garbagefreeThreadContextMap[`log4j2.garbagefreeThreadContextMap`]
+ and
+ xref:manual/systemproperties.adoc#log4j2.shutdownHookEnabled[`log4j2.shutdownHookEnabled`]
+ over the value of
+ xref:manual/systemproperties.adoc#log4j2.isWebapp[`log4j2.isWebapp`].
diff --git a/src/changelog/2.24.0/deprecate_log4j2_default_status_level.xml b/src/changelog/2.24.0/deprecate_log4j2_default_status_level.xml
index b5c6d463a35..12c86c83e9b 100644
--- a/src/changelog/2.24.0/deprecate_log4j2_default_status_level.xml
+++ b/src/changelog/2.24.0/deprecate_log4j2_default_status_level.xml
@@ -4,5 +4,8 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="changed">
- Deprecate `log4j2.defaultStatusLevel` property in Log4j Core in favor of `log4j2.statusLoggerLevel`
+
+ Deprecate `log4j2.defaultStatusLevel` property in Log4j Core in favor of
+ xref:manual/status-logger.adoc#log4j2.statusLoggerLevel[`log4j2.statusLoggerLevel`].
+
diff --git a/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc b/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
index e72136de40f..2bd4b6e75e5 100644
--- a/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
@@ -46,7 +46,7 @@ If a `log4j2.system.properties` file is available on the classpath its contents
[WARNING]
====
-To provide backward compatibility with versions older than 2.10 a certain number of additional property names is also supported using a fuzzy matching algorithm.
+To provide backward compatibility with versions older than 2.10.0 a certain number of additional property names are also supported using a fuzzy matching algorithm.
In case of problems with the properties sub-system, make sure that your application does not use property names with the following case-insensitive prefixes:
From 9ef736f1c7f0b8de5c7a31ee65261a315fd29954 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Wed, 16 Oct 2024 11:12:53 +0200
Subject: [PATCH 05/21] Remove JANSI dependency in `2.x` (#3070)
This commit:
- Removes support for the outdated [Jansi 1.x](http://fusesource.github.io/jansi/) version in `Console` appender.
- Rewrites `JAnsiTextRenderer`, use in the `%m{ansi}` and `%ex{ansi}` pattern converters to use our internal ANSI support instead of Jansi.
Fixes #1736.
---
.../AbstractLog4j1ConfigurationTest.java | 124 +++--
log4j-core-test/pom.xml | 7 -
.../log4j/core/test/categories/Layouts.java | 2 -
.../core/test/categories/package-info.java | 4 +-
.../ConsoleAppenderAnsiMessagesMain.java | 5 +-
.../ConsoleAppenderAnsiStyleJira180Main.java | 8 +-
.../ConsoleAppenderAnsiStyleJira272Main.java | 8 +-
.../ConsoleAppenderAnsiStyleJira319Main.java | 8 +-
.../ConsoleAppenderAnsiStyleLayoutMain.java | 6 +-
...onsoleAppenderAnsiStyleNameLayoutMain.java | 3 +-
...=> ConsoleAppenderAnsiXExceptionMain.java} | 15 +-
...oleAppenderDefaultSuppressedThrowable.java | 11 +-
...oleAppenderHighlightLayoutDefaultMain.java | 3 +-
.../ConsoleAppenderHighlightLayoutMain.java | 3 +-
.../ConsoleAppenderJAnsiMessageMain.java | 84 ----
...enderJira1002ShortThrowableLayoutMain.java | 4 +-
.../ConsoleAppenderNoAnsiStyleLayoutMain.java | 9 +-
.../appender/JansiConsoleAppenderJira965.java | 28 --
.../core/impl/ThrowableFormatOptionsTest.java | 134 +++---
.../core/pattern/JAnsiTextRendererTest.java | 58 +++
...est.java => MessageAnsiConverterTest.java} | 4 +-
.../pattern/MessageStyledConverterTest.java | 2 +-
.../resources/log4j2-console-msg-ansi.xml | 30 --
log4j-core/pom.xml | 8 -
.../log4j/core/appender/ConsoleAppender.java | 151 ++-----
.../core/impl/ThrowableFormatOptions.java | 18 +-
.../logging/log4j/core/impl/package-info.java | 2 +-
.../log4j/core/layout/PatternLayout.java | 13 +-
.../log4j/core/pattern/AnsiEscape.java | 7 +-
.../log4j/core/pattern/JAnsiTextRenderer.java | 423 ++++++++----------
.../core/pattern/MessagePatternConverter.java | 9 +-
.../log4j/core/pattern/package-info.java | 2 +-
.../logging/log4j/core/util/Loader.java | 4 +
.../log4j/smtp/SmtpAppenderAsyncTest.java | 4 +-
log4j-parent/pom.xml | 7 -
pom.xml | 7 -
src/changelog/.2.x.x/.release-notes.adoc.ftl | 6 +
.../.2.x.x/1736_split_jansi_support.xml | 8 +
.../.2.x.x/2916_rewrite_jansi_renderer.xml | 8 +
src/site/antora/antora.tmpl.yml | 1 -
src/site/antora/antora.yml | 1 -
.../modules/ROOT/pages/manual/appenders.adoc | 43 +-
.../ROOT/pages/manual/pattern-layout.adoc | 23 +-
.../ROOT/pages/manual/systemproperties.adoc | 7 -
.../systemproperties/properties-jansi.adoc | 32 --
45 files changed, 513 insertions(+), 831 deletions(-)
rename log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/{ConsoleAppenderJAnsiXExceptionMain.java => ConsoleAppenderAnsiXExceptionMain.java} (78%)
delete mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJAnsiMessageMain.java
delete mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/JansiConsoleAppenderJira965.java
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/JAnsiTextRendererTest.java
rename log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/{MessageJansiConverterTest.java => MessageAnsiConverterTest.java} (94%)
delete mode 100644 log4j-core-test/src/test/resources/log4j2-console-msg-ansi.xml
create mode 100644 src/changelog/.2.x.x/1736_split_jansi_support.xml
create mode 100644 src/changelog/.2.x.x/2916_rewrite_jansi_renderer.xml
delete mode 100644 src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-jansi.adoc
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/config/AbstractLog4j1ConfigurationTest.java b/log4j-1.2-api/src/test/java/org/apache/log4j/config/AbstractLog4j1ConfigurationTest.java
index 90214096ff1..85851f52e6b 100644
--- a/log4j-1.2-api/src/test/java/org/apache/log4j/config/AbstractLog4j1ConfigurationTest.java
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/config/AbstractLog4j1ConfigurationTest.java
@@ -31,7 +31,6 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
-import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
@@ -66,7 +65,6 @@
import org.apache.logging.log4j.core.filter.ThresholdFilter;
import org.apache.logging.log4j.core.layout.HtmlLayout;
import org.apache.logging.log4j.core.layout.PatternLayout;
-import org.apache.logging.log4j.core.util.CloseShieldOutputStream;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
@@ -78,7 +76,7 @@ public abstract class AbstractLog4j1ConfigurationTest {
abstract Configuration getConfiguration(String configResourcePrefix) throws URISyntaxException, IOException;
- protected InputStream getResourceAsStream(final String configResource) throws IOException {
+ protected InputStream getResourceAsStream(final String configResource) {
final InputStream is = getClass().getClassLoader().getResourceAsStream(configResource);
assertNotNull(is);
return is;
@@ -123,7 +121,7 @@ private Layout> testConsole(final String configResource) throws Exception {
final ConsoleAppender appender = configuration.getAppender(name);
assertNotNull(
"Missing appender '" + name + "' in configuration " + configResource + " → " + configuration, appender);
- assertEquals("follow", true, getFollowProperty(appender));
+ assertTrue("follow", getFollowProperty(appender));
assertEquals(Target.SYSTEM_ERR, appender.getTarget());
//
final LoggerConfig loggerConfig = configuration.getLoggerConfig("com.example.foo");
@@ -141,16 +139,15 @@ public void testConsoleTtccLayout() throws Exception {
}
public void testRollingFileAppender() throws Exception {
- testRollingFileAppender("config-1.2/log4j-RollingFileAppender", "RFA", "target/hadoop.log.%i");
+ testRollingFileAppender("config-1.2/log4j-RollingFileAppender");
}
public void testDailyRollingFileAppender() throws Exception {
- testDailyRollingFileAppender(
- "config-1.2/log4j-DailyRollingFileAppender", "DRFA", "target/hadoop.log%d{.dd-MM-yyyy}");
+ testDailyRollingFileAppender("config-1.2/log4j-DailyRollingFileAppender");
}
public void testRollingFileAppenderWithProperties() throws Exception {
- testRollingFileAppender("config-1.2/log4j-RollingFileAppender-with-props", "RFA", "target/hadoop.log.%i");
+ testRollingFileAppender("config-1.2/log4j-RollingFileAppender-with-props");
}
public void testSystemProperties1() throws Exception {
@@ -160,13 +157,13 @@ public void testSystemProperties1() throws Exception {
final Configuration configuration = getConfiguration("config-1.2/log4j-system-properties-1");
try {
final RollingFileAppender appender = configuration.getAppender("RFA");
- assertEquals("append", false, getAppendProperty(appender));
+ assertFalse("append", getAppendProperty(appender));
assertEquals("bufferSize", 1000, appender.getManager().getBufferSize());
- assertEquals("immediateFlush", false, appender.getImmediateFlush());
+ assertFalse("immediateFlush", appender.getImmediateFlush());
final DefaultRolloverStrategy rolloverStrategy =
(DefaultRolloverStrategy) appender.getManager().getRolloverStrategy();
assertEquals(16, rolloverStrategy.getMaxIndex());
- final CompositeTriggeringPolicy ctp = (CompositeTriggeringPolicy) appender.getTriggeringPolicy();
+ final CompositeTriggeringPolicy ctp = appender.getTriggeringPolicy();
final TriggeringPolicy[] triggeringPolicies = ctp.getTriggeringPolicies();
assertEquals(1, triggeringPolicies.length);
final TriggeringPolicy tp = triggeringPolicies[0];
@@ -174,17 +171,11 @@ public void testSystemProperties1() throws Exception {
final SizeBasedTriggeringPolicy sbtp = (SizeBasedTriggeringPolicy) tp;
assertEquals(20 * 1024 * 1024, sbtp.getMaxFileSize());
appender.stop(10, TimeUnit.SECONDS);
- // System.out.println("expected: " + tempFileName + " Actual: " +
- // appender.getFileName());
assertEquals(tempFileName, appender.getFileName());
} finally {
configuration.start();
configuration.stop();
- try {
- Files.deleteIfExists(tempFilePath);
- } catch (final FileSystemException e) {
- e.printStackTrace();
- }
+ Files.deleteIfExists(tempFilePath);
}
}
@@ -195,22 +186,17 @@ public void testSystemProperties2() throws Exception {
assertEquals(tmpDir + "/hadoop.log", appender.getFileName());
appender.stop(10, TimeUnit.SECONDS);
// Try to clean up
- try {
- Path path = new File(appender.getFileName()).toPath();
- Files.deleteIfExists(path);
- path = new File("${java.io.tmpdir}").toPath();
- Files.deleteIfExists(path);
- } catch (IOException e) {
- e.printStackTrace();
- }
+ Path path = new File(appender.getFileName()).toPath();
+ Files.deleteIfExists(path);
+ path = new File("${java.io.tmpdir}").toPath();
+ Files.deleteIfExists(path);
}
- private void testRollingFileAppender(final String configResource, final String name, final String filePattern)
- throws Exception {
+ private void testRollingFileAppender(final String configResource) throws Exception {
final Configuration configuration = getConfiguration(configResource);
- final Appender appender = configuration.getAppender(name);
+ final Appender appender = configuration.getAppender("RFA");
assertNotNull(appender);
- assertEquals(name, appender.getName());
+ assertEquals("RFA", appender.getName());
assertTrue(appender.getClass().getName(), appender instanceof RollingFileAppender);
final RollingFileAppender rfa = (RollingFileAppender) appender;
@@ -218,11 +204,11 @@ private void testRollingFileAppender(final String configResource, final String n
"defaultRolloverStrategy", rfa.getManager().getRolloverStrategy() instanceof DefaultRolloverStrategy);
assertFalse(
"rolloverStrategy", ((DefaultRolloverStrategy) rfa.getManager().getRolloverStrategy()).isUseMax());
- assertEquals("append", false, getAppendProperty(rfa));
+ assertFalse("append", getAppendProperty(rfa));
assertEquals("bufferSize", 1000, rfa.getManager().getBufferSize());
- assertEquals("immediateFlush", false, rfa.getImmediateFlush());
+ assertFalse("immediateFlush", rfa.getImmediateFlush());
assertEquals("target/hadoop.log", rfa.getFileName());
- assertEquals(filePattern, rfa.getFilePattern());
+ assertEquals("target/hadoop.log.%i", rfa.getFilePattern());
final TriggeringPolicy triggeringPolicy = rfa.getTriggeringPolicy();
assertNotNull(triggeringPolicy);
assertTrue(triggeringPolicy.getClass().getName(), triggeringPolicy instanceof CompositeTriggeringPolicy);
@@ -241,20 +227,19 @@ private void testRollingFileAppender(final String configResource, final String n
configuration.stop();
}
- private void testDailyRollingFileAppender(final String configResource, final String name, final String filePattern)
- throws Exception {
+ private void testDailyRollingFileAppender(final String configResource) throws Exception {
final Configuration configuration = getConfiguration(configResource);
try {
- final Appender appender = configuration.getAppender(name);
+ final Appender appender = configuration.getAppender("DRFA");
assertNotNull(appender);
- assertEquals(name, appender.getName());
+ assertEquals("DRFA", appender.getName());
assertTrue(appender.getClass().getName(), appender instanceof RollingFileAppender);
final RollingFileAppender rfa = (RollingFileAppender) appender;
- assertEquals("append", false, getAppendProperty(rfa));
+ assertFalse("append", getAppendProperty(rfa));
assertEquals("bufferSize", 1000, rfa.getManager().getBufferSize());
- assertEquals("immediateFlush", false, rfa.getImmediateFlush());
+ assertFalse("immediateFlush", rfa.getImmediateFlush());
assertEquals("target/hadoop.log", rfa.getFileName());
- assertEquals(filePattern, rfa.getFilePattern());
+ assertEquals("target/hadoop.log%d{.dd-MM-yyyy}", rfa.getFilePattern());
final TriggeringPolicy triggeringPolicy = rfa.getTriggeringPolicy();
assertNotNull(triggeringPolicy);
assertTrue(triggeringPolicy.getClass().getName(), triggeringPolicy instanceof CompositeTriggeringPolicy);
@@ -275,8 +260,8 @@ private void testDailyRollingFileAppender(final String configResource, final Str
}
}
- private Layout> testFile(final String configResource) throws Exception {
- final Configuration configuration = getConfiguration(configResource);
+ private Layout> testFile() throws Exception {
+ final Configuration configuration = getConfiguration("config-1.2/log4j-file-SimpleLayout");
final FileAppender appender = configuration.getAppender("File");
assertNotNull(appender);
assertEquals("target/mylog.txt", appender.getFileName());
@@ -284,9 +269,9 @@ private Layout> testFile(final String configResource) throws Exception {
final LoggerConfig loggerConfig = configuration.getLoggerConfig("com.example.foo");
assertNotNull(loggerConfig);
assertEquals(Level.DEBUG, loggerConfig.getLevel());
- assertEquals("append", false, getAppendProperty(appender));
+ assertFalse("append", getAppendProperty(appender));
assertEquals("bufferSize", 1000, appender.getManager().getBufferSize());
- assertEquals("immediateFlush", false, appender.getImmediateFlush());
+ assertFalse("immediateFlush", appender.getImmediateFlush());
configuration.start();
configuration.stop();
return appender.getLayout();
@@ -316,7 +301,7 @@ public void testConsoleSimpleLayout() throws Exception {
}
public void testFileSimpleLayout() throws Exception {
- final PatternLayout layout = (PatternLayout) testFile("config-1.2/log4j-file-SimpleLayout");
+ final PatternLayout layout = (PatternLayout) testFile();
assertEquals("%v1Level - %m%n", layout.getConversionPattern());
}
@@ -328,14 +313,11 @@ public void testNullAppender() throws Exception {
assertTrue(appender.getClass().getName(), appender instanceof NullAppender);
}
- private boolean getFollowProperty(final ConsoleAppender consoleAppender)
- throws Exception, NoSuchFieldException, IllegalAccessException {
- final CloseShieldOutputStream wrapperStream =
- (CloseShieldOutputStream) getOutputStream(consoleAppender.getManager());
- final Field delegateField = CloseShieldOutputStream.class.getDeclaredField("delegate");
- delegateField.setAccessible(true);
- final boolean follow = !System.out.equals(delegateField.get(wrapperStream));
- return follow;
+ private boolean getFollowProperty(final ConsoleAppender consoleAppender) throws Exception {
+ OutputStream outputStream = getOutputStream(consoleAppender.getManager());
+ String className = outputStream.getClass().getName();
+ return className.endsWith("ConsoleAppender$SystemErrStream")
+ || className.endsWith("ConsoleAppender$SystemOutStream");
}
private boolean getAppendProperty(final RollingFileAppender appender) throws Exception {
@@ -383,7 +365,7 @@ public void testDefaultValues() throws Exception {
final HtmlLayout htmlLayout = (HtmlLayout) testLayout(config, "HTMLLayout");
assertNotNull(htmlLayout);
assertEquals("title", "Log4J Log Messages", htmlLayout.getTitle());
- assertEquals("locationInfo", false, htmlLayout.isLocationInfo());
+ assertFalse("locationInfo", htmlLayout.isLocationInfo());
// PatternLayout
final PatternLayout patternLayout = (PatternLayout) testLayout(config, "PatternLayout");
assertNotNull(patternLayout);
@@ -403,7 +385,7 @@ public void testDefaultValues() throws Exception {
assertNotNull(consoleAppender);
assertEquals("target", Target.SYSTEM_OUT, consoleAppender.getTarget());
final boolean follow = getFollowProperty(consoleAppender);
- assertEquals("follow", false, follow);
+ assertFalse("follow", follow);
// DailyRollingFileAppender
final RollingFileAppender dailyRollingFileAppender = config.getAppender("DailyRollingFileAppender");
assertNotNull(dailyRollingFileAppender);
@@ -411,15 +393,15 @@ public void testDefaultValues() throws Exception {
"equivalent file pattern",
"target/dailyRollingFileAppender%d{.yyyy-MM-dd}",
dailyRollingFileAppender.getFilePattern());
- assertEquals("append", true, getAppendProperty(dailyRollingFileAppender));
+ assertTrue("append", getAppendProperty(dailyRollingFileAppender));
assertEquals("bufferSize", 8192, dailyRollingFileAppender.getManager().getBufferSize());
- assertEquals("immediateFlush", true, dailyRollingFileAppender.getImmediateFlush());
+ assertTrue("immediateFlush", dailyRollingFileAppender.getImmediateFlush());
// FileAppender
final FileAppender fileAppender = config.getAppender("FileAppender");
assertNotNull(fileAppender);
- assertEquals("append", true, getAppendProperty(fileAppender));
+ assertTrue("append", getAppendProperty(fileAppender));
assertEquals("bufferSize", 8192, fileAppender.getManager().getBufferSize());
- assertEquals("immediateFlush", true, fileAppender.getImmediateFlush());
+ assertTrue("immediateFlush", fileAppender.getImmediateFlush());
// RollingFileAppender
final RollingFileAppender rollingFileAppender = config.getAppender("RollingFileAppender");
assertNotNull(rollingFileAppender);
@@ -433,9 +415,9 @@ public void testDefaultValues() throws Exception {
final DefaultRolloverStrategy strategy =
(DefaultRolloverStrategy) rollingFileAppender.getManager().getRolloverStrategy();
assertEquals("maxBackupIndex", 1, strategy.getMaxIndex());
- assertEquals("append", true, getAppendProperty(rollingFileAppender));
+ assertTrue("append", getAppendProperty(rollingFileAppender));
assertEquals("bufferSize", 8192, rollingFileAppender.getManager().getBufferSize());
- assertEquals("immediateFlush", true, rollingFileAppender.getImmediateFlush());
+ assertTrue("immediateFlush", rollingFileAppender.getImmediateFlush());
config.start();
config.stop();
}
@@ -443,7 +425,7 @@ public void testDefaultValues() throws Exception {
/**
* Checks a hierarchy of filters.
*
- * @param filter
+ * @param filter A filter
* @return the number of filters
*/
private int checkFilters(final org.apache.logging.log4j.core.Filter filter) {
@@ -467,7 +449,7 @@ private int checkFilters(final org.apache.logging.log4j.core.Filter filter) {
/**
* Checks a hierarchy of filters.
*
- * @param filter
+ * @param filter A filter
* @return the number of filters
*/
private int checkFilters(final org.apache.log4j.spi.Filter filter) {
@@ -581,9 +563,9 @@ protected void testEnhancedRollingFileAppender(final Configuration configuration
appender = configuration.getAppender("DEFAULT_TIME");
assertTrue("is RollingFileAppender", appender instanceof RollingFileAppender);
final RollingFileAppender defaultTime = (RollingFileAppender) appender;
- assertEquals("append", true, defaultTime.getManager().isAppend());
+ assertTrue("append", defaultTime.getManager().isAppend());
assertEquals("bufferSize", 8192, defaultTime.getManager().getBufferSize());
- assertEquals("immediateFlush", true, defaultTime.getImmediateFlush());
+ assertTrue("immediateFlush", defaultTime.getImmediateFlush());
assertEquals("fileName", "target/EnhancedRollingFileAppender/defaultTime.log", defaultTime.getFileName());
assertEquals(
"filePattern",
@@ -595,9 +577,9 @@ protected void testEnhancedRollingFileAppender(final Configuration configuration
appender = configuration.getAppender("DEFAULT_SIZE");
assertTrue("is RollingFileAppender", appender instanceof RollingFileAppender);
final RollingFileAppender defaultSize = (RollingFileAppender) appender;
- assertEquals("append", true, defaultSize.getManager().isAppend());
+ assertTrue("append", defaultSize.getManager().isAppend());
assertEquals("bufferSize", 8192, defaultSize.getManager().getBufferSize());
- assertEquals("immediateFlush", true, defaultSize.getImmediateFlush());
+ assertTrue("immediateFlush", defaultSize.getImmediateFlush());
assertEquals("fileName", "target/EnhancedRollingFileAppender/defaultSize.log", defaultSize.getFileName());
assertEquals(
"filePattern", "target/EnhancedRollingFileAppender/defaultSize.%i.log", defaultSize.getFilePattern());
@@ -613,9 +595,9 @@ protected void testEnhancedRollingFileAppender(final Configuration configuration
appender = configuration.getAppender("TIME");
assertTrue("is RollingFileAppender", appender instanceof RollingFileAppender);
final RollingFileAppender time = (RollingFileAppender) appender;
- assertEquals("append", false, time.getManager().isAppend());
+ assertFalse("append", time.getManager().isAppend());
assertEquals("bufferSize", 1000, time.getManager().getBufferSize());
- assertEquals("immediateFlush", false, time.getImmediateFlush());
+ assertFalse("immediateFlush", time.getImmediateFlush());
assertEquals("fileName", "target/EnhancedRollingFileAppender/time.log", time.getFileName());
assertEquals(
"filePattern", "target/EnhancedRollingFileAppender/time.%d{yyyy-MM-dd}.log", time.getFilePattern());
@@ -625,9 +607,9 @@ protected void testEnhancedRollingFileAppender(final Configuration configuration
appender = configuration.getAppender("SIZE");
assertTrue("is RollingFileAppender", appender instanceof RollingFileAppender);
final RollingFileAppender size = (RollingFileAppender) appender;
- assertEquals("append", false, size.getManager().isAppend());
+ assertFalse("append", size.getManager().isAppend());
assertEquals("bufferSize", 1000, size.getManager().getBufferSize());
- assertEquals("immediateFlush", false, size.getImmediateFlush());
+ assertFalse("immediateFlush", size.getImmediateFlush());
assertEquals("fileName", "target/EnhancedRollingFileAppender/size.log", size.getFileName());
assertEquals("filePattern", "target/EnhancedRollingFileAppender/size.%i.log", size.getFilePattern());
policy = size.getTriggeringPolicy();
diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml
index 56f7fd885f3..8c8cbc19bbd 100644
--- a/log4j-core-test/pom.xml
+++ b/log4j-core-test/pom.xml
@@ -242,13 +242,6 @@
test
-
-
- org.fusesource.jansi
- jansi
- test
-
-
javax.jms
diff --git a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/categories/Layouts.java b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/categories/Layouts.java
index 0cdc8b45407..89057c90c08 100644
--- a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/categories/Layouts.java
+++ b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/categories/Layouts.java
@@ -22,8 +22,6 @@
public interface Layouts {
interface Csv {}
- interface Jansi {}
-
interface Json {}
interface Xml {}
diff --git a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/categories/package-info.java b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/categories/package-info.java
index a299ed2b4c9..fbbe4172853 100644
--- a/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/categories/package-info.java
+++ b/log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/categories/package-info.java
@@ -20,8 +20,10 @@
* integration tests, an appropriate category interface should be specified.
*/
@Export
-@Version("2.20.1")
+@Version("2.20.2")
+@BaselineIgnore("2.25.0")
package org.apache.logging.log4j.core.test.categories;
+import aQute.bnd.annotation.baseline.BaselineIgnore;
import org.osgi.annotation.bundle.Export;
import org.osgi.annotation.versioning.Version;
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiMessagesMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiMessagesMain.java
index 3088ca2bc1b..a44d064ab49 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiMessagesMain.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiMessagesMain.java
@@ -30,7 +30,7 @@
*
*/
public class ConsoleAppenderDefaultSuppressedThrowable {
@@ -41,12 +41,11 @@ public class ConsoleAppenderDefaultSuppressedThrowable {
public static void main(final String[] args) {
final String config =
args.length == 0 ? "target/test-classes/log4j2-console-default-suppressed-throwable.xml" : args[0];
- test(args, config);
+ test(config);
}
- static void test(final String[] args, final String config) {
- // System.out.println(System.getProperty("java.class.path"));
- try (final LoggerContext ctx =
+ static void test(final String config) {
+ try (final LoggerContext ignored =
Configurator.initialize(ConsoleAppenderDefaultSuppressedThrowable.class.getName(), config)) {
final IOException ioEx = new IOException("test suppressed");
ioEx.addSuppressed(new IOException("test suppressed 1", new IOException("test 1")));
@@ -55,8 +54,6 @@ static void test(final String[] args, final String config) {
ioEx.addSuppressed(new IOException("test suppressed 2", ioEx2));
final IOException e = new IOException("test", ioEx);
LOG.error("Error message {}, suppressed?", "Hi", e);
- System.out.println("printStackTrace");
- e.printStackTrace();
}
}
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderHighlightLayoutDefaultMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderHighlightLayoutDefaultMain.java
index 7a1b7cf616e..d9b5eeba270 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderHighlightLayoutDefaultMain.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderHighlightLayoutDefaultMain.java
@@ -31,8 +31,7 @@ public class ConsoleAppenderHighlightLayoutDefaultMain {
private static final Logger LOG = LogManager.getLogger(ConsoleAppenderHighlightLayoutDefaultMain.class);
public static void main(final String[] args) {
- System.setProperty("log4j.skipJansi", "false"); // LOG4J2-2087: explicitly enable
- try (final LoggerContext ctx = Configurator.initialize(
+ try (final LoggerContext ignored = Configurator.initialize(
ConsoleAppenderAnsiMessagesMain.class.getName(),
"target/test-classes/log4j2-console-highlight-default.xml")) {
LOG.fatal("Fatal message.");
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderHighlightLayoutMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderHighlightLayoutMain.java
index dbb6958ea25..ad86c245707 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderHighlightLayoutMain.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderHighlightLayoutMain.java
@@ -31,8 +31,7 @@ public class ConsoleAppenderHighlightLayoutMain {
private static final Logger LOG = LogManager.getLogger(ConsoleAppenderHighlightLayoutMain.class);
public static void main(final String[] args) {
- System.setProperty("log4j.skipJansi", "false"); // LOG4J2-2087: explicitly enable
- try (final LoggerContext ctx = Configurator.initialize(
+ try (final LoggerContext ignored = Configurator.initialize(
ConsoleAppenderAnsiMessagesMain.class.getName(), "target/test-classes/log4j2-console-highlight.xml")) {
LOG.fatal("Fatal message.");
LOG.error("Error message.");
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJAnsiMessageMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJAnsiMessageMain.java
deleted file mode 100644
index 56cce35a74b..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJAnsiMessageMain.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.core.appender;
-
-import static org.fusesource.jansi.Ansi.Color.CYAN;
-import static org.fusesource.jansi.Ansi.Color.RED;
-import static org.fusesource.jansi.Ansi.ansi;
-
-import java.util.Map.Entry;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.config.Configurator;
-import org.apache.logging.log4j.core.test.categories.Layouts;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.jupiter.api.parallel.ResourceLock;
-import org.junit.jupiter.api.parallel.Resources;
-
-/**
- * Shows how to use ANSI escape codes to color messages. Each message is printed to the console in color, but the rest
- * of the log entry (time stamp for example) is in the default color for that console.
- *
- * Running from a Windows command line from the root of the project:
- *
- *
- *
- * mvn -Dtest=org.apache.logging.log4j.core.appender.ConsoleAppenderJAnsiMessageMain test
- *
* The default syntax for embedded ANSI codes is:
- *
+ *
*
* @|code(,code)* text|@
*
@@ -72,279 +75,245 @@
* logger.info("@|KeyStyle {}|@ = @|ValueStyle {}|@", entry.getKey(), entry.getValue());
*
*
- * Note: This class originally copied and then heavily modified code from JAnsi's AnsiRenderer (which is licensed as
- * Apache 2.0.)
- *
- * @see AnsiRenderer
+ *
+ * Note: this class was originally copied and then heavily modified from
+ * JAnsi/JLine AnsiRenderer,
+ * licensed under an Apache Software License, version 2.0.
+ *
*/
public final class JAnsiTextRenderer implements TextRenderer {
- public static final Map DefaultExceptionStyleMap;
- static final Map DefaultMessageStyleMap;
- private static final Map> PrefedinedStyleMaps;
+ private static final Logger LOGGER = StatusLogger.getLogger();
+
+ public static final Map DefaultExceptionStyleMap;
+ static final Map DEFAULT_MESSAGE_STYLE_MAP;
+ private static final Map> PREFEDINED_STYLE_MAPS;
- private static void put(final Map map, final String name, final Code... codes) {
- map.put(name, codes);
+ private static final String BEGIN_TOKEN = "@|";
+ private static final String END_TOKEN = "|@";
+ // The length of AnsiEscape.CSI
+ private static final int CSI_LENGTH = 2;
+
+ private static Map.Entry entry(final String name, final AnsiEscape... codes) {
+ final StringBuilder sb = new StringBuilder(AnsiEscape.CSI.getCode());
+ for (final AnsiEscape code : codes) {
+ sb.append(code.getCode());
+ }
+ return new AbstractMap.SimpleImmutableEntry<>(name, sb.toString());
+ }
+
+ @SafeVarargs
+ private static Map ofEntries(final Map.Entry... entries) {
+ final Map map = new HashMap<>(entries.length);
+ for (final Map.Entry entry : entries) {
+ map.put(entry.getKey(), entry.getValue());
+ }
+ return Collections.unmodifiableMap(map);
}
static {
- final Map> tempPreDefs = new HashMap<>();
// Default style: Spock
- {
- // TODO Should the keys be in an enum?
- final Map map = new HashMap<>();
- put(map, "Prefix", WHITE);
- put(map, "Name", BG_RED, WHITE);
- put(map, "NameMessageSeparator", BG_RED, WHITE);
- put(map, "Message", BG_RED, WHITE, BOLD);
- put(map, "At", WHITE);
- put(map, "CauseLabel", WHITE);
- put(map, "Text", WHITE);
- put(map, "More", WHITE);
- put(map, "Suppressed", WHITE);
- // StackTraceElement
- put(map, "StackTraceElement.ClassLoaderName", WHITE);
- put(map, "StackTraceElement.ClassLoaderSeparator", WHITE);
- put(map, "StackTraceElement.ModuleName", WHITE);
- put(map, "StackTraceElement.ModuleVersionSeparator", WHITE);
- put(map, "StackTraceElement.ModuleVersion", WHITE);
- put(map, "StackTraceElement.ModuleNameSeparator", WHITE);
- put(map, "StackTraceElement.ClassName", YELLOW);
- put(map, "StackTraceElement.ClassMethodSeparator", YELLOW);
- put(map, "StackTraceElement.MethodName", YELLOW);
- put(map, "StackTraceElement.NativeMethod", YELLOW);
- put(map, "StackTraceElement.FileName", RED);
- put(map, "StackTraceElement.LineNumber", RED);
- put(map, "StackTraceElement.Container", RED);
- put(map, "StackTraceElement.ContainerSeparator", WHITE);
- put(map, "StackTraceElement.UnknownSource", RED);
- // ExtraClassInfo
- put(map, "ExtraClassInfo.Inexact", YELLOW);
- put(map, "ExtraClassInfo.Container", YELLOW);
- put(map, "ExtraClassInfo.ContainerSeparator", YELLOW);
- put(map, "ExtraClassInfo.Location", YELLOW);
- put(map, "ExtraClassInfo.Version", YELLOW);
- // Save
- DefaultExceptionStyleMap = Collections.unmodifiableMap(map);
- tempPreDefs.put("Spock", DefaultExceptionStyleMap);
- }
+ final Map spock = ofEntries(
+ entry("Prefix", WHITE),
+ entry("Name", BG_RED, WHITE),
+ entry("NameMessageSeparator", BG_RED, WHITE),
+ entry("Message", BG_RED, WHITE, BOLD),
+ entry("At", WHITE),
+ entry("CauseLabel", WHITE),
+ entry("Text", WHITE),
+ entry("More", WHITE),
+ entry("Suppressed", WHITE),
+ // StackTraceElement
+ entry("StackTraceElement.ClassLoaderName", WHITE),
+ entry("StackTraceElement.ClassLoaderSeparator", WHITE),
+ entry("StackTraceElement.ModuleName", WHITE),
+ entry("StackTraceElement.ModuleVersionSeparator", WHITE),
+ entry("StackTraceElement.ModuleVersion", WHITE),
+ entry("StackTraceElement.ModuleNameSeparator", WHITE),
+ entry("StackTraceElement.ClassName", YELLOW),
+ entry("StackTraceElement.ClassMethodSeparator", YELLOW),
+ entry("StackTraceElement.MethodName", YELLOW),
+ entry("StackTraceElement.NativeMethod", YELLOW),
+ entry("StackTraceElement.FileName", RED),
+ entry("StackTraceElement.LineNumber", RED),
+ entry("StackTraceElement.Container", RED),
+ entry("StackTraceElement.ContainerSeparator", WHITE),
+ entry("StackTraceElement.UnknownSource", RED),
+ // ExtraClassInfo
+ entry("ExtraClassInfo.Inexact", YELLOW),
+ entry("ExtraClassInfo.Container", YELLOW),
+ entry("ExtraClassInfo.ContainerSeparator", YELLOW),
+ entry("ExtraClassInfo.Location", YELLOW),
+ entry("ExtraClassInfo.Version", YELLOW));
+
// Style: Kirk
- {
- // TODO Should the keys be in an enum?
- final Map map = new HashMap<>();
- put(map, "Prefix", WHITE);
- put(map, "Name", BG_RED, YELLOW, BOLD);
- put(map, "NameMessageSeparator", BG_RED, YELLOW);
- put(map, "Message", BG_RED, WHITE, BOLD);
- put(map, "At", WHITE);
- put(map, "CauseLabel", WHITE);
- put(map, "Text", WHITE);
- put(map, "More", WHITE);
- put(map, "Suppressed", WHITE);
- // StackTraceElement
- put(map, "StackTraceElement.ClassLoaderName", WHITE);
- put(map, "StackTraceElement.ClassLoaderSeparator", WHITE);
- put(map, "StackTraceElement.ModuleName", WHITE);
- put(map, "StackTraceElement.ModuleVersionSeparator", WHITE);
- put(map, "StackTraceElement.ModuleVersion", WHITE);
- put(map, "StackTraceElement.ModuleNameSeparator", WHITE);
- put(map, "StackTraceElement.ClassName", BG_RED, WHITE);
- put(map, "StackTraceElement.ClassMethodSeparator", BG_RED, YELLOW);
- put(map, "StackTraceElement.MethodName", BG_RED, YELLOW);
- put(map, "StackTraceElement.NativeMethod", BG_RED, YELLOW);
- put(map, "StackTraceElement.FileName", RED);
- put(map, "StackTraceElement.LineNumber", RED);
- put(map, "StackTraceElement.Container", RED);
- put(map, "StackTraceElement.ContainerSeparator", WHITE);
- put(map, "StackTraceElement.UnknownSource", RED);
- // ExtraClassInfo
- put(map, "ExtraClassInfo.Inexact", YELLOW);
- put(map, "ExtraClassInfo.Container", WHITE);
- put(map, "ExtraClassInfo.ContainerSeparator", WHITE);
- put(map, "ExtraClassInfo.Location", YELLOW);
- put(map, "ExtraClassInfo.Version", YELLOW);
- // Save
- tempPreDefs.put("Kirk", Collections.unmodifiableMap(map));
- }
- {
- final Map temp = new HashMap<>();
- // TODO
- DefaultMessageStyleMap = Collections.unmodifiableMap(temp);
- }
- PrefedinedStyleMaps = Collections.unmodifiableMap(tempPreDefs);
+ final Map kirk = ofEntries(
+ entry("Prefix", WHITE),
+ entry("Name", BG_RED, YELLOW, BOLD),
+ entry("NameMessageSeparator", BG_RED, YELLOW),
+ entry("Message", BG_RED, WHITE, BOLD),
+ entry("At", WHITE),
+ entry("CauseLabel", WHITE),
+ entry("Text", WHITE),
+ entry("More", WHITE),
+ entry("Suppressed", WHITE),
+ // StackTraceElement
+ entry("StackTraceElement.ClassLoaderName", WHITE),
+ entry("StackTraceElement.ClassLoaderSeparator", WHITE),
+ entry("StackTraceElement.ModuleName", WHITE),
+ entry("StackTraceElement.ModuleVersionSeparator", WHITE),
+ entry("StackTraceElement.ModuleVersion", WHITE),
+ entry("StackTraceElement.ModuleNameSeparator", WHITE),
+ entry("StackTraceElement.ClassName", BG_RED, WHITE),
+ entry("StackTraceElement.ClassMethodSeparator", BG_RED, YELLOW),
+ entry("StackTraceElement.MethodName", BG_RED, YELLOW),
+ entry("StackTraceElement.NativeMethod", BG_RED, YELLOW),
+ entry("StackTraceElement.FileName", RED),
+ entry("StackTraceElement.LineNumber", RED),
+ entry("StackTraceElement.Container", RED),
+ entry("StackTraceElement.ContainerSeparator", WHITE),
+ entry("StackTraceElement.UnknownSource", RED),
+ // ExtraClassInfo
+ entry("ExtraClassInfo.Inexact", YELLOW),
+ entry("ExtraClassInfo.Container", WHITE),
+ entry("ExtraClassInfo.ContainerSeparator", WHITE),
+ entry("ExtraClassInfo.Location", YELLOW),
+ entry("ExtraClassInfo.Version", YELLOW));
+
+ // Save
+ DefaultExceptionStyleMap = spock;
+ DEFAULT_MESSAGE_STYLE_MAP = Collections.emptyMap();
+ Map> predefinedStyleMaps = new HashMap<>();
+ predefinedStyleMaps.put("Spock", spock);
+ predefinedStyleMaps.put("Kirk", kirk);
+ PREFEDINED_STYLE_MAPS = Collections.unmodifiableMap(predefinedStyleMaps);
}
private final String beginToken;
private final int beginTokenLen;
private final String endToken;
private final int endTokenLen;
- private final Map styleMap;
+ private final Map styleMap;
- public JAnsiTextRenderer(final String[] formats, final Map defaultStyleMap) {
- String tempBeginToken = AnsiRenderer.BEGIN_TOKEN;
- String tempEndToken = AnsiRenderer.END_TOKEN;
- final Map map;
+ public JAnsiTextRenderer(final String[] formats, final Map defaultStyleMap) {
+ // The format string is a list of whitespace-separated expressions:
+ // Key=AnsiEscape(,AnsiEscape)*
if (formats.length > 1) {
- final String allStylesStr = formats[1];
- // Style def split
- final String[] allStyleAssignmentsArr = allStylesStr.split(" ");
- map = new HashMap<>(allStyleAssignmentsArr.length + defaultStyleMap.size());
- map.putAll(defaultStyleMap);
- for (final String styleAssignmentStr : allStyleAssignmentsArr) {
- final String[] styleAssignmentArr = styleAssignmentStr.split("=");
- if (styleAssignmentArr.length != 2) {
- StatusLogger.getLogger()
- .warn(
- "{} parsing style \"{}\", expected format: StyleName=Code(,Code)*",
- getClass().getSimpleName(),
- styleAssignmentStr);
+ final String stylesStr = formats[1];
+ final Map map = AnsiEscape.createMap(
+ stylesStr.split("\\s", -1), new String[] {"BeginToken", "EndToken", "Style"}, ",");
+
+ // Handle the special tokens
+ beginToken = Objects.toString(map.remove("BeginToken"), BEGIN_TOKEN);
+ endToken = Objects.toString(map.remove("EndToken"), END_TOKEN);
+ final String predefinedStyle = map.remove("Style");
+
+ // Create style map
+ final Map styleMap = new HashMap<>(map.size() + defaultStyleMap.size());
+ defaultStyleMap.forEach((k, v) -> styleMap.put(toRootUpperCase(k), v));
+ if (predefinedStyle != null) {
+ final Map predefinedMap = PREFEDINED_STYLE_MAPS.get(predefinedStyle);
+ if (predefinedMap != null) {
+ map.putAll(predefinedMap);
} else {
- final String styleName = styleAssignmentArr[0];
- final String codeListStr = styleAssignmentArr[1];
- final String[] codeNames = codeListStr.split(",");
- if (codeNames.length == 0) {
- StatusLogger.getLogger()
- .warn(
- "{} parsing style \"{}\", expected format: StyleName=Code(,Code)*",
- getClass().getSimpleName(),
- styleAssignmentStr);
- } else {
- switch (styleName) {
- case "BeginToken":
- tempBeginToken = codeNames[0];
- break;
- case "EndToken":
- tempEndToken = codeNames[0];
- break;
- case "StyleMapName":
- final String predefinedMapName = codeNames[0];
- final Map predefinedMap = PrefedinedStyleMaps.get(predefinedMapName);
- if (predefinedMap != null) {
- map.putAll(predefinedMap);
- } else {
- StatusLogger.getLogger()
- .warn(
- "Unknown predefined map name {}, pick one of {}",
- predefinedMapName,
- null);
- }
- break;
- default:
- final Code[] codes = new Code[codeNames.length];
- for (int i = 0; i < codes.length; i++) {
- codes[i] = toCode(codeNames[i]);
- }
- map.put(styleName, codes);
- }
- }
+ LOGGER.warn(
+ "Unknown predefined map name {}, pick one of {}",
+ predefinedStyle,
+ PREFEDINED_STYLE_MAPS.keySet());
}
}
+ styleMap.putAll(map);
+ this.styleMap = Collections.unmodifiableMap(styleMap);
} else {
- map = defaultStyleMap;
- }
- styleMap = map;
- beginToken = tempBeginToken;
- endToken = tempEndToken;
- beginTokenLen = tempBeginToken.length();
- endTokenLen = tempEndToken.length();
- }
-
- public Map getStyleMap() {
- return styleMap;
- }
-
- private void render(final Ansi ansi, final Code code) {
- if (code.isColor()) {
- if (code.isBackground()) {
- ansi.bg(code.getColor());
- } else {
- ansi.fg(code.getColor());
- }
- } else if (code.isAttribute()) {
- ansi.a(code.getAttribute());
- }
- }
-
- private void render(final Ansi ansi, final Code... codes) {
- for (final Code code : codes) {
- render(ansi, code);
+ beginToken = BEGIN_TOKEN;
+ endToken = END_TOKEN;
+ this.styleMap = Collections.unmodifiableMap(defaultStyleMap);
}
+ beginTokenLen = beginToken.length();
+ endTokenLen = endToken.length();
}
/**
- * Renders the given text with the given names which can be ANSI code names or Log4j style names.
+ * Renders the given input with the given names which can be ANSI code names or Log4j style names.
*
- * @param text
- * The text to render
- * @param names
+ * @param input
+ * The input to render
+ * @param styleNames
* ANSI code names or Log4j style names.
- * @return A rendered string containing ANSI codes.
*/
- private String render(final String text, final String... names) {
- final Ansi ansi = Ansi.ansi();
- for (final String name : names) {
- final Code[] codes = styleMap.get(name);
- if (codes != null) {
- render(ansi, codes);
+ private void render(final String input, final StringBuilder output, final String... styleNames) {
+ boolean first = true;
+ for (final String styleName : styleNames) {
+ final String escape = styleMap.get(toRootUpperCase(styleName));
+ if (escape != null) {
+ merge(escape, output, first);
} else {
- render(ansi, toCode(name));
+ merge(AnsiEscape.createSequence(styleName), output, first);
}
+ first = false;
+ }
+ output.append(input).append(AnsiEscape.getDefaultStyle());
+ }
+
+ private static void merge(final String escapeSequence, final StringBuilder output, final boolean first) {
+ if (first) {
+ output.append(escapeSequence);
+ } else {
+ // Delete the trailing AnsiEscape.SUFFIX
+ output.setLength(output.length() - 1);
+ output.append(AnsiEscape.SEPARATOR.getCode());
+ output.append(escapeSequence.substring(CSI_LENGTH));
}
- return ansi.a(text).reset().toString();
}
// EXACT COPY OF StringBuilder version of the method but typed as String for input
@Override
public void render(final String input, final StringBuilder output, final String styleName)
throws IllegalArgumentException {
- output.append(render(input, styleName));
+ render(input, output, styleName.split(",", -1));
}
@Override
public void render(final StringBuilder input, final StringBuilder output) throws IllegalArgumentException {
- int i = 0;
- int j, k;
+ int pos = 0;
+ int beginTokenPos, endTokenPos;
while (true) {
- j = input.indexOf(beginToken, i);
- if (j == -1) {
- if (i == 0) {
- output.append(input);
- return;
- }
- output.append(input.substring(i, input.length()));
+ beginTokenPos = input.indexOf(beginToken, pos);
+ if (beginTokenPos == -1) {
+ output.append(pos == 0 ? input : input.substring(pos, input.length()));
return;
}
- output.append(input.substring(i, j));
- k = input.indexOf(endToken, j);
+ output.append(input.substring(pos, beginTokenPos));
+ endTokenPos = input.indexOf(endToken, beginTokenPos);
- if (k == -1) {
- output.append(input);
+ if (endTokenPos == -1) {
+ LOGGER.warn(
+ "Missing matching end token {} for token at position {}: '{}'", endToken, beginTokenPos, input);
+ output.append(beginTokenPos == 0 ? input : input.substring(beginTokenPos, input.length()));
return;
}
- j += beginTokenLen;
- final String spec = input.substring(j, k);
+ beginTokenPos += beginTokenLen;
+ final String spec = input.substring(beginTokenPos, endTokenPos);
- final String[] items = spec.split(AnsiRenderer.CODE_TEXT_SEPARATOR, 2);
+ final String[] items = spec.split("\\s", 2);
if (items.length == 1) {
- output.append(input);
- return;
+ LOGGER.warn("Missing argument in ANSI escape specification '{}'", spec);
+ output.append(beginToken).append(spec).append(endToken);
+ } else {
+ render(items[1], output, items[0].split(",", -1));
}
- final String replacement = render(items[1], items[0].split(","));
-
- output.append(replacement);
-
- i = k + endTokenLen;
+ pos = endTokenPos + endTokenLen;
}
}
- private Code toCode(final String name) {
- return Code.valueOf(toRootUpperCase(name));
+ public Map getStyleMap() {
+ return styleMap;
}
@Override
public String toString() {
- return "JAnsiMessageRenderer [beginToken=" + beginToken + ", beginTokenLen=" + beginTokenLen + ", endToken="
+ return "AnsiMessageRenderer [beginToken=" + beginToken + ", beginTokenLen=" + beginTokenLen + ", endToken="
+ endToken + ", endTokenLen=" + endTokenLen + ", styleMap=" + styleMap + "]";
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MessagePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MessagePatternConverter.java
index 44016664afd..dd7df75faff 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MessagePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MessagePatternConverter.java
@@ -23,10 +23,8 @@
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
-import org.apache.logging.log4j.core.util.Loader;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.MultiformatMessage;
-import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable;
import org.apache.logging.log4j.util.PerformanceSensitive;
import org.apache.logging.log4j.util.StringBuilderFormattable;
@@ -52,12 +50,7 @@ private static TextRenderer loadMessageRenderer(final String[] options) {
for (final String option : options) {
switch (toRootUpperCase(option)) {
case "ANSI":
- if (Loader.isJansiAvailable()) {
- return new JAnsiTextRenderer(options, JAnsiTextRenderer.DefaultMessageStyleMap);
- }
- StatusLogger.getLogger()
- .warn("You requested ANSI message rendering but JANSI is not on the classpath.");
- return null;
+ return new JAnsiTextRenderer(options, JAnsiTextRenderer.DEFAULT_MESSAGE_STYLE_MAP);
case "HTML":
return new HtmlTextRenderer(options);
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
index 5c047848b57..ac6407f47b3 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
@@ -18,7 +18,7 @@
* Provides classes implementing format specifiers in conversion patterns.
*/
@Export
-@Version("2.25.0")
+@Version("2.24.1")
package org.apache.logging.log4j.core.pattern;
import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
index 64ee6be3599..4ae5d463353 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
@@ -349,6 +349,10 @@ public static boolean isClassAvailable(final String className) {
}
}
+ /**
+ * @deprecated Since 2.25.0 without a replacement.
+ */
+ @Deprecated
public static boolean isJansiAvailable() {
return isClassAvailable("org.fusesource.jansi.AnsiRenderer");
}
diff --git a/log4j-jakarta-smtp/src/test/java/org/apache/logging/log4j/smtp/SmtpAppenderAsyncTest.java b/log4j-jakarta-smtp/src/test/java/org/apache/logging/log4j/smtp/SmtpAppenderAsyncTest.java
index 0dbd426d30e..f460c5c6079 100644
--- a/log4j-jakarta-smtp/src/test/java/org/apache/logging/log4j/smtp/SmtpAppenderAsyncTest.java
+++ b/log4j-jakarta-smtp/src/test/java/org/apache/logging/log4j/smtp/SmtpAppenderAsyncTest.java
@@ -16,8 +16,8 @@
*/
package org.apache.logging.log4j.smtp;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
import java.util.Iterator;
import org.apache.logging.log4j.ThreadContext;
diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index 4c82d04e146..3ae996ef972 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -100,7 +100,6 @@
2.24.0.12.3.3
- 2.4.13.3.40.22.11.7.0
@@ -471,12 +470,6 @@
${jakarta-mail.version}
-
- org.fusesource.jansi
- jansi
- ${jansi.version}
-
-
com.google.code.java-allocation-instrumenter
diff --git a/pom.xml b/pom.xml
index 03760b9eb0a..88bf7db62ff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -345,7 +345,6 @@
4.0.01.11.02.18.0
- 1.181.6.24.0.518.3.12
@@ -769,12 +768,6 @@
pom
-
- org.fusesource.jansi
- jansi
- ${site-jansi.version}
-
-
com.sun.mailjavax.mail
diff --git a/src/changelog/.2.x.x/.release-notes.adoc.ftl b/src/changelog/.2.x.x/.release-notes.adoc.ftl
index 16be1ddfc7f..47b4447337a 100644
--- a/src/changelog/.2.x.x/.release-notes.adoc.ftl
+++ b/src/changelog/.2.x.x/.release-notes.adoc.ftl
@@ -38,4 +38,10 @@ This effectively helped with fixing some bugs by matching the feature parity of
Additionally, rendered stack traces are ensured to be prefixed with a newline, which used to be a whitespace in earlier versions.
The support for the `\{ansi}` option in exception converters is removed too.
+=== ANSI support on Windows
+
+Since 2017, Windows 10 and newer have offered native support for ANSI escapes.
+The support for the outdated Jansi 1.x library has therefore been removed.
+See xref:manual/pattern-layout.adoc#jansi[ANSI styling on Windows] for more information.
+
<#include "../.changelog.adoc.ftl">
diff --git a/src/changelog/.2.x.x/1736_split_jansi_support.xml b/src/changelog/.2.x.x/1736_split_jansi_support.xml
new file mode 100644
index 00000000000..a585fed74f4
--- /dev/null
+++ b/src/changelog/.2.x.x/1736_split_jansi_support.xml
@@ -0,0 +1,8 @@
+
+
+
+ Remove JAnsi library support. Windows 10 console has supported ANSI escapes since 2017.
+
diff --git a/src/changelog/.2.x.x/2916_rewrite_jansi_renderer.xml b/src/changelog/.2.x.x/2916_rewrite_jansi_renderer.xml
new file mode 100644
index 00000000000..7fad82b6d5b
--- /dev/null
+++ b/src/changelog/.2.x.x/2916_rewrite_jansi_renderer.xml
@@ -0,0 +1,8 @@
+
+
+
+ Rewrite `JAnsiTextRenderer` to work without JAnsi library.
+
diff --git a/src/site/antora/antora.tmpl.yml b/src/site/antora/antora.tmpl.yml
index 8ec9a597b23..7cdcf41ecfb 100644
--- a/src/site/antora/antora.tmpl.yml
+++ b/src/site/antora/antora.tmpl.yml
@@ -59,7 +59,6 @@ asciidoc:
disruptor-version: "${site-disruptor.version}"
flume-version: "${site-flume.version}"
jackson-version: "${site-jackson.version}"
- jansi-version: "${site-jansi.version}"
javax-mail-version: "${site-javax-mail.version}"
jctools-version: "${site-jctools.version}"
je-version: "${site-je.version}"
diff --git a/src/site/antora/antora.yml b/src/site/antora/antora.yml
index 21652d7e98f..ff6edf566a9 100644
--- a/src/site/antora/antora.yml
+++ b/src/site/antora/antora.yml
@@ -59,7 +59,6 @@ asciidoc:
disruptor-version: "1.2.3-disruptor"
flume-version: "1.2.3-flume"
jackson-version: "1.2.3-jackson"
- jansi-version: "1.2.3-jansi"
javax-mail-version: "1.2.3-javax-mail"
jctools-version: "1.2.3-jctools"
je-version: "1.2.3-je"
diff --git a/src/site/antora/modules/ROOT/pages/manual/appenders.adoc b/src/site/antora/modules/ROOT/pages/manual/appenders.adoc
index 6f0c4aaad7d..a43675fb547 100644
--- a/src/site/antora/modules/ROOT/pages/manual/appenders.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/appenders.adoc
@@ -216,7 +216,7 @@ They are documented in separate pages based on their target resource:
=== Console Appender
As one might expect, the Console Appender writes its output to either the standard output or standard error output.
-The appender supports four different ways to access the output streams:
+The appender supports three different ways to access the output streams:
`direct`::
This mode gives the best performance.
@@ -236,39 +236,6 @@ This setting might be useful in multi-application environments.
Some application servers modify `System.out` and `System.err` to always point to the currently running application.
====
-`JANSI`::
-If the application is running on Windows and the
-https://fusesource.github.io/jansi/[JANSI library]
-is available, the Console appender will use JANSI to emulate ANSI sequence support.
-This mode can be disabled by setting the
-xref:manual/systemproperties.adoc#log4j2.skipJansi[`log4j2.skipJansi`]
-configuration attribute to `true`.
-+
-Additional runtime dependencies are required to use JANSI:
-+
-[tabs]
-====
-Maven::
-+
-[source,xml,subs="+attributes"]
-----
-
- org.fusesource.jansi
- jansi
- {jansi-version}
-
-
-----
-
-Gradle::
-+
-[source,groovy,subs="+attributes"]
-----
-runtimeOnly 'org.fusesource.jansi:jansi:{jansi-version}'
-----
-
-====
-
[#ConsoleAppender-attributes]
.Console Appender configuration attributes
[cols="1m,1,1,5"]
@@ -311,9 +278,7 @@ If other logging backends or the application itself uses `System.out/System.err`
====
This setting is incompatible with the
-<>
-and
-xref:manual/systemproperties.adoc#log4j2.skipJansi[JANSI support].
+<>.
| [[ConsoleAppender-attr-follow]]
follow
@@ -328,9 +293,7 @@ https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#setOut-java.io.P
Otherwise, the value of `System.out` (resp. `System.err`) at configuration time will be used.
This setting is incompatible with the
-<>
-and
-xref:manual/systemproperties.adoc#log4j2.skipJansi[JANSI support].
+<>.
| [[ConsoleAppender-attr-ignoreExceptions]]
ignoreExceptions
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index 1817b388229..49723986482 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -209,7 +209,7 @@ The optional footer to include at the bottom of each log file
|Default value |`false`
|===
-If `true`, do not output ANSI escape codes
+If `true`, do not output ANSI escape codes.
[#plugin-attr-noConsoleNoAnsi]
==== `noConsoleNoAnsi`
@@ -220,7 +220,7 @@ If `true`, do not output ANSI escape codes
|Default value |`false`
|===
-If `true` and `System.console()` is null, do not output ANSI escape codes
+If `true` and `System.console()` is `null`, do not output ANSI escape codes
[#plugin-elements]
=== Plugin elements
@@ -1598,19 +1598,14 @@ If your terminal supports 24-bit colors, you can specify:
[#jansi]
==== ANSI styling on Windows
-ANSI escape sequences are supported natively on many platforms, but not by default on Windows.
-To enable ANSI support add the
-http://fusesource.github.io/jansi/[Jansi]
-dependency to your application, and set xref:manual/systemproperties.adoc#log4j2.skipJansi[the `log4j2.skipJansi` system property] to `false`.
-This allows Log4j to use Jansi to add ANSI escape codes when writing to the console.
+ANSI escape sequences are supported natively on many platforms, but are disabled by default in `cmd.exe` on Windows.
+To enable ANSI escape sequences, create a registry key named `HKEY_CURRENT_USER\Console\VirtualTerminalLevel` of type `DWORD` and set its value to `0x1`.
-[NOTE]
-====
-Before Log4j 2.10, Jansi was enabled by default.
-The fact that Jansi requires native code means that Jansi can only be loaded by a single class loader.
-For web applications, this means the Jansi jar has to be in the web container's classpath.
-To avoid causing problems for web applications, Log4j no longer automatically tries to load Jansi without explicit configuration from Log4j 2.10 onward.
-====
+See
+https://devblogs.microsoft.com/commandline/understanding-windows-console-host-settings/[Understanding Windows Console Host Settings]
+and
+https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences[Console Virtual Terminal Sequences]
+Microsoft documentation for more details.
[#garbage-free]
=== Garbage-free configuration
diff --git a/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc b/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
index 2bd4b6e75e5..318c745e10a 100644
--- a/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
@@ -149,13 +149,6 @@ include::partial$manual/systemproperties/properties-configuration-factory.adoc[l
include::partial$manual/systemproperties/properties-garbage-collection.adoc[leveloffset=+2]
-[id=properties-jansi]
-=== JANSI
-
-If the https://fusesource.github.io/jansi/[JANSI] library is on the runtime classpath of the application, the following property can be used to control its usage:
-
-include::partial$manual/systemproperties/properties-jansi.adoc[leveloffset=+2]
-
[id=properties-jmx]
=== JMX
diff --git a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-jansi.adoc b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-jansi.adoc
deleted file mode 100644
index 6b58ac8c586..00000000000
--- a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-jansi.adoc
+++ /dev/null
@@ -1,32 +0,0 @@
-////
- 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.
-////
-[id=log4j2.skipJansi]
-== `log4j2.skipJansi`
-
-[cols="1h,5"]
-|===
-| Env. variable | `LOG4J_SKIP_JANSI`
-| Type | `boolean`
-| Default value | `true`
-|===
-
-If the following conditions are satisfied:
-
-* Log4j runs on Windows,
-* this property is set to `false`,
-
-Log4j will use the JANSI library to color the output of the console appender.
\ No newline at end of file
From c45f456453a0490ea13d64852dd2abe04273652f Mon Sep 17 00:00:00 2001
From: ASF Logging Services RM
Date: Fri, 18 Oct 2024 11:12:08 +0000
Subject: [PATCH 06/21] Update `co.elastic.clients:elasticsearch-java` to
version `8.15.3` (#3109)
---
log4j-layout-template-json-test/pom.xml | 2 +-
.../.2.x.x/update_co_elastic_clients_elasticsearch_java.xml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/log4j-layout-template-json-test/pom.xml b/log4j-layout-template-json-test/pom.xml
index 0404e5d1106..36a055f40b5 100644
--- a/log4j-layout-template-json-test/pom.xml
+++ b/log4j-layout-template-json-test/pom.xml
@@ -48,7 +48,7 @@
2. The Docker image version of the ELK-stack
As of 2024-09-16, these all (Maven artifacts and Elastic products) get released with the same version. -->
- 8.15.2
+ 8.15.3
diff --git a/src/changelog/.2.x.x/update_co_elastic_clients_elasticsearch_java.xml b/src/changelog/.2.x.x/update_co_elastic_clients_elasticsearch_java.xml
index 2c82df293a5..5c5122f4084 100644
--- a/src/changelog/.2.x.x/update_co_elastic_clients_elasticsearch_java.xml
+++ b/src/changelog/.2.x.x/update_co_elastic_clients_elasticsearch_java.xml
@@ -3,6 +3,6 @@
xmlns="https://logging.apache.org/xml/ns"
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="updated">
-
- Update `co.elastic.clients:elasticsearch-java` to version `8.15.2`
+
+ Update `co.elastic.clients:elasticsearch-java` to version `8.15.3`
From b3a6fcc1847865d12212ad10eff33325cb7e2ce4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Fri, 25 Oct 2024 13:07:44 +0200
Subject: [PATCH 07/21] Rewrite `InstantPatternDynamicFormatter`
---
.../log4j/core/layout/HtmlLayoutTest.java | 14 -
.../pattern/DatePatternConverterTestBase.java | 200 ++---
.../InstantPatternDynamicFormatterTest.java | 184 ----
.../InstantNumberFormatterTest.java | 2 +-
.../InstantPatternDynamicFormatterTest.java | 399 +++++++++
...PatternThreadLocalCachedFormatterTest.java | 2 +-
.../core/pattern/DatePatternConverter.java | 148 +++-
.../log4j/core/util/datetime/DatePrinter.java | 2 +-
.../core/util/datetime/FastDateFormat.java | 2 +-
.../core/util/datetime/FastDatePrinter.java | 2 +-
.../core/util/datetime/FixedDateFormat.java | 50 +-
.../log4j/core/util/datetime/Format.java | 3 +-
.../log4j/core/util/datetime/FormatCache.java | 2 +-
.../core/util/datetime/package-info.java | 2 +-
.../InstantPatternDynamicFormatter.java | 335 --------
.../{ => instant}/InstantFormatter.java | 2 +-
.../{ => instant}/InstantNumberFormatter.java | 2 +-
.../InstantPatternDynamicFormatter.java | 807 ++++++++++++++++++
.../InstantPatternFormatter.java | 71 +-
.../InstantPatternLegacyFormatter.java | 118 +++
...tantPatternThreadLocalCachedFormatter.java | 4 +-
.../util/internal/instant/package-info.java | 20 +
.../json/resolver/TimestampResolver.java | 6 +-
.../perf/jmh/InstantFormatBenchmark.java | 1 +
...rnDynamicFormatterSequencingBenchmark.java | 96 +++
.../InstantPatternFormatterBenchmark.java} | 74 +-
...stantPatternFormatterImpactBenchmark.java} | 47 +-
src/changelog/.2.x.x/.release-notes.adoc.ftl | 7 +
...ate_AbstractLogger_checkMessageFactory.xml | 2 +-
.../.2.x.x/3121_deprecate_FixedDateFormat.xml | 8 +
src/changelog/.2.x.x/3121_instant_format.xml | 8 +
src/changelog/.index.adoc.ftl | 2 +-
.../fix_StatusLogger_instant_formatting.xml | 2 +-
.../ROOT/pages/manual/pattern-layout.adoc | 32 +-
.../properties-log4j-core-misc.adoc | 14 +
35 files changed, 1873 insertions(+), 797 deletions(-)
delete mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatterTest.java
rename log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/{ => instant}/InstantNumberFormatterTest.java (98%)
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
rename log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/{ => instant}/InstantPatternThreadLocalCachedFormatterTest.java (99%)
delete mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatter.java
rename log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/{ => instant}/InstantFormatter.java (95%)
rename log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/{ => instant}/InstantNumberFormatter.java (98%)
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
rename log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/{ => instant}/InstantPatternFormatter.java (63%)
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternLegacyFormatter.java
rename log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/{ => instant}/InstantPatternThreadLocalCachedFormatter.java (98%)
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
create mode 100644 log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java
rename log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/{DateTimeFormatBenchmark.java => instant/InstantPatternFormatterBenchmark.java} (66%)
rename log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/{DateTimeFormatImpactBenchmark.java => instant/InstantPatternFormatterImpactBenchmark.java} (75%)
create mode 100644 src/changelog/.2.x.x/3121_deprecate_FixedDateFormat.xml
create mode 100644 src/changelog/.2.x.x/3121_instant_format.xml
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/HtmlLayoutTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/HtmlLayoutTest.java
index 6a85b012641..9a222e5145f 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/HtmlLayoutTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/layout/HtmlLayoutTest.java
@@ -43,7 +43,6 @@
import org.apache.logging.log4j.core.test.appender.ListAppender;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.test.junit.UsingAnyThreadContext;
@@ -285,19 +284,6 @@ private void testLayoutWithDatePatternFixedFormat(final FixedFormat format, fina
format.getPattern().replace('n', 'S').replace('X', 'x'), locale);
String expected = zonedDateTime.format(dateTimeFormatter);
- final String offset = zonedDateTime.getOffset().toString();
-
- // Truncate minutes if timeZone format is HH and timeZone has minutes. This is required because according to
- // DateTimeFormatter,
- // One letter outputs just the hour, such as '+01', unless the minute is non-zero in which case the minute is
- // also output, such as '+0130'
- // ref : https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
- if (FixedDateFormat.FixedTimeZoneFormat.HH.equals(format.getFixedTimeZoneFormat())
- && offset.contains(":")
- && !"00".equals(offset.split(":")[1])) {
- expected = expected.substring(0, expected.length() - 2);
- }
-
assertEquals(
"
" + expected + "
",
actual,
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java
index b9680290a01..cdd279c6910 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTestBase.java
@@ -20,6 +20,9 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import java.text.SimpleDateFormat;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
@@ -28,8 +31,6 @@
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.MutableInstant;
import org.apache.logging.log4j.core.util.Constants;
-import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
-import org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedTimeZoneFormat;
import org.apache.logging.log4j.util.Strings;
import org.junit.jupiter.api.Test;
@@ -54,27 +55,13 @@ public long getTimeMillis() {
}
}
- /**
- * SimpleTimePattern for DEFAULT.
- */
- private static final String DEFAULT_PATTERN = FixedDateFormat.FixedFormat.DEFAULT.getPattern();
+ private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS";
- /**
- * ISO8601 string literal.
- */
- private static final String ISO8601 = FixedDateFormat.FixedFormat.ISO8601.name();
+ private static final String ISO8601 = "ISO8601";
- /**
- * ISO8601_OFFSET_DATE_TIME_XX string literal.
- */
- private static final String ISO8601_OFFSET_DATE_TIME_HHMM =
- FixedDateFormat.FixedFormat.ISO8601_OFFSET_DATE_TIME_HHMM.name();
+ private static final String ISO8601_OFFSET_DATE_TIME_HHMM = "ISO8601_OFFSET_DATE_TIME_HHMM";
- /**
- * ISO8601_OFFSET_DATE_TIME_XXX string literal.
- */
- private static final String ISO8601_OFFSET_DATE_TIME_HHCMM =
- FixedDateFormat.FixedFormat.ISO8601_OFFSET_DATE_TIME_HHCMM.name();
+ private static final String ISO8601_OFFSET_DATE_TIME_HHCMM = "ISO8601_OFFSET_DATE_TIME_HHCMM";
private static final String[] ISO8601_FORMAT_OPTIONS = {ISO8601};
@@ -91,14 +78,6 @@ private static Date date(final int year, final int month, final int date) {
return cal.getTime();
}
- private String precisePattern(final String pattern, final int precision) {
- final String search = "SSS";
- final int foundIndex = pattern.indexOf(search);
- final String seconds = pattern.substring(0, foundIndex);
- final String remainder = pattern.substring(foundIndex + search.length());
- return seconds + "nnnnnnnnn".substring(0, precision) + remainder;
- }
-
@Test
void testThreadLocalsConstant() {
assertEquals(threadLocalsEnabled, Constants.ENABLE_THREADLOCALS);
@@ -109,6 +88,7 @@ public void testFormatDateStringBuilderDefaultPattern() {
assertDatePattern(null, date(2001, 1, 1), "2001-02-01 14:15:16,123");
}
+ @SuppressWarnings("deprecation")
@Test
public void testFormatDateStringBuilderIso8601() {
final DatePatternConverter converter = DatePatternConverter.newInstance(ISO8601_FORMAT_OPTIONS);
@@ -121,19 +101,18 @@ public void testFormatDateStringBuilderIso8601() {
@Test
public void testFormatDateStringBuilderIso8601BasicWithPeriod() {
- assertDatePattern(
- FixedDateFormat.FixedFormat.ISO8601_BASIC_PERIOD.name(), date(2001, 1, 1), "20010201T141516.123");
+ assertDatePattern("ISO8601_BASIC_PERIOD", date(2001, 1, 1), "20010201T141516.123");
}
@Test
public void testFormatDateStringBuilderIso8601WithPeriod() {
- assertDatePattern(
- FixedDateFormat.FixedFormat.ISO8601_PERIOD.name(), date(2001, 1, 1), "2001-02-01T14:15:16.123");
+ assertDatePattern("ISO8601_PERIOD", date(2001, 1, 1), "2001-02-01T14:15:16.123");
}
+ @SuppressWarnings("deprecation")
@Test
public void testFormatDateStringBuilderIso8601WithPeriodMicroseconds() {
- final String[] pattern = {FixedDateFormat.FixedFormat.ISO8601_PERIOD_MICROS.name(), "Z"};
+ final String[] pattern = {"ISO8601_PERIOD_MICROS", "Z"};
final DatePatternConverter converter = DatePatternConverter.newInstance(pattern);
final StringBuilder sb = new StringBuilder();
final MutableInstant instant = new MutableInstant();
@@ -180,11 +159,12 @@ public void testFormatAmericanPatterns() {
assertDatePattern("US_MONTH_DAY_YEAR4_TIME", date, "11/03/2011 14:15:16.123");
assertDatePattern("US_MONTH_DAY_YEAR2_TIME", date, "11/03/11 14:15:16.123");
assertDatePattern("dd/MM/yyyy HH:mm:ss.SSS", date, "11/03/2011 14:15:16.123");
- assertDatePattern("dd/MM/yyyy HH:mm:ss.nnnnnn", date, "11/03/2011 14:15:16.123000");
+ assertDatePattern("dd/MM/yyyy HH:mm:ss.SSSSSS", date, "11/03/2011 14:15:16.123000");
assertDatePattern("dd/MM/yy HH:mm:ss.SSS", date, "11/03/11 14:15:16.123");
- assertDatePattern("dd/MM/yy HH:mm:ss.nnnnnn", date, "11/03/11 14:15:16.123000");
+ assertDatePattern("dd/MM/yy HH:mm:ss.SSSSSS", date, "11/03/11 14:15:16.123000");
}
+ @SuppressWarnings("deprecation")
private static void assertDatePattern(final String format, final Date date, final String expected) {
final DatePatternConverter converter = DatePatternConverter.newInstance(new String[] {format});
final StringBuilder sb = new StringBuilder();
@@ -219,9 +199,9 @@ public void testFormatLogEventStringBuilderIso8601TimezoneOffsetHHCMM() {
final StringBuilder sb = new StringBuilder();
converter.format(event, sb);
- final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
- final String format = sdf.format(new Date(event.getTimeMillis()));
- final String expected = format.endsWith("Z") ? format.substring(0, format.length() - 1) + "+00:00" : format;
+ final String expected = DateTimeFormatter.ofPattern(converter.getPattern())
+ .withZone(ZoneId.systemDefault())
+ .format((TemporalAccessor) event.getInstant());
assertEquals(expected, sb.toString());
}
@@ -233,9 +213,9 @@ public void testFormatLogEventStringBuilderIso8601TimezoneOffsetHHMM() {
final StringBuilder sb = new StringBuilder();
converter.format(event, sb);
- final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
- final String format = sdf.format(new Date(event.getTimeMillis()));
- final String expected = format.endsWith("Z") ? format.substring(0, format.length() - 1) + "+0000" : format;
+ final String expected = DateTimeFormatter.ofPattern(converter.getPattern())
+ .withZone(ZoneId.systemDefault())
+ .format((TemporalAccessor) event.getInstant());
assertEquals(expected, sb.toString());
}
@@ -311,7 +291,7 @@ public void testGetPatternReturnsDefaultForEmptyOptionsArray() {
@Test
public void testGetPatternReturnsDefaultForInvalidPattern() {
- final String[] invalid = {"ABC I am not a valid date pattern"};
+ final String[] invalid = {"A single `V` is not allow by `DateTimeFormatter` and should cause an exception"};
assertEquals(DEFAULT_PATTERN, DatePatternConverter.newInstance(invalid).getPattern());
}
@@ -344,126 +324,52 @@ public void testGetPatternReturnsNullForUnixMillis() {
assertNull(DatePatternConverter.newInstance(options).getPattern());
}
- @Test
- public void testInvalidLongPatternIgnoresExcessiveDigits() {
- final StringBuilder preciseBuilder = new StringBuilder();
- final StringBuilder milliBuilder = new StringBuilder();
- final LogEvent event = new MyLogEvent();
-
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- final String pattern = format.getPattern();
- final String search = "SSS";
- final int foundIndex = pattern.indexOf(search);
- if (pattern.endsWith("n") || pattern.matches(".+n+X*") || pattern.matches(".+n+Z*")) {
- // ignore patterns that already have precise time formats
- // ignore patterns that do not use seconds.
- continue;
- }
- preciseBuilder.setLength(0);
- milliBuilder.setLength(0);
-
- final DatePatternConverter preciseConverter;
- final String precisePattern;
- if (foundIndex < 0) {
- precisePattern = pattern;
- } else {
- final String subPattern = pattern.substring(0, foundIndex);
- final String remainder = pattern.substring(foundIndex + search.length());
- precisePattern = subPattern + "nnnnnnnnn" + "n" + remainder; // nanos too long
- }
- preciseConverter = DatePatternConverter.newInstance(new String[] {precisePattern});
- preciseConverter.format(event, preciseBuilder);
-
- final String[] milliOptions = {pattern};
- DatePatternConverter.newInstance(milliOptions).format(event, milliBuilder);
- final FixedTimeZoneFormat timeZoneFormat = format.getFixedTimeZoneFormat();
- final int truncateLen = 3 + (timeZoneFormat != null ? timeZoneFormat.getLength() : 0);
- final String tz = timeZoneFormat != null
- ? milliBuilder.substring(milliBuilder.length() - timeZoneFormat.getLength(), milliBuilder.length())
- : Strings.EMPTY;
- milliBuilder.setLength(milliBuilder.length() - truncateLen); // truncate millis
- if (foundIndex >= 0) {
- milliBuilder.append("987123456");
- }
- final String expected = milliBuilder.append(tz).toString();
-
- assertEquals(
- expected,
- preciseBuilder.toString(),
- "format = " + format + ", pattern = " + pattern + ", precisePattern = " + precisePattern);
- // System.out.println(preciseOptions[0] + ": " + precise);
- }
- }
-
@Test
public void testNewInstanceAllowsNullParameter() {
DatePatternConverter.newInstance(null); // no errors
}
- // test with all formats from one 'n' (100s of millis) to 'nnnnnnnnn' (nanosecond precision)
- @Test
- public void testPredefinedFormatWithAnyValidNanoPrecision() {
- final StringBuilder preciseBuilder = new StringBuilder();
- final StringBuilder milliBuilder = new StringBuilder();
- final LogEvent event = new MyLogEvent();
-
- for (final String timeZone : new String[] {"PST", null}) { // Pacific Standard Time=UTC-8:00
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- for (int i = 1; i <= 9; i++) {
- final String pattern = format.getPattern();
- if (pattern.endsWith("n")
- || pattern.matches(".+n+X*")
- || pattern.matches(".+n+Z*")
- || !pattern.contains("SSS")) {
- // ignore patterns that already have precise time formats
- // ignore patterns that do not use seconds.
- continue;
- }
- preciseBuilder.setLength(0);
- milliBuilder.setLength(0);
-
- final String precisePattern = precisePattern(pattern, i);
- final String[] preciseOptions = {precisePattern, timeZone};
- final DatePatternConverter preciseConverter = DatePatternConverter.newInstance(preciseOptions);
- preciseConverter.format(event, preciseBuilder);
-
- final String[] milliOptions = {pattern, timeZone};
- DatePatternConverter.newInstance(milliOptions).format(event, milliBuilder);
- final FixedTimeZoneFormat timeZoneFormat = format.getFixedTimeZoneFormat();
- final int truncateLen = 3 + (timeZoneFormat != null ? timeZoneFormat.getLength() : 0);
- final String tz = timeZoneFormat != null
- ? milliBuilder.substring(
- milliBuilder.length() - timeZoneFormat.getLength(), milliBuilder.length())
- : Strings.EMPTY;
- milliBuilder.setLength(milliBuilder.length() - truncateLen); // truncate millis
- final String expected =
- milliBuilder.append("987123456", 0, i).append(tz).toString();
-
- assertEquals(
- expected,
- preciseBuilder.toString(),
- "format = " + format + ", pattern = " + pattern + ", precisePattern = " + precisePattern);
- // System.out.println(preciseOptions[0] + ": " + precise);
- }
- }
- }
- }
+ private static final String[] PATTERN_NAMES = {
+ "ABSOLUTE",
+ "ABSOLUTE_MICROS",
+ "ABSOLUTE_NANOS",
+ "ABSOLUTE_PERIOD",
+ "COMPACT",
+ "DATE",
+ "DATE_PERIOD",
+ "DEFAULT",
+ "DEFAULT_MICROS",
+ "DEFAULT_NANOS",
+ "DEFAULT_PERIOD",
+ "ISO8601_BASIC",
+ "ISO8601_BASIC_PERIOD",
+ "ISO8601",
+ "ISO8601_OFFSET_DATE_TIME_HH",
+ "ISO8601_OFFSET_DATE_TIME_HHMM",
+ "ISO8601_OFFSET_DATE_TIME_HHCMM",
+ "ISO8601_PERIOD",
+ "ISO8601_PERIOD_MICROS",
+ "US_MONTH_DAY_YEAR2_TIME",
+ "US_MONTH_DAY_YEAR4_TIME"
+ };
@Test
public void testPredefinedFormatWithoutTimezone() {
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- final String[] options = {format.name()};
+ for (final String patternName : PATTERN_NAMES) {
+ final String[] options = {patternName};
final DatePatternConverter converter = DatePatternConverter.newInstance(options);
- assertEquals(format.getPattern(), converter.getPattern());
+ final String expectedPattern = DatePatternConverter.decodeNamedPattern(patternName);
+ assertEquals(expectedPattern, converter.getPattern());
}
}
@Test
public void testPredefinedFormatWithTimezone() {
- for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
- final String[] options = {format.name(), "PST"}; // Pacific Standard Time=UTC-8:00
+ for (final String patternName : PATTERN_NAMES) {
+ final String[] options = {patternName, "PST"}; // Pacific Standard Time=UTC-8:00
final DatePatternConverter converter = DatePatternConverter.newInstance(options);
- assertEquals(format.getPattern(), converter.getPattern());
+ final String expectedPattern = DatePatternConverter.decodeNamedPattern(patternName);
+ assertEquals(expectedPattern, converter.getPattern());
}
}
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatterTest.java
deleted file mode 100644
index 91ab466e667..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatterTest.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * 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.core.util.internal;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import java.time.temporal.ChronoUnit;
-import java.util.Locale;
-import java.util.TimeZone;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.datetime.FastDateFormat;
-import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
-import org.apache.logging.log4j.test.ListStatusListener;
-import org.apache.logging.log4j.test.junit.UsingStatusListener;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
-import org.junit.jupiter.params.provider.ValueSource;
-
-class InstantPatternDynamicFormatterTest {
-
- @ParameterizedTest
- @CsvSource({
- "yyyy-MM-dd'T'HH:mm:ss.SSS" + ",FixedDateFormat",
- "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + ",FastDateFormat",
- "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'" + ",DateTimeFormatter"
- })
- void all_internal_implementations_should_be_used(final String pattern, final String className) {
- final InstantPatternDynamicFormatter formatter =
- new InstantPatternDynamicFormatter(pattern, Locale.getDefault(), TimeZone.getDefault());
- assertThat(formatter.getInternalImplementationClass())
- .asString()
- .describedAs("pattern=`%s`", pattern)
- .endsWith("." + className);
- }
-
- /**
- * Reproduces LOG4J2-3075.
- */
- @Test
- void nanoseconds_should_be_formatted() {
- final InstantFormatter formatter = new InstantPatternDynamicFormatter(
- "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", Locale.getDefault(), TimeZone.getTimeZone("UTC"));
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(0, 123_456_789);
- assertThat(formatter.format(instant)).isEqualTo("1970-01-01T00:00:00.123456789Z");
- }
-
- /**
- * Reproduces LOG4J2-3614.
- */
- @Test
- void FastDateFormat_failures_should_be_handled() {
-
- // Define a pattern causing `FastDateFormat` to fail.
- final String pattern = "ss.nnnnnnnnn";
- final TimeZone timeZone = TimeZone.getTimeZone("UTC");
- final Locale locale = Locale.US;
-
- // Assert that the pattern is not supported by `FixedDateFormat`.
- final FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(pattern, timeZone.getID());
- assertThat(fixedDateFormat).isNull();
-
- // Assert that the pattern indeed causes a `FastDateFormat` failure.
- assertThatThrownBy(() -> FastDateFormat.getInstance(pattern, timeZone, locale))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Illegal pattern component: nnnnnnnnn");
-
- // Assert that `InstantFormatter` falls back to `DateTimeFormatter`.
- final InstantPatternDynamicFormatter formatter =
- new InstantPatternDynamicFormatter(pattern, Locale.getDefault(), timeZone);
- assertThat(formatter.getInternalImplementationClass()).asString().endsWith(".DateTimeFormatter");
-
- // Assert that formatting works.
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(0, 123_456_789);
- assertThat(formatter.format(instant)).isEqualTo("00.123456789");
- }
-
- /**
- * Reproduces #1418.
- */
- @Test
- @UsingStatusListener
- void FixedFormatter_should_allocate_large_enough_buffer(final ListStatusListener listener) {
- final String pattern = "yyyy-MM-dd'T'HH:mm:ss,SSSXXX";
- final TimeZone timeZone = TimeZone.getTimeZone("America/Chicago");
- final Locale locale = Locale.ENGLISH;
- final InstantPatternDynamicFormatter formatter = new InstantPatternDynamicFormatter(pattern, locale, timeZone);
-
- // On this pattern the `FixedFormatter` used a buffer shorter than necessary,
- // which caused exceptions and warnings.
- assertThat(listener.findStatusData(Level.WARN)).hasSize(0);
- assertThat(formatter.getInternalImplementationClass()).asString().endsWith(".FixedDateFormat");
- }
-
- @ParameterizedTest
- @ValueSource(
- strings = {
- // Basics
- "S",
- "SSSS",
- "SSSSS",
- "SSSSSS",
- "SSSSSSS",
- "SSSSSSSSS",
- "n",
- "nn",
- "N",
- "NN",
- // Mixed with other stuff
- "yyyy-MM-dd HH:mm:ss,SSSSSS",
- "yyyy-MM-dd HH:mm:ss,SSSSSS",
- "yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
- "yyyy-MM-dd'T'HH:mm:ss.SXXX"
- })
- void should_recognize_patterns_of_nano_precision(final String pattern) {
- assertPatternPrecision(pattern, ChronoUnit.NANOS);
- }
-
- @ParameterizedTest
- @ValueSource(
- strings = {
- // Basics
- "SS",
- "SSS",
- "A",
- "AA",
- // Mixed with other stuff
- "yyyy-MM-dd HH:mm:ss,SS",
- "yyyy-MM-dd HH:mm:ss,SSS",
- "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
- // Single-quoted text containing nanosecond directives
- "yyyy-MM-dd'S'HH:mm:ss.SSSXXX",
- "yyyy-MM-dd'n'HH:mm:ss.SSSXXX",
- "yyyy-MM-dd'N'HH:mm:ss.SSSXXX",
- })
- void should_recognize_patterns_of_milli_precision(final String pattern) {
- assertPatternPrecision(pattern, ChronoUnit.MILLIS);
- }
-
- @ParameterizedTest
- @ValueSource(
- strings = {
- // Basics
- "s",
- "ss",
- // Mixed with other stuff
- "yyyy-MM-dd HH:mm:ss",
- "yyyy-MM-dd HH:mm:ss",
- "yyyy-MM-dd'T'HH:mm:ss",
- "HH:mm",
- "yyyy-MM-dd'T'",
- // Single-quoted text containing nanosecond and millisecond directives
- "yyyy-MM-dd'S'HH:mm:ss",
- "yyyy-MM-dd'n'HH:mm:ss",
- "yyyy-MM-dd'N'HH:mm:ss",
- "yyyy-MM-dd'A'HH:mm:ss"
- })
- void should_recognize_patterns_of_second_precision(final String pattern) {
- assertPatternPrecision(pattern, ChronoUnit.SECONDS);
- }
-
- private static void assertPatternPrecision(final String pattern, final ChronoUnit expectedPrecision) {
- final ChronoUnit actualPrecision = InstantPatternDynamicFormatter.patternPrecision(pattern);
- assertThat(actualPrecision).as("pattern=`%s`", pattern).isEqualTo(expectedPrecision);
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatterTest.java
similarity index 98%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatterTest.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatterTest.java
index f3c11e3e1aa..3521d4d4105 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantNumberFormatterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatterTest.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.core.util.internal;
+package org.apache.logging.log4j.core.util.internal.instant;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
new file mode 100644
index 00000000000..cb853f01ce6
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
@@ -0,0 +1,399 @@
+/*
+ * 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.core.util.internal.instant;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.sequencePattern;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+import java.util.TimeZone;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.CompositePatternSequence;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.DynamicPatternSequence;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.PatternSequence;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.StaticPatternSequence;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class InstantPatternDynamicFormatterTest {
+
+ @ParameterizedTest
+ @MethodSource("sequencingTestCases")
+ void sequencing_should_work(
+ final String pattern, final ChronoUnit thresholdPrecision, final List expectedSequences) {
+ final List actualSequences = sequencePattern(pattern, thresholdPrecision);
+ assertThat(actualSequences).isEqualTo(expectedSequences);
+ }
+
+ static List sequencingTestCases() {
+ final List testCases = new ArrayList<>();
+
+ // `SSSX` should be treated constant for daily updates
+ testCases.add(Arguments.of("SSSX", ChronoUnit.DAYS, singletonList(pCom(pDyn("SSS"), pDyn("X")))));
+
+ // `yyyyMMddHHmmssSSSX` instant cache updated hourly
+ testCases.add(Arguments.of(
+ "yyyyMMddHHmmssSSSX",
+ ChronoUnit.HOURS,
+ asList(
+ pCom(pDyn("yyyy"), pDyn("MM"), pDyn("dd"), pDyn("HH")),
+ pCom(pDyn("mm"), pDyn("ss"), pDyn("SSS")),
+ pDyn("X"))));
+
+ // `yyyyMMddHHmmssSSSX` instant cache updated per minute
+ testCases.add(Arguments.of(
+ "yyyyMMddHHmmssSSSX",
+ ChronoUnit.MINUTES,
+ asList(
+ pCom(pDyn("yyyy"), pDyn("MM"), pDyn("dd"), pDyn("HH"), pDyn("mm")),
+ pCom(pDyn("ss"), pDyn("SSS")),
+ pDyn("X"))));
+
+ // ISO9601 instant cache updated daily
+ final String iso8601InstantPattern = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
+ testCases.add(Arguments.of(
+ iso8601InstantPattern,
+ ChronoUnit.DAYS,
+ asList(
+ pCom(pDyn("yyyy"), pSta("-"), pDyn("MM"), pSta("-"), pDyn("dd"), pSta("T")),
+ pCom(
+ pDyn("HH"),
+ pSta(":"),
+ pDyn("mm"),
+ pSta(":"),
+ pDyn("ss"),
+ pSta("."),
+ pDyn("SSS"),
+ pDyn("X")))));
+
+ // ISO9601 instant cache updated per minute
+ testCases.add(Arguments.of(
+ iso8601InstantPattern,
+ ChronoUnit.MINUTES,
+ asList(
+ pCom(
+ pDyn("yyyy"),
+ pSta("-"),
+ pDyn("MM"),
+ pSta("-"),
+ pDyn("dd"),
+ pSta("T"),
+ pDyn("HH"),
+ pSta(":"),
+ pDyn("mm"),
+ pSta(":")),
+ pCom(pDyn("ss"), pSta("."), pDyn("SSS")),
+ pDyn("X"))));
+
+ // ISO9601 instant cache updated per second
+ testCases.add(Arguments.of(
+ iso8601InstantPattern,
+ ChronoUnit.SECONDS,
+ asList(
+ pCom(
+ pDyn("yyyy"),
+ pSta("-"),
+ pDyn("MM"),
+ pSta("-"),
+ pDyn("dd"),
+ pSta("T"),
+ pDyn("HH"),
+ pSta(":"),
+ pDyn("mm"),
+ pSta(":"),
+ pDyn("ss"),
+ pSta(".")),
+ pDyn("SSS"),
+ pDyn("X"))));
+
+ return testCases;
+ }
+
+ private static CompositePatternSequence pCom(final PatternSequence... sequences) {
+ return new CompositePatternSequence(asList(sequences));
+ }
+
+ private static DynamicPatternSequence pDyn(final String pattern) {
+ return new DynamicPatternSequence(pattern);
+ }
+
+ private static StaticPatternSequence pSta(final String literal) {
+ return new StaticPatternSequence(literal);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "S",
+ "SSSSSSS",
+ "SSSSSSSSS",
+ "n",
+ "nn",
+ "N",
+ "NN",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm:ss,SSSSSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSSSSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SXXX"
+ })
+ void should_recognize_patterns_of_nano_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.NANOS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "SSSS",
+ "SSSSS",
+ "SSSSSS",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm:ss,SSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
+ // Single-quoted text containing nanosecond directives
+ "yyyy-MM-dd'S'HH:mm:ss.SSSSSSXXX",
+ "yyyy-MM-dd'n'HH:mm:ss.SSSSSSXXX",
+ "yyyy-MM-dd'N'HH:mm:ss.SSSSSSXXX",
+ })
+ void should_recognize_patterns_of_micro_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.MICROS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "SS",
+ "SSS",
+ "A",
+ "AA",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm:ss,SS",
+ "yyyy-MM-dd HH:mm:ss,SSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
+ // Single-quoted text containing nanosecond directives
+ "yyyy-MM-dd'S'HH:mm:ss.SSSXXX",
+ "yyyy-MM-dd'n'HH:mm:ss.SSSXXX",
+ "yyyy-MM-dd'N'HH:mm:ss.SSSXXX",
+ })
+ void should_recognize_patterns_of_milli_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.MILLIS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "s",
+ "ss",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm:s",
+ "yyyy-MM-dd HH:mm:ss",
+ "yyyy-MM-dd'T'HH:mm:ss",
+ "HH:mm:s",
+ // Single-quoted text containing nanosecond and millisecond directives
+ "yyyy-MM-dd'S'HH:mm:ss",
+ "yyyy-MM-dd'n'HH:mm:ss",
+ "yyyy-MM-dd'N'HH:mm:ss",
+ "yyyy-MM-dd'A'HH:mm:ss"
+ })
+ void should_recognize_patterns_of_second_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.SECONDS);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "m",
+ "mm",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH:mm",
+ "yyyy-MM-dd'T'HH:mm",
+ "HH:mm",
+ // Single-quoted text containing nanosecond and millisecond directives
+ "yyyy-MM-dd'S'HH:mm",
+ "yyyy-MM-dd'n'HH:mm"
+ })
+ void should_recognize_patterns_of_minute_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.MINUTES);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ // Basics
+ "H",
+ "HH",
+ "a",
+ "B",
+ "h",
+ "K",
+ "k",
+ "H",
+ "Z",
+ "x",
+ "X",
+ "O",
+ "z",
+ "v",
+ "VV",
+ // Mixed with other stuff
+ "yyyy-MM-dd HH",
+ "yyyy-MM-dd'T'HH",
+ "yyyy-MM-dd HH x",
+ "yyyy-MM-dd'T'HH XX",
+ "ddHH",
+ // Single-quoted text containing nanosecond and millisecond directives
+ "yyyy-MM-dd'S'HH",
+ "yyyy-MM-dd'n'HH"
+ })
+ void should_recognize_patterns_of_hour_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.HOURS);
+ }
+
+ private static void assertPatternPrecision(final String pattern, final ChronoUnit expectedPrecision) {
+ final InstantPatternFormatter formatter =
+ new InstantPatternDynamicFormatter(pattern, Locale.getDefault(), TimeZone.getDefault());
+ assertThat(formatter.getPrecision()).as("pattern=`%s`", pattern).isEqualTo(expectedPrecision);
+ }
+
+ @ParameterizedTest
+ @MethodSource("formatterInputs")
+ void output_should_match_DateTimeFormatter(
+ final String pattern, final Locale locale, final TimeZone timeZone, final MutableInstant instant) {
+ final String log4jOutput = formatInstant(pattern, locale, timeZone, instant);
+ final String javaOutput = DateTimeFormatter.ofPattern(pattern, locale)
+ .withZone(timeZone.toZoneId())
+ .format(instant);
+ assertThat(log4jOutput).isEqualTo(javaOutput);
+ }
+
+ static Stream formatterInputs() {
+ return Stream.of(
+ // Complete list of `FixedDateFormat`-supported patterns in version `2.24.1`
+ "HH:mm:ss,SSS",
+ "HH:mm:ss,SSSSSS",
+ "HH:mm:ss,SSSSSSSSS",
+ "HH:mm:ss.SSS",
+ "yyyyMMddHHmmssSSS",
+ "dd MMM yyyy HH:mm:ss,SSS",
+ "dd MMM yyyy HH:mm:ss.SSS",
+ "yyyy-MM-dd HH:mm:ss,SSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSS",
+ "yyyy-MM-dd HH:mm:ss,SSSSSSSSS",
+ "yyyy-MM-dd HH:mm:ss.SSS",
+ "yyyyMMdd'T'HHmmss,SSS",
+ "yyyyMMdd'T'HHmmss.SSS",
+ "yyyy-MM-dd'T'HH:mm:ss,SSS",
+ "yyyy-MM-dd'T'HH:mm:ss,SSSx",
+ "yyyy-MM-dd'T'HH:mm:ss,SSSxx",
+ "yyyy-MM-dd'T'HH:mm:ss,SSSxxx",
+ "yyyy-MM-dd'T'HH:mm:ss.SSS",
+ "yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
+ "dd/MM/yy HH:mm:ss.SSS",
+ "dd/MM/yyyy HH:mm:ss.SSS")
+ .flatMap(InstantPatternDynamicFormatterTest::formatterInputs);
+ }
+
+ private static final Random RANDOM = new Random(0);
+
+ private static final Locale[] LOCALES = Locale.getAvailableLocales();
+
+ private static final TimeZone[] TIME_ZONES =
+ Arrays.stream(TimeZone.getAvailableIDs()).map(TimeZone::getTimeZone).toArray(TimeZone[]::new);
+
+ static Stream formatterInputs(final String pattern) {
+ return IntStream.range(0, 500).mapToObj(ignoredIndex -> {
+ final Locale locale = LOCALES[RANDOM.nextInt(LOCALES.length)];
+ final TimeZone timeZone = TIME_ZONES[RANDOM.nextInt(TIME_ZONES.length)];
+ final MutableInstant instant = randomInstant();
+ return Arguments.of(pattern, locale, timeZone, instant);
+ });
+ }
+
+ private static MutableInstant randomInstant() {
+ final MutableInstant instant = new MutableInstant();
+ final long epochSecond = RANDOM.nextInt(1_621_280_470); // 2021-05-17 21:41:10
+ final int epochSecondNano = randomNanos();
+ instant.initFromEpochSecond(epochSecond, epochSecondNano);
+ return instant;
+ }
+
+ private static int randomNanos() {
+ int total = 0;
+ for (int digitIndex = 0; digitIndex < 9; digitIndex++) {
+ int number;
+ do {
+ number = RANDOM.nextInt(10);
+ } while (digitIndex == 0 && number == 0);
+ total = total * 10 + number;
+ }
+ return total;
+ }
+
+ private static String formatInstant(
+ final String pattern, final Locale locale, final TimeZone timeZone, final MutableInstant instant) {
+ final InstantPatternFormatter formatter = new InstantPatternDynamicFormatter(pattern, locale, timeZone);
+ final StringBuilder buffer = new StringBuilder();
+ formatter.formatTo(buffer, instant);
+ return buffer.toString();
+ }
+
+ @Test
+ void f() {
+ final String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS";
+ final Locale LOCALE = Locale.US;
+ final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
+ final InstantPatternFormatter formatter = InstantPatternFormatter.newBuilder()
+ .setPattern(pattern)
+ .setLocale(LOCALE)
+ .setTimeZone(TIME_ZONE)
+ .setCachingEnabled(false)
+ .build();
+ final StringBuilder buffer = new StringBuilder();
+ final MutableInstant mutableInstant = new MutableInstant();
+
+ final Instant instant1 = Instant.now();
+ mutableInstant.initFromEpochSecond(instant1.getEpochSecond(), instant1.getNano());
+ formatter.formatTo(buffer, mutableInstant);
+
+ buffer.setLength(0);
+ final Instant instant2 = instant1.plusMillis(1);
+ mutableInstant.initFromEpochSecond(instant2.getEpochSecond(), instant2.getNano());
+ formatter.formatTo(buffer, mutableInstant);
+ }
+}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
similarity index 99%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatterTest.java
rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
index 2d76f508123..b25fb85d741 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.core.util.internal;
+package org.apache.logging.log4j.core.util.internal.instant;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
index b85dc5dc799..bc334f84d24 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
@@ -18,18 +18,21 @@
import static java.util.Objects.requireNonNull;
+import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.stream.Collectors;
+import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.internal.InstantFormatter;
-import org.apache.logging.log4j.core.util.internal.InstantNumberFormatter;
-import org.apache.logging.log4j.core.util.internal.InstantPatternFormatter;
+import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
+import org.apache.logging.log4j.core.util.internal.instant.InstantFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantNumberFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
import org.apache.logging.log4j.util.PerformanceSensitive;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@@ -37,6 +40,7 @@
/**
* Converts and formats the event's date in a StringBuilder.
*/
+@SuppressWarnings("deprecation")
@Plugin(name = "DatePatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"d", "date"})
@PerformanceSensitive("allocation")
@@ -100,15 +104,7 @@ private static Formatter> createFormatter(@Nullable final String[] options) {
try {
return createFormatterUnsafely(options);
} catch (final Exception error) {
- if (LOGGER.isWarnEnabled()) {
- final String quotedOptions =
- Arrays.stream(options).map(option -> '`' + option + '`').collect(Collectors.joining(", "));
- LOGGER.warn(
- "[{}] failed for options: {}, falling back to the default instance",
- CLASS_NAME,
- quotedOptions,
- error);
- }
+ logOptionReadFailure(options, error, "failed for options: {}, falling back to the default instance");
}
final InstantPatternFormatter delegateFormatter =
InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build();
@@ -141,15 +137,137 @@ private static Formatter> createFormatterUnsafely(@Nullable final String[] opt
}
private static String readPattern(@Nullable final String[] options) {
- return options != null && options.length > 0 ? options[0] : DEFAULT_PATTERN;
+ return options != null && options.length > 0 && options[0] != null
+ ? decodeNamedPattern(options[0])
+ : DEFAULT_PATTERN;
+ }
+
+ /**
+ * Decodes {@link FixedDateFormat} named patterns into their corresponding {@link DateTimeFormatter} representations.
+ *
+ * In version {@code 2.25.0}, {@link FixedDateFormat} and {@link FastDateFormat} are deprecated in favor of {@link InstantPatternFormatter}.
+ * We introduced this method to keep backward compatibility with the named patterns provided by {@link FixedDateFormat}.
+ *
+ *
+ * @param pattern a user provided date & time formatting pattern
+ * @return the transformed formatting pattern where {@link FixedDateFormat} named patterns are replaced with their corresponding {@link DateTimeFormatter} representations
+ * @since 2.25.0
+ */
+ static String decodeNamedPattern(final String pattern) {
+
+ // If legacy formatters are enabled, we need to produce output aimed for `FixedDateFormat` and `FastDateFormat`.
+ // Otherwise, we need to produce output aimed for `DateTimeFormatter`.
+ // In conclusion, we need to check if legacy formatters enabled and apply following transformations.
+ //
+ // | Microseconds | Nanoseconds | Time-zone
+ // ------------------------------+--------------+-------------+-----------
+ // Legacy formatter directive | nnnnnn | nnnnnnnnn | X, XX, XXX
+ // `DateTimeFormatter` directive | SSSSSS | SSSSSSSSS | x, xx, xxx
+ //
+ // Enabling legacy formatters mean that user requests the pattern to be formatted using deprecated
+ // `FixedDateFormat` and `FastDateFormat`.
+ // These two have, let's not say _bogus_, but an _interesting_ way of handling certain pattern directives:
+ //
+ // - They say they adhere to `SimpleDateFormat` specification, but use `n` directive.
+ // `n` is neither defined by `SimpleDateFormat`, nor `SimpleDateFormat` supports sub-millisecond precisions.
+ // `n` is probably manually introduced by Log4j to support sub-millisecond precisions.
+ //
+ // - `n` denotes nano-of-second for `DateTimeFormatter`.
+ // In Java 17, `n` and `N` (nano-of-day) always output nanosecond precision.
+ // This is independent of how many times they occur consequently.
+ // Yet legacy formatters use repeated `n` to denote sub-milliseconds precision of certain length.
+ // This doesn't work for `DateTimeFormatter`, which needs
+ //
+ // - `SSSSSS` for 6-digit microsecond precision
+ // - `SSSSSSSSS` for 9-digit nanosecond precision
+ //
+ // - Legacy formatters use `X`, `XX,` and `XXX` to choose between `+00`, `+0000`, or `+00:00`.
+ // This is the correct behaviour for `SimpleDateFormat`.
+ // Though `X` in `DateTimeFormatter` produces `Z` for zero-offset.
+ // To avoid the `Z` output, one needs to use `x` with `DateTimeFormatter`.
+ final boolean compat = InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED;
+
+ switch (pattern) {
+ case "ABSOLUTE":
+ return "HH:mm:ss,SSS";
+ case "ABSOLUTE_MICROS":
+ return "HH:mm:ss," + (compat ? "nnnnnn" : "SSSSSS");
+ case "ABSOLUTE_NANOS":
+ return "HH:mm:ss," + (compat ? "nnnnnnnnn" : "SSSSSSSSS");
+ case "ABSOLUTE_PERIOD":
+ return "HH:mm:ss.SSS";
+ case "COMPACT":
+ return "yyyyMMddHHmmssSSS";
+ case "DATE":
+ return "dd MMM yyyy HH:mm:ss,SSS";
+ case "DATE_PERIOD":
+ return "dd MMM yyyy HH:mm:ss.SSS";
+ case "DEFAULT":
+ return "yyyy-MM-dd HH:mm:ss,SSS";
+ case "DEFAULT_MICROS":
+ return "yyyy-MM-dd HH:mm:ss," + (compat ? "nnnnnn" : "SSSSSS");
+ case "DEFAULT_NANOS":
+ return "yyyy-MM-dd HH:mm:ss," + (compat ? "nnnnnnnnn" : "SSSSSSSSS");
+ case "DEFAULT_PERIOD":
+ return "yyyy-MM-dd HH:mm:ss.SSS";
+ case "ISO8601_BASIC":
+ return "yyyyMMdd'T'HHmmss,SSS";
+ case "ISO8601_BASIC_PERIOD":
+ return "yyyyMMdd'T'HHmmss.SSS";
+ case "ISO8601":
+ return "yyyy-MM-dd'T'HH:mm:ss,SSS";
+ case "ISO8601_OFFSET_DATE_TIME_HH":
+ return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "X" : "x");
+ case "ISO8601_OFFSET_DATE_TIME_HHMM":
+ return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "XX" : "xx");
+ case "ISO8601_OFFSET_DATE_TIME_HHCMM":
+ return "yyyy-MM-dd'T'HH:mm:ss,SSS" + (compat ? "XXX" : "xxx");
+ case "ISO8601_PERIOD":
+ return "yyyy-MM-dd'T'HH:mm:ss.SSS";
+ case "ISO8601_PERIOD_MICROS":
+ return "yyyy-MM-dd'T'HH:mm:ss." + (compat ? "nnnnnn" : "SSSSSS");
+ case "US_MONTH_DAY_YEAR2_TIME":
+ return "dd/MM/yy HH:mm:ss.SSS";
+ case "US_MONTH_DAY_YEAR4_TIME":
+ return "dd/MM/yyyy HH:mm:ss.SSS";
+ }
+ return pattern;
}
private static TimeZone readTimeZone(@Nullable final String[] options) {
- return options != null && options.length > 1 ? TimeZone.getTimeZone(options[1]) : TimeZone.getDefault();
+ try {
+ if (options != null && options.length > 1 && options[1] != null) {
+ return TimeZone.getTimeZone(options[1]);
+ }
+ } catch (final Exception error) {
+ logOptionReadFailure(
+ options,
+ error,
+ "failed to read the time zone at index 1 of options: {}, falling back to the default time zone");
+ }
+ return TimeZone.getDefault();
}
private static Locale readLocale(@Nullable final String[] options) {
- return options != null && options.length > 2 ? Locale.forLanguageTag(options[2]) : Locale.getDefault();
+ try {
+ if (options != null && options.length > 2 && options[2] != null) {
+ return Locale.forLanguageTag(options[2]);
+ }
+ } catch (final Exception error) {
+ logOptionReadFailure(
+ options,
+ error,
+ "failed to read the locale at index 2 of options: {}, falling back to the default locale");
+ }
+ return Locale.getDefault();
+ }
+
+ private static void logOptionReadFailure(final String[] options, final Exception error, final String message) {
+ if (LOGGER.isWarnEnabled()) {
+ final String quotedOptions =
+ Arrays.stream(options).map(option -> '`' + option + '`').collect(Collectors.joining(", "));
+ LOGGER.warn("[{}] " + message, CLASS_NAME, quotedOptions, error);
+ }
}
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DatePrinter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DatePrinter.java
index 4016d4f2b22..ed62303492e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DatePrinter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DatePrinter.java
@@ -36,7 +36,7 @@
*
*
* @since Apache Commons Lang 3.2
- * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be removed in the next major release.
*/
@Deprecated
public interface DatePrinter {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
index c14a296da0f..6dc56242abf 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
@@ -69,7 +69,7 @@
*
*
* @since Apache Commons Lang 2.0
- * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be removed in the next major release.
*/
@Deprecated
public class FastDateFormat extends Format implements DatePrinter {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDatePrinter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDatePrinter.java
index 6345f485499..f4df3ebfa03 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDatePrinter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDatePrinter.java
@@ -78,7 +78,7 @@
*
*
* @since Apache Commons Lang 3.2
- * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be removed in the next major release.
*/
@Deprecated
public class FastDatePrinter implements DatePrinter, Serializable {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
index aba0baaf2ee..89c0c0c6974 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
@@ -16,10 +16,8 @@
*/
package org.apache.logging.log4j.core.util.datetime;
-import java.time.LocalDate;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
@@ -30,7 +28,7 @@
* Custom time formatter that trades flexibility for performance. This formatter only supports the date patterns defined
* in {@link FixedFormat}. For any other date patterns use {@link FastDateFormat}.
*
- * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be removed in the next major release.
*/
@Deprecated
@ProviderType
@@ -49,13 +47,13 @@ public enum FixedFormat {
*/
ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1, 3, null),
/**
- * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,SSSSSS"}.
+ * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,nnnnnn"}.
*/
- ABSOLUTE_MICROS("HH:mm:ss,SSSSSS", null, 0, ':', 1, ',', 1, 6, null),
+ ABSOLUTE_MICROS("HH:mm:ss,nnnnnn", null, 0, ':', 1, ',', 1, 6, null),
/**
- * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,SSSSSSSSS"}.
+ * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,nnnnnnnnn"}.
*/
- ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS", null, 0, ':', 1, ',', 1, 9, null),
+ ABSOLUTE_NANOS("HH:mm:ss,nnnnnnnnn", null, 0, ':', 1, ',', 1, 9, null),
/**
* ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}.
@@ -82,13 +80,13 @@ public enum FixedFormat {
*/
DEFAULT("yyyy-MM-dd HH:mm:ss,SSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 3, null),
/**
- * DEFAULT time format with microsecond precision: {@code "yyyy-MM-dd HH:mm:ss,SSSSSS"}.
+ * DEFAULT time format with microsecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnn"}.
*/
- DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 6, null),
+ DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,nnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 6, null),
/**
- * DEFAULT time format with nanosecond precision: {@code "yyyy-MM-dd HH:mm:ss,SSSSSSSSS"}.
+ * DEFAULT time format with nanosecond precision: {@code "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"}.
*/
- DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 9, null),
+ DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,nnnnnnnnn", "yyyy-MM-dd ", 0, ':', 1, ',', 1, 9, null),
/**
* DEFAULT time format variation with period separator: {@code "yyyy-MM-dd HH:mm:ss.SSS"}.
@@ -143,9 +141,9 @@ public enum FixedFormat {
ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 3, null),
/**
- * ISO8601 time format with support for microsecond precision: {@code "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"}.
+ * ISO8601 time format with support for microsecond precision: {@code "yyyy-MM-dd'T'HH:mm:ss.nnnnnn"}.
*/
- ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 6, null),
+ ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.nnnnnn", "yyyy-MM-dd'T'", 2, ':', 1, '.', 1, 6, null),
/**
* American date/time format with 2-digit year: {@code "dd/MM/yy HH:mm:ss.SSS"}.
@@ -159,7 +157,7 @@ public enum FixedFormat {
private static final String DEFAULT_SECOND_FRACTION_PATTERN = "SSS";
private static final int MILLI_FRACTION_DIGITS = DEFAULT_SECOND_FRACTION_PATTERN.length();
- private static final char SECOND_FRACTION_PATTERN = 'S';
+ private static final char SECOND_FRACTION_PATTERN = 'n';
private final String pattern;
private final String datePattern;
@@ -573,22 +571,22 @@ private void updateMidnightMillis(final long now) {
}
private long calcMidnightMillis(final long time, final int addDays) {
- final ZoneId zoneId = timeZone.toZoneId();
- final ZonedDateTime zonedInstant = java.time.Instant.ofEpochMilli(time).atZone(zoneId);
- return LocalDate.from(zonedInstant)
- .atStartOfDay()
- .plusDays(addDays)
- .atZone(zoneId)
- .toInstant()
- .toEpochMilli();
+ final Calendar cal = Calendar.getInstance(timeZone);
+ cal.setTimeInMillis(time);
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.add(Calendar.DATE, addDays);
+ return cal.getTimeInMillis();
}
private void updateDaylightSavingTime() {
Arrays.fill(dstOffsets, 0);
- final long oneHourMillis = TimeUnit.HOURS.toMillis(1);
- if (timeZone.getOffset(midnightToday) != timeZone.getOffset(midnightToday + 23 * oneHourMillis)) {
+ final int ONE_HOUR = (int) TimeUnit.HOURS.toMillis(1);
+ if (timeZone.getOffset(midnightToday) != timeZone.getOffset(midnightToday + 23 * ONE_HOUR)) {
for (int i = 0; i < dstOffsets.length; i++) {
- final long time = midnightToday + i * oneHourMillis;
+ final long time = midnightToday + i * ONE_HOUR;
dstOffsets[i] = timeZone.getOffset(time) - timeZone.getRawOffset();
}
if (dstOffsets[0] > dstOffsets[23]) { // clock is moved backwards.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
index c51bc58a921..8ec012e1671 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
@@ -20,7 +20,8 @@
/**
* The basic methods for performing date formatting.
- * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
+ *
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be removed in the next major release.
*/
@Deprecated
public abstract class Format {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FormatCache.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FormatCache.java
index c179b605027..ed47bc1f83f 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FormatCache.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FormatCache.java
@@ -32,7 +32,7 @@
*
*
* @since Apache Commons Lang 3.0
- * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be moved to an internal package in the next major release.
+ * @deprecated Starting with version {@code 2.25.0}, this class is assumed to be internal and planned to be removed in the next major release.
*/
// TODO: Before making public move from getDateTimeInstance(Integer,...) to int; or some other approach.
@Deprecated
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
index cb21f6148dd..6b4ae367b69 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
@@ -16,7 +16,7 @@
*/
/**
- * Log4j date & time formatting classes.
+ * Log4j date and time formatting classes.
*
* @deprecated Starting with version {@code 2.25.0}, these classes are assumed to be internal and planned to be moved to an internal package in the next major release.
*/
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatter.java
deleted file mode 100644
index 79de1c9db63..00000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatter.java
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * 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.core.util.internal;
-
-import static java.util.Objects.requireNonNull;
-
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.TimeZone;
-import java.util.function.Supplier;
-import org.apache.logging.log4j.core.time.Instant;
-import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.Constants;
-import org.apache.logging.log4j.core.util.datetime.FastDateFormat;
-import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
-import org.apache.logging.log4j.status.StatusLogger;
-import org.jspecify.annotations.NullMarked;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Formats an {@link Instant} using the specified date & time formatting pattern.
- * This is a composite formatter trying to employ either {@link FixedDateFormat}, {@link FastDateFormat} or {@link DateTimeFormatter} in the given order due to performance reasons.
- *
- * If the given pattern happens to be supported by either {@link FixedDateFormat} or {@link FastDateFormat}, yet they produce a different result compared to {@link DateTimeFormatter}, {@link DateTimeFormatter} will be employed instead.
- * Note that {@link FastDateFormat} supports at most millisecond precision.
- *
- *
- * @since 2.25.0
- */
-@NullMarked
-final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
-
- private static final StatusLogger LOGGER = StatusLogger.getLogger();
-
- /**
- * The list of formatter factories in decreasing efficiency order.
- */
- private static final FormatterFactory[] FORMATTER_FACTORIES = {
- new Log4jFixedFormatterFactory(), new Log4jFastFormatterFactory(), new JavaDateTimeFormatterFactory()
- };
-
- private final String pattern;
-
- private final Locale locale;
-
- private final TimeZone timeZone;
-
- private final Formatter formatter;
-
- InstantPatternDynamicFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
- this.pattern = pattern;
- this.locale = locale;
- this.timeZone = timeZone;
- this.formatter = Arrays.stream(FORMATTER_FACTORIES)
- .map(formatterFactory -> {
- try {
- return formatterFactory.createIfSupported(pattern, locale, timeZone);
- } catch (final Exception error) {
- LOGGER.warn("skipping the failed formatter factory `{}`", formatterFactory, error);
- return null;
- }
- })
- .filter(Objects::nonNull)
- .findFirst()
- .orElseThrow(() -> new AssertionError("could not find a matching formatter"));
- }
-
- @Override
- public void formatTo(final StringBuilder buffer, final Instant instant) {
- requireNonNull(buffer, "buffer");
- requireNonNull(instant, "instant");
- formatter.formatTo(buffer, instant);
- }
-
- @Override
- public ChronoUnit getPrecision() {
- return formatter.precision;
- }
-
- Class> getInternalImplementationClass() {
- return formatter.getInternalImplementationClass();
- }
-
- @Override
- public String getPattern() {
- return pattern;
- }
-
- @Override
- public Locale getLocale() {
- return locale;
- }
-
- @Override
- public TimeZone getTimeZone() {
- return timeZone;
- }
-
- @Override
- public String toString() {
- final StringBuilder buffer = new StringBuilder();
- buffer.append(InstantPatternDynamicFormatter.class.getSimpleName()).append('{');
- buffer.append("pattern=`").append(pattern).append('`');
- if (!locale.equals(Locale.getDefault())) {
- buffer.append(",locale=`").append(locale).append('`');
- }
- if (!timeZone.equals(TimeZone.getDefault())) {
- buffer.append(",timeZone=`").append(timeZone.getID()).append('`');
- }
- buffer.append('}');
- return buffer.toString();
- }
-
- private interface FormatterFactory {
-
- @Nullable
- Formatter createIfSupported(String pattern, Locale locale, TimeZone timeZone);
- }
-
- private abstract static class Formatter {
-
- private final ChronoUnit precision;
-
- Formatter(final ChronoUnit precision) {
- this.precision = precision;
- }
-
- abstract Class> getInternalImplementationClass();
-
- abstract void formatTo(StringBuilder buffer, Instant instant);
- }
-
- private static final class JavaDateTimeFormatterFactory implements FormatterFactory {
-
- @Override
- public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
- return new JavaDateTimeFormatter(pattern, locale, timeZone);
- }
- }
-
- private static final class JavaDateTimeFormatter extends Formatter {
-
- private final DateTimeFormatter formatter;
-
- private final MutableInstant mutableInstant;
-
- private JavaDateTimeFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
- super(patternPrecision(pattern));
- this.formatter =
- DateTimeFormatter.ofPattern(pattern).withLocale(locale).withZone(timeZone.toZoneId());
- this.mutableInstant = new MutableInstant();
- }
-
- @Override
- public Class> getInternalImplementationClass() {
- return DateTimeFormatter.class;
- }
-
- @Override
- public void formatTo(final StringBuilder buffer, final Instant instant) {
- if (instant instanceof MutableInstant) {
- formatMutableInstant((MutableInstant) instant, buffer);
- } else {
- formatInstant(instant, buffer);
- }
- }
-
- private void formatMutableInstant(final MutableInstant instant, final StringBuilder buffer) {
- formatter.formatTo(instant, buffer);
- }
-
- private void formatInstant(final Instant instant, final StringBuilder buffer) {
- mutableInstant.initFrom(instant);
- formatMutableInstant(mutableInstant, buffer);
- }
- }
-
- private static final class Log4jFastFormatterFactory implements FormatterFactory {
-
- @Override
- public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
- final Log4jFastFormatter formatter = new Log4jFastFormatter(pattern, locale, timeZone);
- final boolean patternSupported = patternSupported(pattern, locale, timeZone, formatter);
- return patternSupported ? formatter : null;
- }
- }
-
- private static final class Log4jFastFormatter extends Formatter {
-
- private final FastDateFormat formatter;
-
- private final Supplier calendarSupplier;
-
- private Log4jFastFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
- super(effectivePatternPrecision(pattern));
- this.formatter = FastDateFormat.getInstance(pattern, timeZone, locale);
- this.calendarSupplier = memoryEfficientInstanceSupplier(() -> Calendar.getInstance(timeZone, locale));
- }
-
- @Override
- public Class> getInternalImplementationClass() {
- return FastDateFormat.class;
- }
-
- private static ChronoUnit effectivePatternPrecision(final String pattern) {
- final ChronoUnit patternPrecision = patternPrecision(pattern);
- // `FastDateFormat` doesn't support precision higher than millisecond
- return ChronoUnit.MILLIS.compareTo(patternPrecision) > 0 ? ChronoUnit.MILLIS : patternPrecision;
- }
-
- @Override
- public void formatTo(final StringBuilder stringBuilder, final Instant instant) {
- final Calendar calendar = calendarSupplier.get();
- calendar.setTimeInMillis(instant.getEpochMillisecond());
- formatter.format(calendar, stringBuilder);
- }
- }
-
- private static final class Log4jFixedFormatterFactory implements FormatterFactory {
-
- @Override
- @Nullable
- public Formatter createIfSupported(final String pattern, final Locale locale, final TimeZone timeZone) {
- final FixedDateFormat internalFormatter = FixedDateFormat.createIfSupported(pattern, timeZone.getID());
- if (internalFormatter == null) {
- return null;
- }
- final Log4jFixedFormatter formatter = new Log4jFixedFormatter(internalFormatter);
- final boolean patternSupported = patternSupported(pattern, locale, timeZone, formatter);
- return patternSupported ? formatter : null;
- }
- }
-
- private static final class Log4jFixedFormatter extends Formatter {
-
- private final FixedDateFormat formatter;
-
- private final Supplier bufferSupplier;
-
- private Log4jFixedFormatter(final FixedDateFormat formatter) {
- super(patternPrecision(formatter.getFormat()));
- this.formatter = formatter;
- this.bufferSupplier = memoryEfficientInstanceSupplier(() -> {
- // Double size for locales with lengthy `DateFormatSymbols`
- return new char[formatter.getLength() << 1];
- });
- }
-
- @Override
- public Class> getInternalImplementationClass() {
- return FixedDateFormat.class;
- }
-
- @Override
- public void formatTo(final StringBuilder buffer, final Instant instant) {
- final char[] charBuffer = bufferSupplier.get();
- final int length = formatter.formatInstant(instant, charBuffer, 0);
- buffer.append(charBuffer, 0, length);
- }
- }
-
- private static Supplier memoryEfficientInstanceSupplier(final Supplier supplier) {
- return Constants.ENABLE_THREADLOCALS ? ThreadLocal.withInitial(supplier)::get : supplier;
- }
-
- /**
- * Checks if the provided formatter output matches with the one generated by {@link DateTimeFormatter}.
- */
- private static boolean patternSupported(
- final String pattern, final Locale locale, final TimeZone timeZone, final Formatter formatter) {
- final DateTimeFormatter javaFormatter =
- DateTimeFormatter.ofPattern(pattern).withLocale(locale).withZone(timeZone.toZoneId());
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(
- // 2021-05-17 21:41:10
- 1_621_280_470,
- // Using the highest nanosecond precision possible to
- // differentiate formatters only supporting millisecond
- // precision.
- 123_456_789);
- final String expectedFormat = javaFormatter.format(instant);
- final StringBuilder buffer = new StringBuilder();
- formatter.formatTo(buffer, instant);
- final String actualFormat = buffer.toString();
- return expectedFormat.equals(actualFormat);
- }
-
- /**
- * @param pattern a date & time formatting pattern
- * @return the time precision of the output when formatted using the specified {@code pattern}
- */
- static ChronoUnit patternPrecision(final String pattern) {
- // Remove text blocks
- final String trimmedPattern = pattern.replaceAll("'[^']*'", "");
- // A single `S` (fraction-of-second) outputs nanosecond precision
- if (trimmedPattern.matches(".*(?not of {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} precision are precomputed, cached, and updated once every {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD}.
+ * The rest is computed dynamically.
+ *
+ * For instance, given the pattern {@code yyyy-MM-dd'T'HH:mm:ss.SSSX}, the generated formatter will
+ *
+ *
+ *
Sequence the pattern and assign a time precision to each part (e.g., {@code MM} is of month precision)
+ *
Precompute and cache the output for parts that are of precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} (i.e., {@code yyyy-MM-dd'T'HH:mm:}, {@code .}, and {@code X}) and cache it
+ *
Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss} and {@code SSS})
+ *
+ *
+ * @since 2.25.0
+ */
+public final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
+
+ static final ChronoUnit PRECISION_THRESHOLD = ChronoUnit.MINUTES;
+
+ private final AtomicReference timestampedFormatterRef;
+
+ InstantPatternDynamicFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
+ final TimestampedFormatter timestampedFormatter = createTimestampedFormatter(pattern, locale, timeZone, null);
+ this.timestampedFormatterRef = new AtomicReference<>(timestampedFormatter);
+ }
+
+ @Override
+ public String getPattern() {
+ return timestampedFormatterRef.get().formatter.getPattern();
+ }
+
+ @Override
+ public Locale getLocale() {
+ return timestampedFormatterRef.get().formatter.getLocale();
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return timestampedFormatterRef.get().formatter.getTimeZone();
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return timestampedFormatterRef.get().formatter.getPrecision();
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ requireNonNull(buffer, "buffer");
+ requireNonNull(instant, "instant");
+ getEffectiveFormatter(instant).formatTo(buffer, instant);
+ }
+
+ private InstantPatternFormatter getEffectiveFormatter(final Instant instant) {
+
+ // Reuse the instance formatter, if timestamps match
+ TimestampedFormatter oldTimestampedFormatter = timestampedFormatterRef.get();
+ final long instantEpochMinutes = toEpochMinutes(instant);
+ final InstantPatternFormatter oldFormatter = oldTimestampedFormatter.formatter;
+ if (oldTimestampedFormatter.instantEpochMinutes == instantEpochMinutes) {
+ return oldFormatter;
+ }
+
+ // Create a new formatter, [try to] update the instance formatter, and return that
+ final TimestampedFormatter newTimestampedFormatter = createTimestampedFormatter(
+ oldFormatter.getPattern(), oldFormatter.getLocale(), oldFormatter.getTimeZone(), instant);
+ timestampedFormatterRef.compareAndSet(oldTimestampedFormatter, newTimestampedFormatter);
+ return newTimestampedFormatter.formatter;
+ }
+
+ private static TimestampedFormatter createTimestampedFormatter(
+ final String pattern, final Locale locale, final TimeZone timeZone, @Nullable Instant creationInstant) {
+ if (creationInstant == null) {
+ creationInstant = new MutableInstant();
+ final java.time.Instant currentInstant = java.time.Instant.now();
+ ((MutableInstant) creationInstant)
+ .initFromEpochSecond(currentInstant.getEpochSecond(), creationInstant.getNanoOfSecond());
+ }
+ final InstantPatternFormatter formatter =
+ createFormatter(pattern, locale, timeZone, PRECISION_THRESHOLD, creationInstant);
+ final long creationInstantEpochMinutes = toEpochMinutes(creationInstant);
+ return new TimestampedFormatter(creationInstantEpochMinutes, formatter);
+ }
+
+ private static final class TimestampedFormatter {
+
+ private final long instantEpochMinutes;
+
+ private final InstantPatternFormatter formatter;
+
+ private TimestampedFormatter(final long instantEpochMinutes, final InstantPatternFormatter formatter) {
+ this.instantEpochMinutes = instantEpochMinutes;
+ this.formatter = formatter;
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static InstantPatternFormatter createFormatter(
+ final String pattern,
+ final Locale locale,
+ final TimeZone timeZone,
+ final ChronoUnit precisionThreshold,
+ final Instant creationInstant) {
+
+ // Sequence the pattern and create associated formatters
+ final List sequences = sequencePattern(pattern, precisionThreshold);
+ final List formatters = sequences.stream()
+ .map(sequence -> {
+ final InstantPatternFormatter formatter = sequence.createFormatter(locale, timeZone);
+ final boolean constant = sequence.isConstantForDurationOf(precisionThreshold);
+ if (!constant) {
+ return formatter;
+ }
+ final String formattedInstant;
+ {
+ final StringBuilder buffer = new StringBuilder();
+ formatter.formatTo(buffer, creationInstant);
+ formattedInstant = buffer.toString();
+ }
+ return new AbstractFormatter(formatter.getPattern(), locale, timeZone, formatter.getPrecision()) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ buffer.append(formattedInstant);
+ }
+ };
+ })
+ .collect(Collectors.toList());
+
+ switch (formatters.size()) {
+
+ // If found an empty pattern, return an empty formatter
+ case 0:
+ return new AbstractFormatter(pattern, locale, timeZone, ChronoUnit.FOREVER) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ // Do nothing
+ }
+ };
+
+ // If extracted a single formatter, return it as is
+ case 1:
+ return formatters.get(0);
+
+ // Combine all extracted formatters into one
+ default:
+ final ChronoUnit precision = new CompositePatternSequence(sequences).precision;
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int formatterIndex = 0; formatterIndex < formatters.size(); formatterIndex++) {
+ final InstantPatternFormatter formatter = formatters.get(formatterIndex);
+ formatter.formatTo(buffer, instant);
+ }
+ }
+ };
+ }
+ }
+
+ static List sequencePattern(final String pattern, final ChronoUnit precisionThreshold) {
+ List sequences = sequencePattern(pattern);
+ final List mergedSequences = mergeDynamicSequences(sequences, precisionThreshold);
+ return mergeConsequentEffectivelyConstantSequences(mergedSequences, precisionThreshold);
+ }
+
+ private static List sequencePattern(final String pattern) {
+ if (pattern.isEmpty()) {
+ return Collections.emptyList();
+ }
+ final List sequences = new ArrayList<>();
+ for (int startIndex = 0; startIndex < pattern.length(); ) {
+ final char c = pattern.charAt(startIndex);
+
+ // Handle dynamic pattern letters
+ final boolean dynamic = isDynamicPatternLetter(c);
+ if (dynamic) {
+ int endIndex = startIndex + 1;
+ while (endIndex < pattern.length() && pattern.charAt(endIndex) == c) {
+ endIndex++;
+ }
+ final String sequenceContent = pattern.substring(startIndex, endIndex);
+ final PatternSequence sequence = new DynamicPatternSequence(sequenceContent);
+ sequences.add(sequence);
+ startIndex = endIndex;
+ }
+
+ // Handle single-quotes
+ else if (c == '\'') {
+ final int endIndex = pattern.indexOf('\'', startIndex + 1);
+ if (endIndex < 0) {
+ final String message = String.format(
+ "pattern ends with an incomplete string literal that started at index %d: `%s`",
+ startIndex, pattern);
+ throw new IllegalArgumentException(message);
+ }
+ final String sequenceLiteral =
+ (startIndex + 1) == endIndex ? "'" : pattern.substring(startIndex + 1, endIndex);
+ final PatternSequence sequence = new StaticPatternSequence(sequenceLiteral);
+ sequences.add(sequence);
+ startIndex = endIndex + 1;
+ }
+
+ // Handle unknown literal
+ else {
+ final PatternSequence sequence = new StaticPatternSequence("" + c);
+ sequences.add(sequence);
+ startIndex++;
+ }
+ }
+ return mergeConsequentStaticPatternSequences(sequences);
+ }
+
+ private static boolean isDynamicPatternLetter(final char c) {
+ return "GuyDMLdgQqYwWEecFaBhKkHmsSAnNVvzOXxZ".indexOf(c) >= 0;
+ }
+
+ /**
+ * Merges consequent static sequences.
+ *
+ *
+ * For example, the sequencing of the {@code [MM-dd] HH:mm} pattern will create two static sequences for {@code ]} (right brace) and {@code } (whitespace) characters.
+ * This method will combine such consequent static sequences into one.
+ *
+ *
+ *
Example
+ *
+ *
+ * The {@code [MM-dd] HH:mm} pattern will result in following sequences:
+ *
+ * The above sequencing implies creation of 8 {@link AbstractFormatter}s.
+ *
+ *
+ * @param sequences sequences to be transformed
+ * @return transformed sequencing where consequent static sequences are merged
+ */
+ private static List mergeConsequentStaticPatternSequences(final List sequences) {
+
+ // Short-circuit if there is nothing to merge
+ if (sequences.size() < 2) {
+ return sequences;
+ }
+
+ final List mergedSequences = new ArrayList<>();
+ final List accumulatedSequences = new ArrayList<>();
+ for (final PatternSequence sequence : sequences) {
+
+ // Spotted a static sequence? Stage it for merging.
+ if (sequence instanceof StaticPatternSequence) {
+ accumulatedSequences.add((StaticPatternSequence) sequence);
+ }
+
+ // Spotted a dynamic sequence.
+ // Merge the accumulated static sequences, and then append the dynamic sequence.
+ else {
+ mergeConsequentStaticPatternSequences(mergedSequences, accumulatedSequences);
+ mergedSequences.add(sequence);
+ }
+ }
+
+ // Merge leftover static sequences
+ mergeConsequentStaticPatternSequences(mergedSequences, accumulatedSequences);
+ return mergedSequences;
+ }
+
+ private static void mergeConsequentStaticPatternSequences(
+ final List mergedSequences, final List accumulatedSequences) {
+ mergeAccumulatedSequences(mergedSequences, accumulatedSequences, () -> {
+ final String literal = accumulatedSequences.stream()
+ .map(sequence -> sequence.literal)
+ .collect(Collectors.joining());
+ return new StaticPatternSequence(literal);
+ });
+ }
+
+ /**
+ * Merges the sequences in between the first and the last found dynamic (i.e., non-constant) sequences.
+ *
+ *
+ * For example, given the {@code ss.SSS} pattern – where {@code ss} and {@code SSS} is effectively not constant, yet {@code .} is – this method will combine it into a single dynamic sequence.
+ * Because, as demonstrated in {@code DateTimeFormatterSequencingBenchmark}, formatting {@code ss.SSS} is approximately 20% faster than formatting first {@code ss}, then manually appending a {@code .}, and then formatting {@code SSS}.
+ *
+ *
+ *
Example
+ *
+ *
+ * Assume {@link #mergeConsequentStaticPatternSequences(List)} produced the following:
+ *
+ * For a threshold precision of {@link ChronoUnit#MINUTES}, this sequencing effectively translates to two {@link DateTimeFormatter#formatTo(TemporalAccessor, Appendable)} invocations for each {@link #formatTo(StringBuilder, Instant)} call: one for {@code ss}, and another one for {@code SSS}.
+ * This method transforms the above sequencing into the following:
+ *
+ * The resultant sequencing effectively translates to a single {@link DateTimeFormatter#formatTo(TemporalAccessor, Appendable)} invocation for each {@link #formatTo(StringBuilder, Instant)} call: only one fore {@code ss.SSS}.
+ *
+ *
+ * @param sequences sequences, preferable produced by {@link #mergeConsequentStaticPatternSequences(List)}, to be transformed
+ * @param precisionThreshold a precision threshold to determine dynamic (i.e., non-constant) sequences
+ * @return transformed sequencing where sequences in between the first and the last found dynamic (i.e., non-constant) sequences are merged
+ */
+ private static List mergeDynamicSequences(
+ final List sequences, final ChronoUnit precisionThreshold) {
+
+ // Locate the first and the last dynamic (i.e., non-constant) sequence indices
+ int firstDynamicSequenceIndex = -1;
+ int lastDynamicSequenceIndex = -1;
+ for (int sequenceIndex = 0; sequenceIndex < sequences.size(); sequenceIndex++) {
+ final PatternSequence sequence = sequences.get(sequenceIndex);
+ final boolean constant = sequence.isConstantForDurationOf(precisionThreshold);
+ if (!constant) {
+ if (firstDynamicSequenceIndex < 0) {
+ firstDynamicSequenceIndex = sequenceIndex;
+ }
+ lastDynamicSequenceIndex = sequenceIndex;
+ }
+ }
+
+ // Short-circuit if there are less than 2 dynamic sequences
+ if (firstDynamicSequenceIndex < 0 || firstDynamicSequenceIndex == lastDynamicSequenceIndex) {
+ return sequences;
+ }
+
+ // Merge dynamic sequences
+ final List mergedSequences = new ArrayList<>();
+ if (firstDynamicSequenceIndex > 0) {
+ mergedSequences.addAll(sequences.subList(0, firstDynamicSequenceIndex));
+ }
+ final PatternSequence mergedDynamicSequence = new CompositePatternSequence(
+ sequences.subList(firstDynamicSequenceIndex, lastDynamicSequenceIndex + 1));
+ mergedSequences.add(mergedDynamicSequence);
+ if ((lastDynamicSequenceIndex + 1) < sequences.size()) {
+ mergedSequences.addAll(sequences.subList(lastDynamicSequenceIndex + 1, sequences.size()));
+ }
+ return mergedSequences;
+ }
+
+ /**
+ * Merges sequences that are consequent and effectively constant for the provided precision threshold.
+ *
+ *
+ * For example, given the {@code yyyy-MM-dd'T'HH:mm:ss.SSS} pattern and a precision threshold of {@link ChronoUnit#MINUTES}, this method will combine sequences associated with {@code yyyy-MM-dd'T'HH:mm:} into a single sequence, since these are consequent and effectively constant sequences.
+ *
+ *
+ *
Example
+ *
+ *
+ * Assume {@link #mergeDynamicSequences(List, ChronoUnit)} produced the following:
+ *
+ * The resultant sequencing effectively translates to 3 {@link AbstractFormatter}s.
+ *
+ *
+ * @param sequences sequences, preferable produced by {@link #mergeDynamicSequences(List, ChronoUnit)}, to be transformed
+ * @param precisionThreshold a precision threshold to determine effectively constant sequences
+ * @return transformed sequencing where sequences that are consequent and effectively constant for the provided precision threshold are merged
+ */
+ private static List mergeConsequentEffectivelyConstantSequences(
+ final List sequences, final ChronoUnit precisionThreshold) {
+
+ // Short-circuit if there is nothing to merge
+ if (sequences.size() < 2) {
+ return sequences;
+ }
+
+ final List mergedSequences = new ArrayList<>();
+ boolean accumulatorConstant = true;
+ final List accumulatedSequences = new ArrayList<>();
+ for (final PatternSequence sequence : sequences) {
+ final boolean sequenceConstant = sequence.isConstantForDurationOf(precisionThreshold);
+ if (sequenceConstant != accumulatorConstant) {
+ mergeConsequentEffectivelyConstantSequences(mergedSequences, accumulatedSequences);
+ accumulatorConstant = sequenceConstant;
+ }
+ accumulatedSequences.add(sequence);
+ }
+
+ // Merge the accumulator leftover
+ mergeConsequentEffectivelyConstantSequences(mergedSequences, accumulatedSequences);
+ return mergedSequences;
+ }
+
+ private static void mergeConsequentEffectivelyConstantSequences(
+ final List mergedSequences, final List accumulatedSequences) {
+ mergeAccumulatedSequences(
+ mergedSequences, accumulatedSequences, () -> new CompositePatternSequence(accumulatedSequences));
+ }
+
+ private static void mergeAccumulatedSequences(
+ final List mergedSequences,
+ final List accumulatedSequences,
+ final Supplier mergedSequenceSupplier) {
+ if (accumulatedSequences.isEmpty()) {
+ return;
+ }
+ final PatternSequence mergedSequence =
+ accumulatedSequences.size() == 1 ? accumulatedSequences.get(0) : mergedSequenceSupplier.get();
+ mergedSequences.add(mergedSequence);
+ accumulatedSequences.clear();
+ }
+
+ private static long toEpochMinutes(final Instant instant) {
+ return instant.getEpochSecond() / 60;
+ }
+
+ private static TemporalAccessor toTemporalAccessor(final Instant instant) {
+ return instant instanceof TemporalAccessor
+ ? (TemporalAccessor) instant
+ : java.time.Instant.ofEpochSecond(instant.getEpochSecond(), instant.getNanoOfSecond());
+ }
+
+ private abstract static class AbstractFormatter implements InstantPatternFormatter {
+
+ private final String pattern;
+
+ private final Locale locale;
+
+ private final TimeZone timeZone;
+
+ private final ChronoUnit precision;
+
+ private AbstractFormatter(
+ final String pattern, final Locale locale, final TimeZone timeZone, final ChronoUnit precision) {
+ this.pattern = pattern;
+ this.locale = locale;
+ this.timeZone = timeZone;
+ this.precision = precision;
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return precision;
+ }
+
+ @Override
+ public String getPattern() {
+ return pattern;
+ }
+
+ @Override
+ public Locale getLocale() {
+ return locale;
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+ }
+
+ abstract static class PatternSequence {
+
+ final String pattern;
+
+ final ChronoUnit precision;
+
+ @SuppressWarnings("ReturnValueIgnored")
+ PatternSequence(final String pattern, final ChronoUnit precision) {
+ DateTimeFormatter.ofPattern(pattern); // Validate the pattern
+ this.pattern = pattern;
+ this.precision = precision;
+ }
+
+ InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone) {
+ final DateTimeFormatter dateTimeFormatter =
+ DateTimeFormatter.ofPattern(pattern, locale).withZone(timeZone.toZoneId());
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ final TemporalAccessor instantAccessor = toTemporalAccessor(instant);
+ dateTimeFormatter.formatTo(instantAccessor, buffer);
+ }
+ };
+ }
+
+ private boolean isConstantForDurationOf(final ChronoUnit thresholdPrecision) {
+ return precision.compareTo(thresholdPrecision) >= 0;
+ }
+
+ @Override
+ public boolean equals(final Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ PatternSequence sequence = (PatternSequence) object;
+ return Objects.equals(pattern, sequence.pattern) && precision == sequence.precision;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pattern, precision);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("<%s>%s", pattern, precision);
+ }
+ }
+
+ static final class StaticPatternSequence extends PatternSequence {
+
+ private final String literal;
+
+ StaticPatternSequence(final String literal) {
+ super(literal.equals("'") ? "''" : ("'" + literal + "'"), ChronoUnit.FOREVER);
+ this.literal = literal;
+ }
+
+ @Override
+ InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone) {
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ buffer.append(literal);
+ }
+ };
+ }
+ }
+
+ static final class DynamicPatternSequence extends PatternSequence {
+
+ DynamicPatternSequence(final String content) {
+ super(content, contentPrecision(content));
+ }
+
+ /**
+ * @param content a single-letter directive content complying (e.g., {@code H}, {@code HH}, or {@code pHH})
+ * @return the time precision of the directive
+ */
+ @Nullable
+ private static ChronoUnit contentPrecision(final String content) {
+
+ validateContent(content);
+ final String paddingRemovedContent = removePadding(content);
+
+ if (paddingRemovedContent.matches("[GuyY]+")) {
+ return ChronoUnit.YEARS;
+ } else if (paddingRemovedContent.matches("[MLQq]+")) {
+ return ChronoUnit.MONTHS;
+ } else if (paddingRemovedContent.matches("[wW]+")) {
+ return ChronoUnit.WEEKS;
+ } else if (paddingRemovedContent.matches("[DdgEecF]+")) {
+ return ChronoUnit.DAYS;
+ } else if (paddingRemovedContent.matches("[aBhKkH]+")
+ // Time-zone directives
+ || paddingRemovedContent.matches("[ZxXOzvV]+")) {
+ return ChronoUnit.HOURS;
+ } else if (paddingRemovedContent.contains("m")) {
+ return ChronoUnit.MINUTES;
+ } else if (paddingRemovedContent.contains("s")) {
+ return ChronoUnit.SECONDS;
+ }
+
+ // 2 to 3 consequent `S` characters output millisecond precision
+ else if (paddingRemovedContent.matches("S{2,3}")
+ // `A` (milli-of-day) outputs millisecond precision.
+ || paddingRemovedContent.contains("A")) {
+ return ChronoUnit.MILLIS;
+ }
+
+ // 4 to 6 consequent `S` characters output microsecond precision
+ else if (paddingRemovedContent.matches("S{4,6}")) {
+ return ChronoUnit.MICROS;
+ }
+
+ // A single `S` (fraction-of-second) outputs nanosecond precision
+ else if (paddingRemovedContent.equals("S")
+ // 7 to 9 consequent `S` characters output nanosecond precision
+ || paddingRemovedContent.matches("S{7,9}")
+ // `n` (nano-of-second) and `N` (nano-of-day) always output nanosecond precision.
+ // This is independent of how many times they occur sequentially.
+ || paddingRemovedContent.matches("[nN]+")) {
+ return ChronoUnit.NANOS;
+ }
+
+ final String message = String.format("unrecognized pattern: `%s`", content);
+ throw new IllegalArgumentException(message);
+ }
+
+ private static void validateContent(final String content) {
+
+ // Is the content empty?
+ final String paddingRemovedContent = removePadding(content);
+ if (paddingRemovedContent.isEmpty()) {
+ final String message = String.format("empty content: `%s`", content);
+ throw new IllegalArgumentException(message);
+ }
+
+ // Does the content start with a recognized letter?
+ final char letter = paddingRemovedContent.charAt(0);
+ final boolean dynamic = isDynamicPatternLetter(letter);
+ if (!dynamic) {
+ String message =
+ String.format("pattern sequence doesn't start with a dynamic pattern letter: `%s`", content);
+ throw new IllegalArgumentException(message);
+ }
+
+ // Is the content composed of repetitions of the first letter?
+ final boolean repeated = paddingRemovedContent.matches("^(\\Q" + letter + "\\E)+$");
+ if (!repeated) {
+ String message = String.format(
+ "was expecting letter `%c` to be repeated through the entire pattern sequence: `%s`",
+ letter, content);
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ private static String removePadding(final String content) {
+ return content.replaceAll("^p+", "");
+ }
+ }
+
+ static final class CompositePatternSequence extends PatternSequence {
+
+ CompositePatternSequence(final List sequences) {
+ super(concatSequencePatterns(sequences), findSequenceMaxPrecision(sequences));
+ // Only allow two or more sequences
+ if (sequences.size() < 2) {
+ throw new IllegalArgumentException("was expecting two or more sequences: " + sequences);
+ }
+ }
+
+ @SuppressWarnings("OptionalGetWithoutIsPresent")
+ private static ChronoUnit findSequenceMaxPrecision(List sequences) {
+ return sequences.stream()
+ .map(sequence -> sequence.precision)
+ .min(Comparator.comparing(ChronoUnit::getDuration))
+ .get();
+ }
+
+ private static String concatSequencePatterns(List sequences) {
+ return sequences.stream().map(sequence -> sequence.pattern).collect(Collectors.joining());
+ }
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
similarity index 63%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternFormatter.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
index 96e5613fba5..37d16ab8f20 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
@@ -14,15 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.core.util.internal;
+package org.apache.logging.log4j.core.util.internal.instant;
import static java.util.Objects.requireNonNull;
import static org.apache.logging.log4j.util.Strings.isBlank;
+import java.time.temporal.ChronoUnit;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.util.PropertiesUtil;
/**
* Contract for formatting {@link Instant}s using a date & time formatting pattern.
@@ -31,6 +33,8 @@
*/
public interface InstantPatternFormatter extends InstantFormatter {
+ boolean LEGACY_FORMATTERS_ENABLED = "legacy".equalsIgnoreCase(PropertiesUtil.getProperties().getStringProperty("log4j2.instant.formatter"));
+
String getPattern();
Locale getLocale();
@@ -51,6 +55,8 @@ final class Builder {
private boolean cachingEnabled = Constants.ENABLE_THREADLOCALS;
+ private boolean legacyFormattersEnabled = LEGACY_FORMATTERS_ENABLED;
+
private Builder() {}
public String getPattern() {
@@ -89,13 +95,39 @@ public Builder setCachingEnabled(boolean cachingEnabled) {
return this;
}
+ public boolean isLegacyFormattersEnabled() {
+ return legacyFormattersEnabled;
+ }
+
+ public Builder setLegacyFormattersEnabled(boolean legacyFormattersEnabled) {
+ this.legacyFormattersEnabled = legacyFormattersEnabled;
+ return this;
+ }
+
public InstantPatternFormatter build() {
- validate();
+
+ // Validate arguments
+ requireNonNull(locale, "locale");
+ requireNonNull(timeZone, "timeZone");
+
+ // Return a literal formatter if the pattern is blank
+ if (isBlank(pattern)) {
+ return createLiteralFormatter(pattern, locale, timeZone);
+ }
+
+ // Return legacy formatters, if requested
+ if (legacyFormattersEnabled) {
+ return new InstantPatternLegacyFormatter(pattern, locale, timeZone);
+ }
+
+ // Create the formatter, and return it, if caching is disabled
final InstantPatternDynamicFormatter formatter =
new InstantPatternDynamicFormatter(pattern, locale, timeZone);
if (!cachingEnabled) {
return formatter;
}
+
+ // Wrap the formatter with caching, if necessary
switch (formatter.getPrecision()) {
// It is not worth caching when a precision equal to or higher than microsecond is requested
@@ -113,12 +145,35 @@ public InstantPatternFormatter build() {
}
}
- private void validate() {
- if (isBlank(pattern)) {
- throw new IllegalArgumentException("blank pattern");
- }
- requireNonNull(locale, "locale");
- requireNonNull(timeZone, "timeZone");
+ private static InstantPatternFormatter createLiteralFormatter(
+ final String literal, final Locale locale, final TimeZone timeZone) {
+ return new InstantPatternFormatter() {
+
+ @Override
+ public String getPattern() {
+ return literal;
+ }
+
+ @Override
+ public Locale getLocale() {
+ return locale;
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return ChronoUnit.FOREVER;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ buffer.append(literal);
+ }
+ };
}
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternLegacyFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternLegacyFormatter.java
new file mode 100644
index 00000000000..aaf380c7f0a
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternLegacyFormatter.java
@@ -0,0 +1,118 @@
+/*
+ * 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.core.util.internal.instant;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.temporal.ChronoUnit;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.core.util.datetime.FastDateFormat;
+import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
+import org.apache.logging.log4j.util.BiConsumer;
+
+/**
+ * A {@link InstantPatternFormatter} implementation using {@link FixedDateFormat} and {@link FastDateFormat} under the hood.
+ */
+@SuppressWarnings("deprecation")
+final class InstantPatternLegacyFormatter implements InstantPatternFormatter {
+
+ private final ChronoUnit precision;
+
+ private final String pattern;
+
+ private final Locale locale;
+
+ private final TimeZone timeZone;
+
+ private final BiConsumer formatter;
+
+ InstantPatternLegacyFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
+ this.precision = new InstantPatternDynamicFormatter(pattern, locale, timeZone).getPrecision();
+ this.pattern = pattern;
+ this.locale = locale;
+ this.timeZone = timeZone;
+ this.formatter = createFormatter(pattern, locale, timeZone);
+ }
+
+ private static BiConsumer createFormatter(
+ final String pattern, final Locale locale, final TimeZone timeZone) {
+ final FixedDateFormat fixedFormatter = FixedDateFormat.createIfSupported(pattern, timeZone.getID());
+ return fixedFormatter != null
+ ? adaptFixedFormatter(fixedFormatter)
+ : createFastFormatter(pattern, locale, timeZone);
+ }
+
+ private static BiConsumer adaptFixedFormatter(final FixedDateFormat formatter) {
+ final Supplier charBufferSupplier = memoryEfficientInstanceSupplier(() -> {
+ // Double size for locales with lengthy `DateFormatSymbols`
+ return new char[formatter.getLength() << 1];
+ });
+ return (buffer, instant) -> {
+ final char[] charBuffer = charBufferSupplier.get();
+ final int length = formatter.formatInstant(instant, charBuffer, 0);
+ buffer.append(charBuffer, 0, length);
+ };
+ }
+
+ private static BiConsumer createFastFormatter(
+ final String pattern, final Locale locale, final TimeZone timeZone) {
+ final FastDateFormat formatter = FastDateFormat.getInstance(pattern, timeZone, locale);
+ final Supplier calendarSupplier =
+ memoryEfficientInstanceSupplier(() -> Calendar.getInstance(timeZone, locale));
+ return (buffer, instant) -> {
+ final Calendar calendar = calendarSupplier.get();
+ calendar.setTimeInMillis(instant.getEpochMillisecond());
+ formatter.format(calendar, buffer);
+ };
+ }
+
+ private static Supplier memoryEfficientInstanceSupplier(final Supplier supplier) {
+ return Constants.ENABLE_THREADLOCALS ? ThreadLocal.withInitial(supplier)::get : supplier;
+ }
+
+ @Override
+ public ChronoUnit getPrecision() {
+ return precision;
+ }
+
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ requireNonNull(buffer, "buffer");
+ requireNonNull(instant, "instant");
+ formatter.accept(buffer, instant);
+ }
+
+ @Override
+ public String getPattern() {
+ return pattern;
+ }
+
+ @Override
+ public Locale getLocale() {
+ return locale;
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatter.java
similarity index 98%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatter.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatter.java
index 3c5291f7fa0..96bf4504aa3 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatter.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.core.util.internal;
+package org.apache.logging.log4j.core.util.internal.instant;
import static java.util.Objects.requireNonNull;
@@ -23,14 +23,12 @@
import java.util.TimeZone;
import java.util.function.Function;
import org.apache.logging.log4j.core.time.Instant;
-import org.jspecify.annotations.NullMarked;
/**
* An {@link InstantFormatter} wrapper caching the last formatted output in a {@link ThreadLocal} and trying to reuse it.
*
* @since 2.25.0
*/
-@NullMarked
final class InstantPatternThreadLocalCachedFormatter implements InstantPatternFormatter {
private final InstantPatternFormatter formatter;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
new file mode 100644
index 00000000000..b6fb098c6c5
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+@NullMarked
+package org.apache.logging.log4j.core.util.internal.instant;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
index 489bdda1fab..1cd3f6bb0db 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
@@ -20,9 +20,9 @@
import java.util.TimeZone;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.time.Instant;
-import org.apache.logging.log4j.core.util.internal.InstantFormatter;
-import org.apache.logging.log4j.core.util.internal.InstantNumberFormatter;
-import org.apache.logging.log4j.core.util.internal.InstantPatternFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantNumberFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/InstantFormatBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/InstantFormatBenchmark.java
index 6dfe084100d..6cd6de640e2 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/InstantFormatBenchmark.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/InstantFormatBenchmark.java
@@ -43,6 +43,7 @@
* Benchmarks certain {@link Instant} formatters with various patterns and instant collections.
*/
@State(Scope.Thread)
+@SuppressWarnings("deprecation")
public class InstantFormatBenchmark {
private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java
new file mode 100644
index 00000000000..d4fdbbe4e79
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java
@@ -0,0 +1,96 @@
+/*
+ * 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.perf.jmh.instant;
+
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.stream.IntStream;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Compares {@link DateTimeFormatter} efficiency for formatting the {@code ss.SSS} singleton versus formatting the {@code ss}, {@code .}, and {@code SSS} sequence.
+ * This comparison is influential on the sequence merging strategies of {@link org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter}.
+ */
+@State(Scope.Thread)
+public class InstantPatternDynamicFormatterSequencingBenchmark {
+
+ static final Locale LOCALE = Locale.US;
+
+ static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
+
+ private static final Instant[] INSTANTS = createInstants();
+
+ private static Instant[] createInstants() {
+ final Instant initInstant = Instant.parse("2020-05-14T10:44:23.901Z");
+ return IntStream.range(0, 1_000)
+ .mapToObj((final int index) -> Instant.ofEpochSecond(
+ Math.addExact(initInstant.getEpochSecond(), index),
+ Math.addExact(initInstant.getNano(), index)))
+ .toArray(Instant[]::new);
+ }
+
+ @FunctionalInterface
+ private interface Formatter {
+
+ void formatTo(TemporalAccessor instantAccessor, StringBuilder buffer);
+ }
+
+ private static final Formatter SINGLETON_FORMATTER =
+ DateTimeFormatter.ofPattern("ss.SSS").withLocale(LOCALE).withZone(TIME_ZONE.toZoneId())::formatTo;
+
+ private static final Formatter SEQUENCED_FORMATTER = new Formatter() {
+
+ private final Formatter[] formatters = {
+ DateTimeFormatter.ofPattern("ss").withLocale(LOCALE).withZone(TIME_ZONE.toZoneId())::formatTo,
+ (temporal, appendable) -> appendable.append("."),
+ DateTimeFormatter.ofPattern("SSS").withLocale(LOCALE).withZone(TIME_ZONE.toZoneId())::formatTo
+ };
+
+ @Override
+ public void formatTo(final TemporalAccessor instantAccessor, final StringBuilder buffer) {
+ for (Formatter formatter : formatters) {
+ formatter.formatTo(instantAccessor, buffer);
+ }
+ }
+ };
+
+ private final StringBuilder buffer = new StringBuilder();
+
+ @Benchmark
+ public void singleton(final Blackhole blackhole) {
+ benchmark(blackhole, SINGLETON_FORMATTER);
+ }
+
+ @Benchmark
+ public void sequenced(final Blackhole blackhole) {
+ benchmark(blackhole, SEQUENCED_FORMATTER);
+ }
+
+ private void benchmark(final Blackhole blackhole, final Formatter formatter) {
+ for (final Instant instant : INSTANTS) {
+ formatter.formatTo(instant, buffer);
+ blackhole.consume(buffer);
+ buffer.setLength(0);
+ }
+ }
+}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
similarity index 66%
rename from log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java
rename to log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
index 06b4818df83..ac07b476cbb 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.perf.jmh;
+package org.apache.logging.log4j.perf.jmh.instant;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
@@ -29,23 +29,26 @@
import org.apache.logging.log4j.core.time.MutableInstant;
import org.apache.logging.log4j.core.util.datetime.FastDatePrinter;
import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;
/**
- * Compares {@link MutableInstant} formatting efficiency of {@link FastDatePrinter}, {@link FixedDateFormat}, and {@link DateTimeFormatter}.
+ * Compares {@link MutableInstant} formatting efficiency of {@link InstantPatternFormatter}, {@link FastDatePrinter}, {@link FixedDateFormat}, and {@link DateTimeFormatter}.
*
* The major formatting efficiency is mostly provided by caching, i.e., reusing the earlier formatter output if timestamps match.
- * We deliberately exclude this optimization, since it is applicable to all formatters.
+ * We deliberately exclude this optimization (by means of always distinct instants), since it is applicable to all formatters.
* This benchmark rather focuses on only and only the formatting efficiency.
*
*
- * @see DateTimeFormatImpactBenchmark for the performance impact of different date & time formatters on a typical layout
+ * @see InstantPatternFormatterImpactBenchmark for the performance impact of different date & time formatters on a typical layout
*/
@State(Scope.Thread)
-public class DateTimeFormatBenchmark {
+@SuppressWarnings("deprecation")
+public class InstantPatternFormatterBenchmark {
static final Locale LOCALE = Locale.US;
@@ -57,26 +60,40 @@ private static MutableInstant[] createInstants() {
final Instant initInstant = Instant.parse("2020-05-14T10:44:23.901Z");
MutableInstant[] instants = IntStream.range(0, 1_000)
.mapToObj((final int index) -> {
- final MutableInstant instant = new MutableInstant();
- instant.initFromEpochSecond(
- Math.addExact(initInstant.getEpochSecond(), index),
- Math.addExact(initInstant.getNano(), index));
- return instant;
+ final Instant instant = initInstant.plusMillis(index).plusNanos(1);
+ final MutableInstant mutableInstant = new MutableInstant();
+ mutableInstant.initFromEpochSecond(instant.getEpochSecond(), instant.getNano());
+ return mutableInstant;
})
.toArray(MutableInstant[]::new);
- validateEpochMillisForFixedDateFormatCache(
- () -> Arrays.stream(instants).mapToLong(MutableInstant::getEpochMillisecond));
+ validateInstants(instants);
return instants;
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
- static void validateEpochMillisForFixedDateFormatCache(final Supplier millisStreamSupplier) {
+ static void validateInstants(final I[] instants) {
+
+ // Find the instant offset
+ final Supplier millisStreamSupplier = () ->
+ Arrays.stream(instants).mapToLong(org.apache.logging.log4j.core.time.Instant::getEpochMillisecond);
final long minMillis = millisStreamSupplier.get().min().getAsLong();
final long maxMillis = millisStreamSupplier.get().max().getAsLong();
final long offMillis = maxMillis - minMillis;
+
+ // Validate for `FixedDateFormat`
if (TimeUnit.DAYS.toMillis(1) <= offMillis) {
- throw new IllegalStateException(
- "instant samples must be of the same day to exploit the `FixedDateTime` caching");
+ final String message = String.format(
+ "instant samples must be of the same day to exploit the `%s` caching",
+ FixedDateFormat.class.getSimpleName());
+ throw new IllegalStateException(message);
+ }
+
+ // Validate for `InstantPatternDynamicFormatter`
+ if (TimeUnit.MINUTES.toMillis(1) <= offMillis) {
+ final String message = String.format(
+ "instant samples must be of the same week to exploit the `%s` caching",
+ InstantPatternDynamicFormatter.class.getSimpleName());
+ throw new IllegalStateException(message);
}
}
@@ -92,6 +109,8 @@ static final class Formatters {
final FixedDateFormat fixedFormatter;
+ final InstantPatternFormatter instantFormatter;
+
final DateTimeFormatter javaFormatter;
Formatters(final String pattern) {
@@ -102,7 +121,14 @@ static final class Formatters {
final String message = String.format(
"couldn't create `%s` for pattern `%s` and time zone `%s`",
FixedDateFormat.class.getSimpleName(), pattern, TIME_ZONE.getID());
+ throw new IllegalStateException(message);
}
+ this.instantFormatter = InstantPatternFormatter.newBuilder()
+ .setPattern(pattern)
+ .setLocale(LOCALE)
+ .setTimeZone(TIME_ZONE)
+ .setCachingEnabled(false)
+ .build();
this.javaFormatter = DateTimeFormatter.ofPattern(pattern)
.withZone(TIME_ZONE.toZoneId())
.withLocale(LOCALE);
@@ -116,6 +142,24 @@ static final class Formatters {
private final Calendar calendar = Calendar.getInstance(TIME_ZONE, LOCALE);
+ @Benchmark
+ public void instantFormatter_dateTime(final Blackhole blackhole) {
+ instantFormatter(blackhole, DATE_TIME_FORMATTERS.instantFormatter);
+ }
+
+ @Benchmark
+ public void instantFormatter_time(final Blackhole blackhole) {
+ instantFormatter(blackhole, TIME_FORMATTERS.instantFormatter);
+ }
+
+ private void instantFormatter(final Blackhole blackhole, final InstantPatternFormatter formatter) {
+ for (final MutableInstant instant : INSTANTS) {
+ stringBuilder.setLength(0);
+ formatter.formatTo(stringBuilder, instant);
+ blackhole.consume(stringBuilder.length());
+ }
+ }
+
@Benchmark
public void fastFormatter_dateTime(final Blackhole blackhole) {
fastFormatter(blackhole, DATE_TIME_FORMATTERS.fastFormatter);
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatImpactBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java
similarity index 75%
rename from log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatImpactBenchmark.java
rename to log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java
index 2d91b04f969..292d1f041a9 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatImpactBenchmark.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java
@@ -14,9 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.perf.jmh;
+package org.apache.logging.log4j.perf.jmh.instant;
-import static org.apache.logging.log4j.perf.jmh.DateTimeFormatBenchmark.validateEpochMillisForFixedDateFormatCache;
+import static org.apache.logging.log4j.perf.jmh.instant.InstantPatternFormatterBenchmark.validateInstants;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
@@ -25,9 +25,11 @@
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.NullConfiguration;
import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.MutableInstant;
import org.apache.logging.log4j.core.util.datetime.FastDatePrinter;
import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
import org.apache.logging.log4j.layout.template.json.LogEventFixture;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
@@ -37,10 +39,11 @@
/**
* Benchmarks the impact of different date & time formatters on a typical layout.
*
- * @see DateTimeFormatBenchmark for isolated benchmarks of date & time formatters
+ * @see InstantPatternFormatterBenchmark for isolated benchmarks of date & time formatters
*/
@State(Scope.Thread)
-public class DateTimeFormatImpactBenchmark {
+@SuppressWarnings("deprecation")
+public class InstantPatternFormatterImpactBenchmark {
private static final List LITE_LOG_EVENTS = createLogEvents(LogEventFixture::createLiteLogEvents);
@@ -52,7 +55,8 @@ private static List createLogEvents(final BiFunction logEvents.stream().mapToLong(LogEvent::getTimeMillis));
+ final Instant[] instants = logEvents.stream().map(LogEvent::getInstant).toArray(Instant[]::new);
+ validateInstants(instants);
return logEvents;
}
@@ -63,15 +67,15 @@ private static List createLogEvents(final BiFunction logEvents, final InstantPatternFormatter formatter) {
+ // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
+ for (int logEventIndex = 0; logEventIndex < logEvents.size(); logEventIndex++) {
+
+ // 1. Encode event
+ final LogEvent logEvent = logEvents.get(logEventIndex);
+ stringBuilder.setLength(0);
+ LAYOUT.serialize(logEvent, stringBuilder);
+
+ // 2. Encode date & time
+ final MutableInstant instant = (MutableInstant) logEvent.getInstant();
+ formatter.formatTo(stringBuilder, instant);
+ blackhole.consume(stringBuilder.length());
+ }
+ }
+
@Benchmark
public void javaFormatter_lite(final Blackhole blackhole) {
javaFormatter(blackhole, LITE_LOG_EVENTS, FORMATTERS.javaFormatter);
diff --git a/src/changelog/.2.x.x/.release-notes.adoc.ftl b/src/changelog/.2.x.x/.release-notes.adoc.ftl
index 47b4447337a..c8d30dd67f6 100644
--- a/src/changelog/.2.x.x/.release-notes.adoc.ftl
+++ b/src/changelog/.2.x.x/.release-notes.adoc.ftl
@@ -38,6 +38,13 @@ This effectively helped with fixing some bugs by matching the feature parity of
Additionally, rendered stack traces are ensured to be prefixed with a newline, which used to be a whitespace in earlier versions.
The support for the `\{ansi}` option in exception converters is removed too.
+[#release-notes-2-25-0-instant-format]
+=== Date & time formatting
+
+Historically, Log4j contains some date & time formatting utilities for performance reasons, in particular, link:javadoc/log4j-core/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.html[`FixedDateFormat`] and link:javadoc/log4j-core/org/apache/logging/log4j/core/util/datetime/FastDateFormat.html[`FastDateFormat`].
+These have been deprecated for removal in favor of Java's https://docs.oracle.com/javase/{java-target-version}/docs/api/java/time/format/DateTimeFormatter.html[`DateTimeFormatter`].
+After upgrading, if you experience any date & time formatting problems, please {logging-services-url}/support.html#issues[submit an issue ticket] – as a temporary workaround, you can set xref:manual/systemproperties.adoc#log4j2.instant.formatter[the `log4j2.instant.formatter` property] to `legacy` to switch to the old behaviour.
+
=== ANSI support on Windows
Since 2017, Windows 10 and newer have offered native support for ANSI escapes.
diff --git a/src/changelog/.2.x.x/2936_deprecate_AbstractLogger_checkMessageFactory.xml b/src/changelog/.2.x.x/2936_deprecate_AbstractLogger_checkMessageFactory.xml
index e72a33566a4..d57d4d14282 100644
--- a/src/changelog/.2.x.x/2936_deprecate_AbstractLogger_checkMessageFactory.xml
+++ b/src/changelog/.2.x.x/2936_deprecate_AbstractLogger_checkMessageFactory.xml
@@ -4,5 +4,5 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="deprecated">
- Deprecate `AbstractLogger.checkMessageFactory()`, since all created `Logger`s are already `MessageFactory`-namespaced
+ Deprecate `AbstractLogger.checkMessageFactory()`, since all created ``Logger``s are already `MessageFactory`-namespaced
diff --git a/src/changelog/.2.x.x/3121_deprecate_FixedDateFormat.xml b/src/changelog/.2.x.x/3121_deprecate_FixedDateFormat.xml
new file mode 100644
index 00000000000..f6c318a25f4
--- /dev/null
+++ b/src/changelog/.2.x.x/3121_deprecate_FixedDateFormat.xml
@@ -0,0 +1,8 @@
+
+
+
+ Deprecated `FixedDateTime`, `FastDateTime`, and supporting classes
+
diff --git a/src/changelog/.2.x.x/3121_instant_format.xml b/src/changelog/.2.x.x/3121_instant_format.xml
new file mode 100644
index 00000000000..4930f7629e8
--- /dev/null
+++ b/src/changelog/.2.x.x/3121_instant_format.xml
@@ -0,0 +1,8 @@
+
+
+
+ Switch to using Java's `DateTimeFormatter` for date & time formatting of log event instants
+
diff --git a/src/changelog/.index.adoc.ftl b/src/changelog/.index.adoc.ftl
index b71f9905f31..24c1ef38957 100644
--- a/src/changelog/.index.adoc.ftl
+++ b/src/changelog/.index.adoc.ftl
@@ -37,7 +37,7 @@
:page-toclevels: 1
[#release-notes]
-= Release Notes
+= Release notes
<#list releases as release><#if release.changelogEntryCount gt 0>
include::_release-notes/${release.version}.adoc[]
diff --git a/src/changelog/2.23.1/fix_StatusLogger_instant_formatting.xml b/src/changelog/2.23.1/fix_StatusLogger_instant_formatting.xml
index 4b965ca3c09..f69f0524b14 100644
--- a/src/changelog/2.23.1/fix_StatusLogger_instant_formatting.xml
+++ b/src/changelog/2.23.1/fix_StatusLogger_instant_formatting.xml
@@ -6,7 +6,7 @@
Add
- xref:manual/statusLogger.adoc#log4j2.statusLoggerDateFormatZone[`log4j2.statusLoggerDateFormatZone`]
+ xref:manual/status-logger.adoc#log4j2.statusLoggerDateFormatZone[`log4j2.statusLoggerDateFormatZone`]
system property to set the time-zone `StatusLogger` uses to format `java.time.Instant`.
Without this, formatting patterns accessing to time-zone-specific fields (e.g., year-of-era) cause failures.
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index 49723986482..2d8cb1d2433 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -385,7 +385,7 @@ See xref:manual/layouts.adoc#LocationInformation[this section of the layouts pag
[#converter-date]
==== Date
-Outputs the date of the log event
+Outputs the instant of the log event
.link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/DatePatternConverter.html[`DatePatternConverter`] specifier grammar
[source,text]
@@ -394,7 +394,7 @@ d{pattern}[{timezone}]
date{pattern}[{timezone}]
----
-The date conversion specifier may be followed by a set of braces containing a date and time pattern string per https://docs.oracle.com/javase/{java-target-version}/docs/api/java/text/SimpleDateFormat.html[`SimpleDateFormat`].
+The date conversion specifier may be followed by a set of braces containing a date and time formatting pattern per https://docs.oracle.com/javase/{java-target-version}/docs/api/java/time/format/DateTimeFormatter.html[`DateTimeFormatter`].
The predefined _named_ formats are:
[%header,cols="2m,3m"]
@@ -448,8 +448,8 @@ The predefined _named_ formats are:
|1351866842781
|===
-You can also use a set of braces containing a time zone id per https://docs.oracle.com/javase/{java-target-version}/docs/api/java/util/TimeZone.html#getTimeZone(java.lang.String)[`java.util.TimeZone#getTimeZone(String)`].
-If no date format specifier is given then the `DEFAULT` format is used.
+You can also use a set of braces containing a time zone id per https://docs.oracle.com/javase/{java-target-version}/docs/api/java/util/TimeZone.html#getTimeZone(java.lang.String)[`TimeZone#getTimeZone(String)`].
+If no date format specifier is given, then the `DEFAULT` format is used.
You can also define custom date formats, see following examples:
@@ -461,17 +461,8 @@ You can also define custom date formats, see following examples:
|%d{HH:mm:ss,SSS}
|14:34:02,123
-|%d{HH:mm:ss,nnnn} to %d{HH:mm:ss,nnnnnnnnn}
-|14:34:02,1234 to 14:34:02,123456789
-
-|%d{dd MMM yyyy HH:mm:ss,SSS}
-|02 Nov 2012 14:34:02,123
-
-|%d{dd MMM yyyy HH:mm:ss,nnnn} to %d{dd MMM yyyy HH:mm:ss,nnnnnnnnn}
-|02 Nov 2012 14:34:02,1234 to 02 Nov 2012 14:34:02,123456789
-
-|%d{HH:mm:ss}{GMT+0}
-|18:34:02
+|%d{yyyy-mm-dd'T'HH:mm:ss.SSS'Z'}\{UTC}
+|2012-11-02T14:34:02.123Z
|===
`%d\{UNIX}` outputs the UNIX time in seconds.
@@ -480,19 +471,8 @@ The `UNIX` time is the difference – in seconds for `UNIX` and in milliseconds
While the time unit is milliseconds, the granularity depends on the platform.
This is an efficient way to output the event time because only a conversion from `long` to `String` takes place, there is no `Date` formatting involved.
-There is also limited support for timestamps more precise than milliseconds when running on Java 9 or later.
-Note that not all
-https://docs.oracle.com/javase/{java-target-version}/docs/api/java/time/format/DateTimeFormatter.html[`DateTimeFormatter`]
-formats are supported.
-Only timestamps in the formats mentioned in the table above may use the _nano-of-second_ pattern letter `n` instead of the _fraction-of-second_ pattern letter `S`.
-
Users may revert to a millisecond-precision clock when running on Java 9 by setting xref:manual/systemproperties.adoc#log4j2.clock[the `log4j2.clock` system property] to `SystemMillisClock`.
-[WARNING]
-====
-Only named date formats (`DEFAULT`, `ISO8601`, `UNIX`, `UNIX_MILLIS`, etc.) are garbage-free.
-====
-
[#converter-encode]
==== Encode
diff --git a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-log4j-core-misc.adoc b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-log4j-core-misc.adoc
index 8ac34f1101d..beae1c22810 100644
--- a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-log4j-core-misc.adoc
+++ b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-log4j-core-misc.adoc
@@ -251,6 +251,20 @@ link:../javadoc/log4j-api/org/apache/logging/log4j/message/FlowMessageFactory.ht
implementation to be used by all loggers.
// end::flow-tracing[]
+[id=log4j2.instant.formatter]
+== `log4j2.instant.formatter`
+
+[cols="1h,5"]
+|===
+| Env. variable | `LOG4J_INSTANT_FORMATTER`
+| Type | `String`
+|===
+
+Configures the date & time formatter used for log event instants.
+The following values are accepted:
+
+`legacy`:: Enables the usage of legacy formatters (i.e., link:javadoc/log4j-core/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.html[`FixedDateFormat`] and link:javadoc/log4j-core/org/apache/logging/log4j/core/util/datetime/FastDateFormat.html[`FastDateFormat`])
+
[id=log4j2.loggerContextStacktraceOnStart]
== `log4j2.loggerContextStacktraceOnStart`
From 3e39405a501771f320d51b16180e0ff29f56c093 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Fri, 25 Oct 2024 13:37:20 +0200
Subject: [PATCH 08/21] Fix Javadoc
---
.../util/internal/instant/InstantPatternDynamicFormatter.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
index 35bcf686eeb..00b7e280a39 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
@@ -37,7 +37,7 @@
/**
* An {@link InstantPatternFormatter} that uses {@link DateTimeFormatter} under the hood.
- * The pattern is analyzed and parts that are not of {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} precision are precomputed, cached, and updated once every {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD}.
+ * The pattern is analyzed and parts that require a precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} are precomputed, cached, and updated once every {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD}.
* The rest is computed dynamically.
*
* For instance, given the pattern {@code yyyy-MM-dd'T'HH:mm:ss.SSSX}, the generated formatter will
From 8d3ba30906d9f58fa01fc2ed9097af666089c2b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Fri, 25 Oct 2024 13:42:31 +0200
Subject: [PATCH 09/21] More Javadoc fixes
---
.../util/internal/instant/InstantPatternDynamicFormatter.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
index 00b7e280a39..f1ebb7c8c3b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
@@ -44,8 +44,8 @@
*
*
*
Sequence the pattern and assign a time precision to each part (e.g., {@code MM} is of month precision)
- *
Precompute and cache the output for parts that are of precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} (i.e., {@code yyyy-MM-dd'T'HH:mm:}, {@code .}, and {@code X}) and cache it
- *
Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss} and {@code SSS})
+ *
Precompute and cache the output for parts that are of precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} (i.e., {@code yyyy-MM-dd'T'HH:mm:} and {@code X}) and cache it
+ *
Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss.SSS})
*
*
* @since 2.25.0
From 80a7c2721c3fafc5e4613a5b77de1ddf28f53e25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Fri, 25 Oct 2024 13:44:19 +0200
Subject: [PATCH 10/21] Fix Spotless failures
---
.../core/util/internal/instant/InstantPatternFormatter.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
index 37d16ab8f20..138eb09a874 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
@@ -33,7 +33,8 @@
*/
public interface InstantPatternFormatter extends InstantFormatter {
- boolean LEGACY_FORMATTERS_ENABLED = "legacy".equalsIgnoreCase(PropertiesUtil.getProperties().getStringProperty("log4j2.instant.formatter"));
+ boolean LEGACY_FORMATTERS_ENABLED =
+ "legacy".equalsIgnoreCase(PropertiesUtil.getProperties().getStringProperty("log4j2.instant.formatter"));
String getPattern();
From 4f431d662017d810f0dcc5b649d01c3786aa9fbc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Fri, 25 Oct 2024 14:24:36 +0200
Subject: [PATCH 11/21] Add #2943 to the changelog
---
src/changelog/.2.x.x/3121_instant_format.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/changelog/.2.x.x/3121_instant_format.xml b/src/changelog/.2.x.x/3121_instant_format.xml
index 4930f7629e8..271c3c3dd7a 100644
--- a/src/changelog/.2.x.x/3121_instant_format.xml
+++ b/src/changelog/.2.x.x/3121_instant_format.xml
@@ -4,5 +4,6 @@
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="changed">
+ Switch to using Java's `DateTimeFormatter` for date & time formatting of log event instants
From 2f74ffc05030e7b7ad9f87478ef723b7a30ec66f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Fri, 25 Oct 2024 20:01:11 +0200
Subject: [PATCH 12/21] Fix Java 8 errors
---
.../InstantPatternDynamicFormatterTest.java | 22 ++++++++++++-------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
index cb853f01ce6..7a724c7cf7f 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
@@ -37,6 +37,7 @@
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.DynamicPatternSequence;
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.PatternSequence;
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.StaticPatternSequence;
+import org.apache.logging.log4j.util.Constants;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -253,13 +254,17 @@ void should_recognize_patterns_of_minute_precision(final String pattern) {
}
@ParameterizedTest
- @ValueSource(
- strings = {
+ @MethodSource("hourPrecisionPatterns")
+ void should_recognize_patterns_of_hour_precision(final String pattern) {
+ assertPatternPrecision(pattern, ChronoUnit.HOURS);
+ }
+
+ static List hourPrecisionPatterns() {
+ final List java8Patterns = new ArrayList<>(asList(
// Basics
"H",
"HH",
"a",
- "B",
"h",
"K",
"k",
@@ -269,7 +274,6 @@ void should_recognize_patterns_of_minute_precision(final String pattern) {
"X",
"O",
"z",
- "v",
"VV",
// Mixed with other stuff
"yyyy-MM-dd HH",
@@ -279,10 +283,12 @@ void should_recognize_patterns_of_minute_precision(final String pattern) {
"ddHH",
// Single-quoted text containing nanosecond and millisecond directives
"yyyy-MM-dd'S'HH",
- "yyyy-MM-dd'n'HH"
- })
- void should_recognize_patterns_of_hour_precision(final String pattern) {
- assertPatternPrecision(pattern, ChronoUnit.HOURS);
+ "yyyy-MM-dd'n'HH"));
+ if (Constants.JAVA_MAJOR_VERSION > 8) {
+ java8Patterns.add("B");
+ java8Patterns.add("v");
+ }
+ return java8Patterns;
}
private static void assertPatternPrecision(final String pattern, final ChronoUnit expectedPrecision) {
From ff4f6882b618ed65bb547d4bcb3c46a1eb4f810e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Fri, 25 Oct 2024 20:22:39 +0200
Subject: [PATCH 13/21] Remove redundant `DatePatternConverter.Formatter`
---
.../core/pattern/DatePatternConverter.java | 69 ++++---------------
1 file changed, 14 insertions(+), 55 deletions(-)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
index bc334f84d24..f26a6d54c56 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
@@ -51,67 +51,23 @@ public final class DatePatternConverter extends LogEventPatternConverter impleme
private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS";
- private abstract static class Formatter {
-
- final F delegate;
-
- private Formatter(final F delegate) {
- this.delegate = delegate;
- }
-
- @Nullable
- public String toPattern() {
- return null;
- }
-
- public TimeZone getTimeZone() {
- return TimeZone.getDefault();
- }
- }
-
- private static final class PatternFormatter extends Formatter {
-
- private PatternFormatter(final InstantPatternFormatter delegate) {
- super(delegate);
- }
-
- @Override
- public String toPattern() {
- return delegate.getPattern();
- }
-
- @Override
- public TimeZone getTimeZone() {
- return delegate.getTimeZone();
- }
- }
-
- private static final class NumberFormatter extends Formatter {
-
- private NumberFormatter(final InstantNumberFormatter delegate) {
- super(delegate);
- }
- }
-
- private final Formatter> formatter;
+ private final InstantFormatter formatter;
private DatePatternConverter(@Nullable final String[] options) {
super("Date", "date");
this.formatter = createFormatter(options);
}
- private static Formatter> createFormatter(@Nullable final String[] options) {
+ private static InstantFormatter createFormatter(@Nullable final String[] options) {
try {
return createFormatterUnsafely(options);
} catch (final Exception error) {
logOptionReadFailure(options, error, "failed for options: {}, falling back to the default instance");
}
- final InstantPatternFormatter delegateFormatter =
- InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build();
- return new PatternFormatter(delegateFormatter);
+ return InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build();
}
- private static Formatter> createFormatterUnsafely(@Nullable final String[] options) {
+ private static InstantFormatter createFormatterUnsafely(@Nullable final String[] options) {
// Read options
final String pattern = readPattern(options);
@@ -120,20 +76,19 @@ private static Formatter> createFormatterUnsafely(@Nullable final String[] opt
// Is it epoch seconds?
if ("UNIX".equals(pattern)) {
- return new NumberFormatter(InstantNumberFormatter.EPOCH_SECONDS_ROUNDED);
+ return InstantNumberFormatter.EPOCH_SECONDS_ROUNDED;
}
// Is it epoch milliseconds?
if ("UNIX_MILLIS".equals(pattern)) {
- return new NumberFormatter(InstantNumberFormatter.EPOCH_MILLIS_ROUNDED);
+ return InstantNumberFormatter.EPOCH_MILLIS_ROUNDED;
}
- final InstantPatternFormatter delegateFormatter = InstantPatternFormatter.newBuilder()
+ return InstantPatternFormatter.newBuilder()
.setPattern(pattern)
.setTimeZone(timeZone)
.setLocale(locale)
.build();
- return new PatternFormatter(delegateFormatter);
}
private static String readPattern(@Nullable final String[] options) {
@@ -320,7 +275,7 @@ public void format(final long epochMillis, final StringBuilder buffer) {
*/
@Deprecated
public void format(final Instant instant, final StringBuilder buffer) {
- formatter.delegate.formatTo(buffer, instant);
+ formatter.formatTo(buffer, instant);
}
@Override
@@ -361,13 +316,17 @@ public void format(final StringBuilder buffer, @Nullable final Object... objects
* @return the pattern string describing this date format or {@code null} if the format does not have a pattern.
*/
public String getPattern() {
- return formatter.toPattern();
+ return (formatter instanceof InstantPatternFormatter)
+ ? ((InstantPatternFormatter) formatter).getPattern()
+ : null;
}
/**
* @return the time zone used by this date format
*/
public TimeZone getTimeZone() {
- return formatter.getTimeZone();
+ return (formatter instanceof InstantPatternFormatter)
+ ? ((InstantPatternFormatter) formatter).getTimeZone()
+ : null;
}
}
From 12ec63a9e05911dbf6e1806e2e77f6747ba38e7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Sun, 27 Oct 2024 20:38:42 +0100
Subject: [PATCH 14/21] Decrease the visibility of
`InstantPatternDynamicFormatter`
---
.../util/internal/instant/InstantPatternDynamicFormatter.java | 2 +-
.../InstantPatternDynamicFormatterSequencingBenchmark.java | 2 +-
.../perf/jmh/instant/InstantPatternFormatterBenchmark.java | 3 +--
3 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
index f1ebb7c8c3b..791bf1e8963 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
@@ -50,7 +50,7 @@
*
* @since 2.25.0
*/
-public final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
+final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
static final ChronoUnit PRECISION_THRESHOLD = ChronoUnit.MINUTES;
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java
index d4fdbbe4e79..d46c431ea0b 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternDynamicFormatterSequencingBenchmark.java
@@ -29,7 +29,7 @@
/**
* Compares {@link DateTimeFormatter} efficiency for formatting the {@code ss.SSS} singleton versus formatting the {@code ss}, {@code .}, and {@code SSS} sequence.
- * This comparison is influential on the sequence merging strategies of {@link org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter}.
+ * This comparison is influential on the sequence merging strategies of {@code InstantPatternDynamicFormatter}.
*/
@State(Scope.Thread)
public class InstantPatternDynamicFormatterSequencingBenchmark {
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
index ac07b476cbb..18ed205b621 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
@@ -29,7 +29,6 @@
import org.apache.logging.log4j.core.time.MutableInstant;
import org.apache.logging.log4j.core.util.datetime.FastDatePrinter;
import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
-import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter;
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
@@ -92,7 +91,7 @@ static void validateInsta
if (TimeUnit.MINUTES.toMillis(1) <= offMillis) {
final String message = String.format(
"instant samples must be of the same week to exploit the `%s` caching",
- InstantPatternDynamicFormatter.class.getSimpleName());
+ InstantPatternFormatter.class.getSimpleName());
throw new IllegalStateException(message);
}
}
From 8281082b00b0203077f7de7a3feb010cef7330ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Sun, 27 Oct 2024 20:39:10 +0100
Subject: [PATCH 15/21] Remove deprecated `TimeFormatBenchmark`
---
.../log4j/perf/jmh/TimeFormatBenchmark.java | 299 ------------------
1 file changed, 299 deletions(-)
delete mode 100644 log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
deleted file mode 100644
index 0a59cb23903..00000000000
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * 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.perf.jmh;
-
-import java.nio.ByteBuffer;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-import org.apache.logging.log4j.core.util.datetime.FastDateFormat;
-import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.BenchmarkMode;
-import org.openjdk.jmh.annotations.Mode;
-import org.openjdk.jmh.annotations.OutputTimeUnit;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.State;
-
-/**
- * Tests performance of various time format implementation.
- */
-@State(Scope.Benchmark)
-public class TimeFormatBenchmark {
-
- ThreadLocal threadLocalSimpleDateFormat = new ThreadLocal<>() {
- @Override
- protected SimpleDateFormat initialValue() {
- return new SimpleDateFormat("HH:mm:ss.SSS");
- }
- };
- FastDateFormat fastDateFormat = FastDateFormat.getInstance("HH:mm:ss.SSS");
- FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported("ABSOLUTE");
- volatile long midnightToday;
- volatile long midnightTomorrow;
-
- @State(Scope.Thread)
- public static class BufferState {
- final ByteBuffer buffer = ByteBuffer.allocate(12);
- final StringBuilder stringBuilder = new StringBuilder(12);
- final char[] charArray = new char[12];
- }
-
- private long millisSinceMidnight(final long now) {
- if (now >= midnightTomorrow) {
- midnightToday = calcMidnightMillis(now, 0);
- midnightTomorrow = calcMidnightMillis(now, 1);
- }
- return now - midnightToday;
- }
-
- private long calcMidnightMillis(final long time, final int addDays) {
- final Calendar cal = Calendar.getInstance();
- cal.setTimeInMillis(time);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- cal.add(Calendar.DATE, addDays);
- return cal.getTimeInMillis();
- }
-
- public static void main(final String[] args) {
- System.out.println(new TimeFormatBenchmark().fixedBitFiddlingReuseCharArray(new BufferState()));
- System.out.println(new TimeFormatBenchmark().fixedFormatReuseStringBuilder(new BufferState()));
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String simpleDateFormat() {
- return threadLocalSimpleDateFormat.get().format(new Date());
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fastDateFormatCreateNewStringBuilder() {
- return fastDateFormat.format(new Date());
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fastDateFormatReuseStringBuilder(final BufferState state) {
- state.stringBuilder.setLength(0);
- fastDateFormat.format(new Date(), state.stringBuilder);
- return new String(state.stringBuilder);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedBitFiddlingReuseCharArray(final BufferState state) {
- final int len = formatCharArrayBitFiddling(System.currentTimeMillis(), state.charArray, 0);
- return new String(state.charArray, 0, len);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedDateFormatCreateNewCharArray(final BufferState state) {
- return fixedDateFormat.format(System.currentTimeMillis());
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedDateFormatReuseCharArray(final BufferState state) {
- final int len = fixedDateFormat.format(System.currentTimeMillis(), state.charArray, 0);
- return new String(state.charArray, 0, len);
- }
-
- @Benchmark
- @BenchmarkMode(Mode.SampleTime)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public String fixedFormatReuseStringBuilder(final BufferState state) {
- state.stringBuilder.setLength(0);
- formatStringBuilder(System.currentTimeMillis(), state.stringBuilder);
- return new String(state.stringBuilder);
- }
-
- int formatCharArrayBitFiddling(final long time, final char[] buffer, final int pos) {
- // Calculate values by getting the ms values first and do then
- // shave off the hour minute and second values with multiplications
- // and bit shifts instead of simple but expensive divisions.
-
- // Get daytime in ms which does fit into an int
- // int ms = (int) (time % 86400000);
- int ms = (int) (millisSinceMidnight(time));
-
- // well ... it works
- final int hour = (int) (((ms >> 7) * 9773437L) >> 38);
- ms -= 3600000 * hour;
-
- final int minute = (int) (((ms >> 5) * 2290650L) >> 32);
- ms -= 60000 * minute;
-
- final int second = ((ms >> 3) * 67109) >> 23;
- ms -= 1000 * second;
-
- // Hour
- // 13/128 is nearly the same as /10 for values up to 65
- int temp = (hour * 13) >> 7;
- int p = pos;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (hour - 10 * temp + '0'));
- buffer[p++] = ((char) ':');
-
- // Minute
- // 13/128 is nearly the same as /10 for values up to 65
- temp = (minute * 13) >> 7;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (minute - 10 * temp + '0'));
- buffer[p++] = ((char) ':');
-
- // Second
- // 13/128 is nearly the same as /10 for values up to 65
- temp = (second * 13) >> 7;
- buffer[p++] = ((char) (temp + '0'));
- buffer[p++] = ((char) (second - 10 * temp + '0'));
- buffer[p++] = ((char) '.');
-
- // Millisecond
- // 41/4096 is nearly the same as /100
- temp = (ms * 41) >> 12;
- buffer[p++] = ((char) (temp + '0'));
-
- ms -= 100 * temp;
- temp = (ms * 205) >> 11; // 205/2048 is nearly the same as /10
- buffer[p++] = ((char) (temp + '0'));
-
- ms -= 10 * temp;
- buffer[p++] = ((char) (ms + '0'));
- return p;
- }
-
- StringBuilder formatStringBuilder(final long time, final StringBuilder buffer) {
- // Calculate values by getting the ms values first and do then
- // calculate the hour minute and second values divisions.
-
- // Get daytime in ms which does fit into an int
- // int ms = (int) (time % 86400000);
- int ms = (int) (millisSinceMidnight(time));
-
- final int hours = ms / 3600000;
- ms -= 3600000 * hours;
-
- final int minutes = ms / 60000;
- ms -= 60000 * minutes;
-
- final int seconds = ms / 1000;
- ms -= 1000 * seconds;
-
- // Hour
- int temp = hours / 10;
- buffer.append((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer.append((char) (hours - 10 * temp + '0'));
- buffer.append((char) ':');
-
- // Minute
- temp = minutes / 10;
- buffer.append((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer.append((char) (minutes - 10 * temp + '0'));
- buffer.append((char) ':');
-
- // Second
- temp = seconds / 10;
- buffer.append((char) (temp + '0'));
- buffer.append((char) (seconds - 10 * temp + '0'));
- buffer.append((char) '.');
-
- // Millisecond
- temp = ms / 100;
- buffer.append((char) (temp + '0'));
-
- ms -= 100 * temp;
- temp = ms / 10;
- buffer.append((char) (temp + '0'));
-
- ms -= 10 * temp;
- buffer.append((char) (ms + '0'));
- return buffer;
- }
-
- int formatCharArray(final long time, final char[] buffer, final int pos) {
- // Calculate values by getting the ms values first and do then
- // calculate the hour minute and second values divisions.
-
- // Get daytime in ms which does fit into an int
- // int ms = (int) (time % 86400000);
- int ms = (int) (millisSinceMidnight(time));
-
- final int hours = ms / 3600000;
- ms -= 3600000 * hours;
-
- final int minutes = ms / 60000;
- ms -= 60000 * minutes;
-
- final int seconds = ms / 1000;
- ms -= 1000 * seconds;
-
- // Hour
- int temp = hours / 10;
- int p = pos;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (hours - 10 * temp + '0'));
- buffer[p++] = ((char) ':');
-
- // Minute
- temp = minutes / 10;
- buffer[p++] = ((char) (temp + '0'));
-
- // Do subtract to get remainder instead of doing % 10
- buffer[p++] = ((char) (minutes - 10 * temp + '0'));
- buffer[p++] = ((char) ':');
-
- // Second
- temp = seconds / 10;
- buffer[p++] = ((char) (temp + '0'));
- buffer[p++] = ((char) (seconds - 10 * temp + '0'));
- buffer[p++] = ((char) '.');
-
- // Millisecond
- temp = ms / 100;
- buffer[p++] = ((char) (temp + '0'));
-
- ms -= 100 * temp;
- temp = ms / 10;
- buffer[p++] = ((char) (temp + '0'));
-
- ms -= 10 * temp;
- buffer[p++] = ((char) (ms + '0'));
- return p;
- }
-}
From 1f848066b52c6d4e291488244271a06f66e7b677 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Mon, 28 Oct 2024 15:47:23 +0100
Subject: [PATCH 16/21] Export `o.a.l.l.c.util.internal.instant` package
---
.../internal/instant/InstantNumberFormatter.java | 6 ++++++
.../internal/instant/InstantPatternFormatter.java | 6 ++++++
.../core/util/internal/instant/package-info.java | 15 +++++++++++++++
3 files changed, 27 insertions(+)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatter.java
index 75466571560..0ca4a982b31 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatter.java
@@ -27,6 +27,12 @@
*
* 1 Epoch is a fixed instant on {@code 1970-01-01Z}.
*
+ *
Internal usage only!
+ *
+ * This class is intended only for internal Log4j usage.
+ * Log4j users should not use this class!
+ * This class is not subject to any backward compatibility concerns.
+ *
*
* @since 2.25.0
*/
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
index 138eb09a874..7dd57ea0410 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
@@ -28,6 +28,12 @@
/**
* Contract for formatting {@link Instant}s using a date & time formatting pattern.
+ *
Internal usage only!
+ *
+ * This class is intended only for internal Log4j usage.
+ * Log4j users should not use this class!
+ * This class is not subject to any backward compatibility concerns.
+ *
*
* @since 2.25.0
*/
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
index b6fb098c6c5..c349c42add3 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
@@ -14,7 +14,22 @@
* See the license for the specific language governing permissions and
* limitations under the license.
*/
+/**
+ * Utilities for formatting log event {@link org.apache.logging.log4j.core.time.Instant}s.
+ *
Internal usage only!
+ *
+ * This package is intended only for internal Log4j usage.
+ * Log4j users should not use this package!
+ * This package is not subject to any backward compatibility concerns.
+ *
+ *
+ * @since 2.25.0
+ */
+@Export
+@Version("2.25.0")
@NullMarked
package org.apache.logging.log4j.core.util.internal.instant;
import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
From 612adf8ff76c6df50f4e743241163a3b640b5c52 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Tue, 29 Oct 2024 14:05:48 +0100
Subject: [PATCH 17/21] Add `@ExportTo` for the
`o.a.l.l.c.util.internal.instant` package
---
.../logging/log4j/core/util/internal/instant/package-info.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
index c349c42add3..3c87d12589e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
@@ -26,10 +26,12 @@
* @since 2.25.0
*/
@Export
+@ExportTo("org.apache.logging.log4j.layout.template.json")
@Version("2.25.0")
@NullMarked
package org.apache.logging.log4j.core.util.internal.instant;
+import aQute.bnd.annotation.jpms.ExportTo;
import org.jspecify.annotations.NullMarked;
import org.osgi.annotation.bundle.Export;
import org.osgi.annotation.versioning.Version;
From 9c64a39baf68d69c2e7a4287df81d00b1afcbf34 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Tue, 29 Oct 2024 14:05:57 +0100
Subject: [PATCH 18/21] Improve docs
---
src/changelog/.2.x.x/.release-notes.adoc.ftl | 4 ++--
.../modules/ROOT/pages/manual/pattern-layout.adoc | 13 ++++++++-----
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/src/changelog/.2.x.x/.release-notes.adoc.ftl b/src/changelog/.2.x.x/.release-notes.adoc.ftl
index c8d30dd67f6..ce8c2df4033 100644
--- a/src/changelog/.2.x.x/.release-notes.adoc.ftl
+++ b/src/changelog/.2.x.x/.release-notes.adoc.ftl
@@ -41,9 +41,9 @@ The support for the `\{ansi}` option in exception converters is removed too.
[#release-notes-2-25-0-instant-format]
=== Date & time formatting
-Historically, Log4j contains some date & time formatting utilities for performance reasons, in particular, link:javadoc/log4j-core/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.html[`FixedDateFormat`] and link:javadoc/log4j-core/org/apache/logging/log4j/core/util/datetime/FastDateFormat.html[`FastDateFormat`].
+Historically, Log4j contains custom date & time formatting utilities for performance reasons, i.e., link:javadoc/log4j-core/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.html[`FixedDateFormat`] and link:javadoc/log4j-core/org/apache/logging/log4j/core/util/datetime/FastDateFormat.html[`FastDateFormat`].
These have been deprecated for removal in favor of Java's https://docs.oracle.com/javase/{java-target-version}/docs/api/java/time/format/DateTimeFormatter.html[`DateTimeFormatter`].
-After upgrading, if you experience any date & time formatting problems, please {logging-services-url}/support.html#issues[submit an issue ticket] – as a temporary workaround, you can set xref:manual/systemproperties.adoc#log4j2.instant.formatter[the `log4j2.instant.formatter` property] to `legacy` to switch to the old behaviour.
+After upgrading, if you experience any date & time formatting problems (in particular, related with the usage of `n` and `x` directives), please {logging-services-url}/support.html#issues[submit an issue ticket] – as a temporary workaround, you can set xref:manual/systemproperties.adoc#log4j2.instant.formatter[the `log4j2.instant.formatter` property] to `legacy` to switch to the old behaviour.
=== ANSI support on Windows
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index 2d8cb1d2433..b2067b95cba 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -465,14 +465,17 @@ You can also define custom date formats, see following examples:
|2012-11-02T14:34:02.123Z
|===
-`%d\{UNIX}` outputs the UNIX time in seconds.
-`%d\{UNIX_MILLIS}` outputs the UNIX time in milliseconds.
-The `UNIX` time is the difference – in seconds for `UNIX` and in milliseconds for `UNIX_MILLIS` – between the current time and 1970-01-01 00:00:00 (UTC).
-While the time unit is milliseconds, the granularity depends on the platform.
-This is an efficient way to output the event time because only a conversion from `long` to `String` takes place, there is no `Date` formatting involved.
+`%d\{UNIX}` outputs the epoch time in seconds, i.e., the difference in seconds between the current time and 1970-01-01 00:00:00 (UTC).
+`%d\{UNIX_MILLIS}` outputs the epoch time in milliseconds.
+Note that the granularity of the sub-second formatters depends on the platform.
Users may revert to a millisecond-precision clock when running on Java 9 by setting xref:manual/systemproperties.adoc#log4j2.clock[the `log4j2.clock` system property] to `SystemMillisClock`.
+[WARNING]
+====
+Except `UNIX` and `UNIX_MILLIS` named patterns, the rest of the date & time formatters are not garbage-free.
+====
+
[#converter-encode]
==== Encode
From cce4fa99deb6ded0f4daa83152f77b542973eb14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Tue, 29 Oct 2024 14:15:03 +0100
Subject: [PATCH 19/21] Fix `site` failure
---
.../core/util/internal/instant/InstantPatternFormatter.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
index 7dd57ea0410..0ec8598ce9e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
@@ -27,7 +27,7 @@
import org.apache.logging.log4j.util.PropertiesUtil;
/**
- * Contract for formatting {@link Instant}s using a date & time formatting pattern.
+ * Contract for formatting {@link Instant}s using a date and time formatting pattern.
*
Internal usage only!
*
* This class is intended only for internal Log4j usage.
From 33218ce97725159078144b3618b4c7ef4a11c30c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Tue, 29 Oct 2024 20:38:23 +0100
Subject: [PATCH 20/21] Fix date pattern in `log4j-rolling-size-with-time.xml`
---
.../src/test/resources/log4j-rolling-size-with-time.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml b/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml
index 25083065536..c6a411c8732 100644
--- a/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml
+++ b/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml
@@ -23,7 +23,7 @@
+ filePattern="target/rolling-size-test/rollingtest-%d{yyyy-MM-dd'T'HH-mm-ss-SSS}.log">
%m%n
From 02f7bee5782e0904d78970277522cc8d5ba774ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Volkan=20Yaz=C4=B1c=C4=B1?=
Date: Wed, 30 Oct 2024 22:53:53 +0100
Subject: [PATCH 21/21] Document potential optimization directions
---
.../InstantPatternDynamicFormatterTest.java | 67 ++++++++++++-------
.../InstantPatternDynamicFormatter.java | 24 +++++++
2 files changed, 67 insertions(+), 24 deletions(-)
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
index 7a724c7cf7f..ddb8c311039 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
@@ -21,7 +21,6 @@
import static org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.sequencePattern;
import static org.assertj.core.api.Assertions.assertThat;
-import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -38,7 +37,6 @@
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.PatternSequence;
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.StaticPatternSequence;
import org.apache.logging.log4j.util.Constants;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@@ -379,27 +377,48 @@ private static String formatInstant(
return buffer.toString();
}
- @Test
- void f() {
- final String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS";
- final Locale LOCALE = Locale.US;
- final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
- final InstantPatternFormatter formatter = InstantPatternFormatter.newBuilder()
- .setPattern(pattern)
- .setLocale(LOCALE)
- .setTimeZone(TIME_ZONE)
- .setCachingEnabled(false)
- .build();
- final StringBuilder buffer = new StringBuilder();
- final MutableInstant mutableInstant = new MutableInstant();
-
- final Instant instant1 = Instant.now();
- mutableInstant.initFromEpochSecond(instant1.getEpochSecond(), instant1.getNano());
- formatter.formatTo(buffer, mutableInstant);
-
- buffer.setLength(0);
- final Instant instant2 = instant1.plusMillis(1);
- mutableInstant.initFromEpochSecond(instant2.getEpochSecond(), instant2.getNano());
- formatter.formatTo(buffer, mutableInstant);
+ @ParameterizedTest
+ @MethodSource("formatterInputs")
+ void verify_manually_computed_sub_minute_precision_values(
+ final String ignoredPattern,
+ final Locale ignoredLocale,
+ final TimeZone timeZone,
+ final MutableInstant instant) {
+ final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
+ "HH:mm:ss.S-SS-SSS-SSSS-SSSSS-SSSSSS-SSSSSSS-SSSSSSSS-SSSSSSSSS|n")
+ .withZone(timeZone.toZoneId());
+ final String formatterOutput = formatter.format(instant);
+ final int offsetMillis = timeZone.getOffset(instant.getEpochMillisecond());
+ final long adjustedEpochSeconds = (instant.getEpochMillisecond() + offsetMillis) / 1000;
+ // 86400 seconds per day, 3600 seconds per hour
+ final int local_H = (int) ((adjustedEpochSeconds % 86400L) / 3600L);
+ final int local_m = (int) ((adjustedEpochSeconds / 60) % 60);
+ final int local_s = (int) (adjustedEpochSeconds % 60);
+ final int local_S = instant.getNanoOfSecond() / 100000000;
+ final int local_SS = instant.getNanoOfSecond() / 10000000;
+ final int local_SSS = instant.getNanoOfSecond() / 1000000;
+ final int local_SSSS = instant.getNanoOfSecond() / 100000;
+ final int local_SSSSS = instant.getNanoOfSecond() / 10000;
+ final int local_SSSSSS = instant.getNanoOfSecond() / 1000;
+ final int local_SSSSSSS = instant.getNanoOfSecond() / 100;
+ final int local_SSSSSSSS = instant.getNanoOfSecond() / 10;
+ final int local_SSSSSSSSS = instant.getNanoOfSecond();
+ final int local_n = instant.getNanoOfSecond();
+ final String output = String.format(
+ "%02d:%02d:%02d.%d-%d-%d-%d-%d-%d-%d-%d-%d|%d",
+ local_H,
+ local_m,
+ local_s,
+ local_S,
+ local_SS,
+ local_SSS,
+ local_SSSS,
+ local_SSSSS,
+ local_SSSSSS,
+ local_SSSSSSS,
+ local_SSSSSSSS,
+ local_SSSSSSSSS,
+ local_n);
+ assertThat(output).isEqualTo(formatterOutput);
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
index 791bf1e8963..9c93dd34066 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
@@ -47,6 +47,30 @@
*
Precompute and cache the output for parts that are of precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} (i.e., {@code yyyy-MM-dd'T'HH:mm:} and {@code X}) and cache it
*
Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss.SSS})
*
+ *
Implementation note
+ *
+ * Formatting can actually even be made faster and garbage-free by manually formatting sub-minute precision directives as follows:
+ *
+ *
{@code
+ * int offsetMillis = timeZone.getOffset(mutableInstant.getEpochMillisecond());
+ * long adjustedEpochSeconds = (instant.getEpochMillisecond() + offsetMillis) / 1000;
+ * int local_s = (int) (adjustedEpochSeconds % 60);
+ * int local_S = instant.getNanoOfSecond() / 100000000;
+ * int local_SS = instant.getNanoOfSecond() / 10000000;
+ * int local_SSS = instant.getNanoOfSecond() / 1000000;
+ * int local_SSSS = instant.getNanoOfSecond() / 100000;
+ * int local_SSSSS = instant.getNanoOfSecond() / 10000;
+ * int local_SSSSSS = instant.getNanoOfSecond() / 1000;
+ * int local_SSSSSSS = instant.getNanoOfSecond() / 100;
+ * int local_SSSSSSSS = instant.getNanoOfSecond() / 10;
+ * int local_SSSSSSSSS = instant.getNanoOfSecond();
+ * int local_n = instant.getNanoOfSecond();
+ * }
+ *
+ * Though this will require more hardcoded formatting and a change in the sequence merging strategies.
+ * Hence, this optimization is intentionally shelved off due to involved complexity.
+ * See {@code verify_manually_computed_sub_minute_precision_values()} in {@code InstantPatternDynamicFormatterTest} for a demonstration of this optimization.
+ *