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 testCases() { + return Stream.concat( + testCases(1581082727, 982123456, new Object[][] { + {InstantNumberFormatter.EPOCH_SECONDS, "1581082727.982123456"}, + {InstantNumberFormatter.EPOCH_SECONDS_ROUNDED, "1581082727"}, + {InstantNumberFormatter.EPOCH_SECONDS_NANOS, "982123456"}, + {InstantNumberFormatter.EPOCH_MILLIS, "1581082727982.123456"}, + {InstantNumberFormatter.EPOCH_MILLIS_ROUNDED, "1581082727982"}, + {InstantNumberFormatter.EPOCH_MILLIS_NANOS, "123456"}, + {InstantNumberFormatter.EPOCH_NANOS, "1581082727982123456"} + }), + testCases(1591177590, 5000001, new Object[][] { + {InstantNumberFormatter.EPOCH_SECONDS, "1591177590.005000001"}, + {InstantNumberFormatter.EPOCH_SECONDS_ROUNDED, "1591177590"}, + {InstantNumberFormatter.EPOCH_SECONDS_NANOS, "5000001"}, + {InstantNumberFormatter.EPOCH_MILLIS, "1591177590005.000001"}, + {InstantNumberFormatter.EPOCH_MILLIS_ROUNDED, "1591177590005"}, + {InstantNumberFormatter.EPOCH_MILLIS_NANOS, "1"}, + {InstantNumberFormatter.EPOCH_NANOS, "1591177590005000001"} + })); + } + + private static Stream testCases( + long epochSeconds, int epochSecondsNanos, Object[][] formatterAndOutputPairs) { + return Arrays.stream(formatterAndOutputPairs).map(formatterAndOutputPair -> { + final InstantFormatter formatter = (InstantFormatter) formatterAndOutputPair[0]; + final String expectedOutput = (String) formatterAndOutputPair[1]; + final MutableInstant instant = new MutableInstant(); + instant.initFromEpochSecond(epochSeconds, epochSecondsNanos); + return new Object[] {formatter, instant, expectedOutput}; + }); + } +} 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 new file mode 100644 index 00000000000..91ab466e667 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternDynamicFormatterTest.java @@ -0,0 +1,184 @@ +/* + * 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/InstantPatternThreadLocalCachedFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatterTest.java new file mode 100644 index 00000000000..2d76f508123 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InstantPatternThreadLocalCachedFormatterTest.java @@ -0,0 +1,292 @@ +/* + * 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.time.temporal.ChronoUnit; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; +import java.util.function.Function; +import org.apache.logging.log4j.core.time.Instant; +import org.apache.logging.log4j.core.time.MutableInstant; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class InstantPatternThreadLocalCachedFormatterTest { + + private static final Locale LOCALE = Locale.getDefault(); + + private static final TimeZone TIME_ZONE = TimeZone.getDefault(); + + @ParameterizedTest + @MethodSource("getterTestCases") + void getters_should_work( + final Function cachedFormatterSupplier, + final String pattern, + final Locale locale, + final TimeZone timeZone) { + final InstantPatternDynamicFormatter dynamicFormatter = + new InstantPatternDynamicFormatter(pattern, locale, timeZone); + final InstantPatternThreadLocalCachedFormatter cachedFormatter = + cachedFormatterSupplier.apply(dynamicFormatter); + assertThat(cachedFormatter.getPattern()).isEqualTo(pattern); + assertThat(cachedFormatter.getLocale()).isEqualTo(locale); + assertThat(cachedFormatter.getTimeZone()).isEqualTo(timeZone); + } + + static Object[][] getterTestCases() { + + // Choosing two different locale & time zone pairs to ensure having one that doesn't match the system default + final Locale locale1 = Locale.forLanguageTag("nl_NL"); + final Locale locale2 = Locale.forLanguageTag("tr_TR"); + final String[] timeZoneIds = TimeZone.getAvailableIDs(); + final int timeZone1IdIndex = new Random(0).nextInt(timeZoneIds.length); + final int timeZone2IdIndex = (timeZone1IdIndex + 1) % timeZoneIds.length; + final TimeZone timeZone1 = TimeZone.getTimeZone(timeZoneIds[timeZone1IdIndex]); + final TimeZone timeZone2 = TimeZone.getTimeZone(timeZoneIds[timeZone2IdIndex]); + + // Create test cases + return new Object[][] { + // For `ofMilliPrecision()` + { + (Function) + InstantPatternThreadLocalCachedFormatter::ofMilliPrecision, + "HH:mm.SSS", + locale1, + timeZone1 + }, + { + (Function) + InstantPatternThreadLocalCachedFormatter::ofMilliPrecision, + "HH:mm.SSS", + locale2, + timeZone2 + }, + // For `ofSecondPrecision()` + { + (Function) + InstantPatternThreadLocalCachedFormatter::ofSecondPrecision, + "yyyy", + locale1, + timeZone1 + }, + { + (Function) + InstantPatternThreadLocalCachedFormatter::ofSecondPrecision, + "yyyy", + locale2, + timeZone2 + } + }; + } + + @ParameterizedTest + @ValueSource(strings = {"S", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS", "n", "N"}) + void ofMilliPrecision_should_fail_on_inconsistent_precision(final String subMilliPattern) { + final InstantPatternDynamicFormatter dynamicFormatter = + new InstantPatternDynamicFormatter(subMilliPattern, LOCALE, TIME_ZONE); + assertThatThrownBy(() -> InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(dynamicFormatter)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "instant formatter `%s` is of `%s` precision, whereas the requested cache precision is `%s`", + dynamicFormatter, dynamicFormatter.getPrecision(), ChronoUnit.MILLIS); + } + + @ParameterizedTest + @ValueSource(strings = {"SSS", "s", "ss", "m", "mm", "H", "HH"}) + void ofMilliPrecision_should_truncate_precision_to_milli(final String superMilliPattern) { + final InstantPatternDynamicFormatter dynamicFormatter = + new InstantPatternDynamicFormatter(superMilliPattern, LOCALE, TIME_ZONE); + final InstantPatternThreadLocalCachedFormatter cachedFormatter = + InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(dynamicFormatter); + assertThat(cachedFormatter.getPrecision()).isEqualTo(ChronoUnit.MILLIS); + assertThat(cachedFormatter.getPrecision().compareTo(dynamicFormatter.getPrecision())) + .isLessThanOrEqualTo(0); + } + + @ParameterizedTest + @ValueSource( + strings = {"S", "SS", "SSS", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS", "n", "N", "A"}) + void ofSecondPrecision_should_fail_on_inconsistent_precision(final String subSecondPattern) { + final InstantPatternDynamicFormatter dynamicFormatter = + new InstantPatternDynamicFormatter(subSecondPattern, LOCALE, TIME_ZONE); + assertThatThrownBy(() -> InstantPatternThreadLocalCachedFormatter.ofSecondPrecision(dynamicFormatter)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "instant formatter `%s` is of `%s` precision, whereas the requested cache precision is `%s`", + dynamicFormatter, dynamicFormatter.getPrecision(), ChronoUnit.SECONDS); + } + + @ParameterizedTest + @ValueSource(strings = {"s", "ss", "m", "mm", "H", "HH"}) + void ofSecondPrecision_should_truncate_precision_to_second(final String superSecondPattern) { + final InstantPatternDynamicFormatter dynamicFormatter = + new InstantPatternDynamicFormatter(superSecondPattern, LOCALE, TIME_ZONE); + final InstantPatternThreadLocalCachedFormatter cachedFormatter = + InstantPatternThreadLocalCachedFormatter.ofSecondPrecision(dynamicFormatter); + assertThat(cachedFormatter.getPrecision()).isEqualTo(ChronoUnit.SECONDS); + assertThat(cachedFormatter.getPrecision().compareTo(dynamicFormatter.getPrecision())) + .isLessThanOrEqualTo(0); + } + + private static final MutableInstant INSTANT0 = createInstant(0, 0); + + @Test + void ofMilliPrecision_should_cache() { + + // Mock a pattern formatter + final InstantPatternFormatter patternFormatter = mock(InstantPatternFormatter.class); + when(patternFormatter.getPrecision()).thenReturn(ChronoUnit.MILLIS); + + // Configure the pattern formatter for the 1st instant + final Instant instant1 = INSTANT0; + final String output1 = "instant1"; + doAnswer(invocation -> { + final StringBuilder buffer = invocation.getArgument(0); + buffer.append(output1); + return null; + }) + .when(patternFormatter) + .formatTo(any(StringBuilder.class), eq(instant1)); + + // Create a 2nd distinct instant that shares the same milliseconds with the 1st instant. + // That is, the 2nd instant should trigger a cache hit. + final MutableInstant instant2 = offsetInstant(instant1, 0, 1); + assertThat(instant1.getEpochMillisecond()).isEqualTo(instant2.getEpochMillisecond()); + assertThat(instant1).isNotEqualTo(instant2); + + // Configure the pattern for a 3rd distinct instant. + // The 3rd instant should be of different milliseconds with the 1st (and 2nd) instants to trigger a cache miss. + final MutableInstant instant3 = offsetInstant(instant2, 1, 0); + assertThat(instant2.getEpochMillisecond()).isNotEqualTo(instant3.getEpochMillisecond()); + final String output3 = "instant3"; + doAnswer(invocation -> { + final StringBuilder buffer = invocation.getArgument(0); + buffer.append(output3); + return null; + }) + .when(patternFormatter) + .formatTo(any(StringBuilder.class), eq(instant3)); + + // Create a 4th distinct instant that shares the same milliseconds with the 3rd instant. + // That is, the 4th instant should trigger a cache hit. + final MutableInstant instant4 = offsetInstant(instant3, 0, 1); + assertThat(instant3.getEpochMillisecond()).isEqualTo(instant4.getEpochMillisecond()); + assertThat(instant3).isNotEqualTo(instant4); + + // Create the cached formatter and verify its output + final InstantFormatter cachedFormatter = + InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(patternFormatter); + assertThat(cachedFormatter.format(instant1)).isEqualTo(output1); // Cache miss + assertThat(cachedFormatter.format(instant2)).isEqualTo(output1); // Cache hit + assertThat(cachedFormatter.format(instant2)).isEqualTo(output1); // Repeated cache hit + assertThat(cachedFormatter.format(instant3)).isEqualTo(output3); // Cache miss + assertThat(cachedFormatter.format(instant4)).isEqualTo(output3); // Cache hit + assertThat(cachedFormatter.format(instant4)).isEqualTo(output3); // Repeated cache hit + + // Verify the pattern formatter interaction + verify(patternFormatter).getPrecision(); + verify(patternFormatter).formatTo(any(StringBuilder.class), eq(instant1)); + verify(patternFormatter).formatTo(any(StringBuilder.class), eq(instant3)); + verifyNoMoreInteractions(patternFormatter); + } + + @Test + void ofSecondPrecision_should_cache() { + + // Mock a pattern formatter + final InstantPatternFormatter patternFormatter = mock(InstantPatternFormatter.class); + when(patternFormatter.getPrecision()).thenReturn(ChronoUnit.SECONDS); + + // Configure the pattern formatter for the 1st instant + final Instant instant1 = INSTANT0; + final String output1 = "instant1"; + doAnswer(invocation -> { + final StringBuilder buffer = invocation.getArgument(0); + buffer.append(output1); + return null; + }) + .when(patternFormatter) + .formatTo(any(StringBuilder.class), eq(instant1)); + + // Create a 2nd distinct instant that shares the same seconds with the 1st instant. + // That is, the 2nd instant should trigger a cache hit. + final MutableInstant instant2 = offsetInstant(instant1, 1, 0); + assertThat(instant1.getEpochSecond()).isEqualTo(instant2.getEpochSecond()); + assertThat(instant1).isNotEqualTo(instant2); + + // Configure the pattern for a 3rd distinct instant. + // The 3rd instant should be of different seconds with the 1st (and 2nd) instants to trigger a cache miss. + final MutableInstant instant3 = offsetInstant(instant2, 1_000, 0); + assertThat(instant2.getEpochSecond()).isNotEqualTo(instant3.getEpochSecond()); + final String output3 = "instant3"; + doAnswer(invocation -> { + final StringBuilder buffer = invocation.getArgument(0); + buffer.append(output3); + return null; + }) + .when(patternFormatter) + .formatTo(any(StringBuilder.class), eq(instant3)); + + // Create a 4th distinct instant that shares the same seconds with the 3rd instant. + // That is, the 4th instant should trigger a cache hit. + final MutableInstant instant4 = offsetInstant(instant3, 1, 0); + assertThat(instant3.getEpochSecond()).isEqualTo(instant4.getEpochSecond()); + assertThat(instant3).isNotEqualTo(instant4); + + // Create the cached formatter and verify its output + final InstantFormatter cachedFormatter = + InstantPatternThreadLocalCachedFormatter.ofSecondPrecision(patternFormatter); + assertThat(cachedFormatter.format(instant1)).isEqualTo(output1); // Cache miss + assertThat(cachedFormatter.format(instant2)).isEqualTo(output1); // Cache hit + assertThat(cachedFormatter.format(instant2)).isEqualTo(output1); // Repeated cache hit + assertThat(cachedFormatter.format(instant3)).isEqualTo(output3); // Cache miss + assertThat(cachedFormatter.format(instant4)).isEqualTo(output3); // Cache hit + assertThat(cachedFormatter.format(instant4)).isEqualTo(output3); // Repeated cache hit + + // Verify the pattern formatter interaction + verify(patternFormatter).getPrecision(); + verify(patternFormatter).formatTo(any(StringBuilder.class), eq(instant1)); + verify(patternFormatter).formatTo(any(StringBuilder.class), eq(instant3)); + verifyNoMoreInteractions(patternFormatter); + } + + private static MutableInstant offsetInstant( + final Instant instant, final long epochMillisOffset, final int epochMillisNanosOffset) { + final long epochMillis = Math.addExact(instant.getEpochMillisecond(), epochMillisOffset); + final int epochMillisNanos = Math.addExact(instant.getNanoOfMillisecond(), epochMillisNanosOffset); + return createInstant(epochMillis, epochMillisNanos); + } + + private static MutableInstant createInstant(final long epochMillis, final int epochMillisNanos) { + final MutableInstant instant = new MutableInstant(); + instant.initFromEpochMilli(epochMillis, epochMillisNanos); + return instant; + } +} 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 05157f812a3..b85dc5dc799 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 @@ -16,21 +16,23 @@ */ package org.apache.logging.log4j.core.pattern; +import static java.util.Objects.requireNonNull; + import java.util.Arrays; import java.util.Date; import java.util.Locale; -import java.util.Objects; import java.util.TimeZone; -import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; 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.Constants; -import org.apache.logging.log4j.core.util.datetime.FastDateFormat; -import org.apache.logging.log4j.core.util.datetime.FixedDateFormat; -import org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedFormat; +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.util.PerformanceSensitive; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Converts and formats the event's date in a StringBuilder. @@ -38,16 +40,22 @@ @Plugin(name = "DatePatternConverter", category = PatternConverter.CATEGORY) @ConverterKeys({"d", "date"}) @PerformanceSensitive("allocation") +@NullMarked public final class DatePatternConverter extends LogEventPatternConverter implements ArrayPatternConverter { - private abstract static class Formatter { - long previousTime; // for ThreadLocal caching mode - int nanos; + private static final String CLASS_NAME = DatePatternConverter.class.getSimpleName(); + + private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS"; + + private abstract static class Formatter { - abstract String format(final Instant instant); + final F delegate; - abstract void formatToBuffer(final Instant instant, StringBuilder destination); + private Formatter(final F delegate) { + this.delegate = delegate; + } + @Nullable public String toPattern() { return null; } @@ -57,159 +65,91 @@ public TimeZone getTimeZone() { } } - private static final class PatternFormatter extends Formatter { - private final FastDateFormat fastDateFormat; - - // this field is only used in ThreadLocal caching mode - private final StringBuilder cachedBuffer = new StringBuilder(64); - - PatternFormatter(final FastDateFormat fastDateFormat) { - this.fastDateFormat = fastDateFormat; - } - - @Override - String format(final Instant instant) { - return fastDateFormat.format(instant.getEpochMillisecond()); - } + private static final class PatternFormatter extends Formatter { - @Override - void formatToBuffer(final Instant instant, final StringBuilder destination) { - final long timeMillis = instant.getEpochMillisecond(); - if (previousTime != timeMillis) { - cachedBuffer.setLength(0); - fastDateFormat.format(timeMillis, cachedBuffer); - } - destination.append(cachedBuffer); + private PatternFormatter(final InstantPatternFormatter delegate) { + super(delegate); } @Override public String toPattern() { - return fastDateFormat.getPattern(); + return delegate.getPattern(); } @Override public TimeZone getTimeZone() { - return fastDateFormat.getTimeZone(); + return delegate.getTimeZone(); } } - private static final class FixedFormatter extends Formatter { - private final FixedDateFormat fixedDateFormat; - - // below fields are only used in ThreadLocal caching mode - private final char[] cachedBuffer = new char[70]; // max length of formatted date-time in any format < 70 - private int length = 0; - - FixedFormatter(final FixedDateFormat fixedDateFormat) { - this.fixedDateFormat = fixedDateFormat; - } - - @Override - String format(final Instant instant) { - return fixedDateFormat.formatInstant(instant); - } - - @Override - void formatToBuffer(final Instant instant, final StringBuilder destination) { - final long epochSecond = instant.getEpochSecond(); - final int nanoOfSecond = instant.getNanoOfSecond(); - if (!fixedDateFormat.isEquivalent(previousTime, nanos, epochSecond, nanoOfSecond)) { - length = fixedDateFormat.formatInstant(instant, cachedBuffer, 0); - previousTime = epochSecond; - nanos = nanoOfSecond; - } - destination.append(cachedBuffer, 0, length); - } - - @Override - public String toPattern() { - return fixedDateFormat.getFormat(); - } + private static final class NumberFormatter extends Formatter { - @Override - public TimeZone getTimeZone() { - return fixedDateFormat.getTimeZone(); + private NumberFormatter(final InstantNumberFormatter delegate) { + super(delegate); } } - private static final class UnixFormatter extends Formatter { + private final Formatter formatter; - @Override - String format(final Instant instant) { - return Long.toString(instant.getEpochSecond()); - } + private DatePatternConverter(@Nullable final String[] options) { + super("Date", "date"); + this.formatter = createFormatter(options); + } - @Override - void formatToBuffer(final Instant instant, final StringBuilder destination) { - destination.append(instant.getEpochSecond()); // no need for caching + 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); + } } + final InstantPatternFormatter delegateFormatter = + InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build(); + return new PatternFormatter(delegateFormatter); } - private static final class UnixMillisFormatter extends Formatter { + private static Formatter createFormatterUnsafely(@Nullable final String[] options) { - @Override - String format(final Instant instant) { - return Long.toString(instant.getEpochMillisecond()); - } + // Read options + final String pattern = readPattern(options); + final TimeZone timeZone = readTimeZone(options); + final Locale locale = readLocale(options); - @Override - void formatToBuffer(final Instant instant, final StringBuilder destination) { - destination.append(instant.getEpochMillisecond()); // no need for caching + // Is it epoch seconds? + if ("UNIX".equals(pattern)) { + return new NumberFormatter(InstantNumberFormatter.EPOCH_SECONDS_ROUNDED); } - } - - private final class CachedTime { - public long epochSecond; - public int nanoOfSecond; - public String formatted; - public CachedTime(final Instant instant) { - this.epochSecond = instant.getEpochSecond(); - this.nanoOfSecond = instant.getNanoOfSecond(); - this.formatted = formatter.format(instant); + // Is it epoch milliseconds? + if ("UNIX_MILLIS".equals(pattern)) { + return new NumberFormatter(InstantNumberFormatter.EPOCH_MILLIS_ROUNDED); } - } - - /** - * UNIX formatter in seconds (standard). - */ - private static final String UNIX_FORMAT = "UNIX"; - - /** - * UNIX formatter in milliseconds - */ - private static final String UNIX_MILLIS_FORMAT = "UNIX_MILLIS"; - private final String[] options; - private final ThreadLocal threadLocalMutableInstant = new ThreadLocal<>(); - private final ThreadLocal threadLocalFormatter = new ThreadLocal<>(); - private final AtomicReference cachedTime; - private final Formatter formatter; + final InstantPatternFormatter delegateFormatter = InstantPatternFormatter.newBuilder() + .setPattern(pattern) + .setTimeZone(timeZone) + .setLocale(locale) + .build(); + return new PatternFormatter(delegateFormatter); + } - /** - * Private constructor. - * - * @param options options, may be null. - */ - private DatePatternConverter(final String[] options) { - super("Date", "date"); - this.options = options == null ? null : Arrays.copyOf(options, options.length); - this.formatter = createFormatter(options); - cachedTime = new AtomicReference<>(fromEpochMillis(System.currentTimeMillis())); + private static String readPattern(@Nullable final String[] options) { + return options != null && options.length > 0 ? options[0] : DEFAULT_PATTERN; } - private CachedTime fromEpochMillis(final long epochMillis) { - final MutableInstant temp = new MutableInstant(); - temp.initFromEpochMilli(epochMillis, 0); - return new CachedTime(temp); + private static TimeZone readTimeZone(@Nullable final String[] options) { + return options != null && options.length > 1 ? TimeZone.getTimeZone(options[1]) : TimeZone.getDefault(); } - private Formatter createFormatter(final String[] options) { - final FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(options); - if (fixedDateFormat != null) { - return createFixedFormatter(fixedDateFormat); - } - return createNonFixedFormatter(options); + private static Locale readLocale(@Nullable final String[] options) { + return options != null && options.length > 2 ? Locale.forLanguageTag(options[2]) : Locale.getDefault(); } /** @@ -222,153 +162,92 @@ public static DatePatternConverter newInstance(final String[] options) { return new DatePatternConverter(options); } - private static Formatter createFixedFormatter(final FixedDateFormat fixedDateFormat) { - return new FixedFormatter(fixedDateFormat); - } - - private static Formatter createNonFixedFormatter(final String[] options) { - // if we get here, options is a non-null array with at least one element (first of which non-null) - Objects.requireNonNull(options); - if (options.length == 0) { - throw new IllegalArgumentException("Options array must have at least one element"); - } - Objects.requireNonNull(options[0]); - final String patternOption = options[0]; - if (UNIX_FORMAT.equals(patternOption)) { - return new UnixFormatter(); - } - if (UNIX_MILLIS_FORMAT.equals(patternOption)) { - return new UnixMillisFormatter(); - } - // LOG4J2-1149: patternOption may be a name (if a time zone was specified) - final FixedDateFormat.FixedFormat fixedFormat = FixedDateFormat.FixedFormat.lookup(patternOption); - final String pattern = fixedFormat == null ? patternOption : fixedFormat.getPattern(); - - // if the option list contains a TZ option, then set it. - TimeZone tz = null; - if (options.length > 1 && options[1] != null) { - tz = TimeZone.getTimeZone(options[1]); - } - - Locale locale = null; - if (options.length > 2 && options[2] != null) { - locale = Locale.forLanguageTag(options[2]); - } - - try { - final FastDateFormat tempFormat = FastDateFormat.getInstance(pattern, tz, locale); - return new PatternFormatter(tempFormat); - } catch (final IllegalArgumentException e) { - LOGGER.warn("Could not instantiate FastDateFormat with pattern " + pattern, e); - - // default to the DEFAULT format - return createFixedFormatter(FixedDateFormat.create(FixedFormat.DEFAULT, tz)); - } - } - /** - * Appends formatted date to string buffer. + * Formats the given date to the provided buffer. * - * @param date date - * @param toAppendTo buffer to which formatted date is appended. + * @param date a date + * @param buffer a buffer to append to + * @deprecated Starting with version {@code 2.25.0}, this method is deprecated and planned to be removed in the next major release. */ - public void format(final Date date, final StringBuilder toAppendTo) { - format(date.getTime(), toAppendTo); + @Deprecated + public void format(final Date date, final StringBuilder buffer) { + format(date.getTime(), buffer); } - /** - * {@inheritDoc} - */ @Override public void format(final LogEvent event, final StringBuilder output) { format(event.getInstant(), output); } - public void format(final long epochMilli, final StringBuilder output) { - final MutableInstant instant = getMutableInstant(); - instant.initFromEpochMilli(epochMilli, 0); - format(instant, output); - } - - private MutableInstant getMutableInstant() { - if (Constants.ENABLE_THREADLOCALS) { - MutableInstant result = threadLocalMutableInstant.get(); - if (result == null) { - result = new MutableInstant(); - threadLocalMutableInstant.set(result); - } - return result; - } - return new MutableInstant(); - } - - public void format(final Instant instant, final StringBuilder output) { - if (Constants.ENABLE_THREADLOCALS) { - formatWithoutAllocation(instant, output); - } else { - formatWithoutThreadLocals(instant, output); - } - } - - private void formatWithoutAllocation(final Instant instant, final StringBuilder output) { - getThreadLocalFormatter().formatToBuffer(instant, output); - } - - private Formatter getThreadLocalFormatter() { - Formatter result = threadLocalFormatter.get(); - if (result == null) { - result = createFormatter(options); - threadLocalFormatter.set(result); - } - return result; - } - - private void formatWithoutThreadLocals(final Instant instant, final StringBuilder output) { - final CachedTime effective; - final CachedTime cached = cachedTime.get(); - if (instant.getEpochSecond() != cached.epochSecond || instant.getNanoOfSecond() != cached.nanoOfSecond) { - effective = new CachedTime(instant); - cachedTime.compareAndSet(cached, effective); - } else { - effective = cached; - } - output.append(effective.formatted); + /** + * Formats the given epoch milliseconds to the provided buffer. + * + * @param epochMillis epoch milliseconds + * @param buffer a buffer to append to + * @deprecated Starting with version {@code 2.25.0}, this method is deprecated and planned to be removed in the next major release. + */ + @Deprecated + public void format(final long epochMillis, final StringBuilder buffer) { + final MutableInstant instant = new MutableInstant(); + instant.initFromEpochMilli(epochMillis, 0); + format(instant, buffer); } /** - * {@inheritDoc} + * Formats the given instant to the provided buffer. + * + * @param instant an instant + * @param buffer a buffer to append to + * @deprecated Starting with version {@code 2.25.0}, this method is deprecated and planned to be removed in the next major release. */ + @Deprecated + public void format(final Instant instant, final StringBuilder buffer) { + formatter.delegate.formatTo(buffer, instant); + } + @Override - public void format(final Object obj, final StringBuilder output) { - if (obj instanceof Date) { - format((Date) obj, output); - } - super.format(obj, output); + public void format(@Nullable final Object object, final StringBuilder buffer) { + requireNonNull(buffer, "buffer"); + if (object == null) { + return; + } + if (object instanceof LogEvent) { + format((LogEvent) object, buffer); + } else if (object instanceof Date) { + format((Date) object, buffer); + } else if (object instanceof Instant) { + format((Instant) object, buffer); + } else if (object instanceof Long) { + format((long) object, buffer); + } + LOGGER.warn( + "[{}]: unsupported object type `{}`", + CLASS_NAME, + object.getClass().getCanonicalName()); } @Override - public void format(final StringBuilder toAppendTo, final Object... objects) { - for (final Object obj : objects) { - if (obj instanceof Date) { - format(obj, toAppendTo); - break; + public void format(final StringBuilder buffer, @Nullable final Object... objects) { + requireNonNull(buffer, "buffer"); + if (objects != null) { + for (final Object object : objects) { + if (object instanceof Date) { + format((Date) object, buffer); + break; + } } } } /** - * Gets the pattern string describing this date format. - * - * @return the pattern string describing this date format or {@code null} if the format does not have a pattern. + * @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(); } /** - * Gets the timezone used by this date format. - * - * @return the timezone used by this date format. + * @return the time zone used by this date format */ public TimeZone getTimeZone() { return formatter.getTimeZone(); 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 ef4052fc275..4016d4f2b22 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,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 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.17 1.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 @@ *

* *
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%HOME%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiMessagesMain log4j-core/target/test-classes/log4j2-console.xml
+ * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiMessagesMain log4j-core/target/test-classes/log4j2-console.xml
  * 
*/ public class ConsoleAppenderAnsiMessagesMain { @@ -38,8 +38,7 @@ public class ConsoleAppenderAnsiMessagesMain { private static final Logger LOG = LogManager.getLogger(ConsoleAppenderAnsiMessagesMain.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.xml")) { LOG.fatal("\u001b[1;35mFatal message.\u001b[0m"); LOG.error("\u001b[1;31mError message.\u001b[0m"); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira180Main.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira180Main.java index dffc5b42893..bd8491c6667 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira180Main.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira180Main.java @@ -23,13 +23,13 @@ import org.apache.logging.log4j.core.config.Configurator; /** - * Tests https://issues.apache.org/jira/browse/LOG4J2-180 + * Tests LOG4J2-180 *

* Running from a Windows command line from the root of the project: *

* *
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%HOME%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiStyleJira180Main log4j-core/target/test-classes/log4j2-180.xml
+ * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiStyleJira180Main log4j-core/target/test-classes/log4j2-180.xml
  * 
*/ public class ConsoleAppenderAnsiStyleJira180Main { @@ -37,10 +37,8 @@ public class ConsoleAppenderAnsiStyleJira180Main { private static final Logger LOG = LogManager.getLogger(ConsoleAppenderAnsiStyleJira180Main.class); public static void main(final String[] args) { - System.setProperty("log4j.skipJansi", "false"); // LOG4J2-2087: explicitly enable - // System.out.println(System.getProperty("java.class.path")); final String config = args.length == 0 ? "target/test-classes/log4j2-180.xml" : args[0]; - try (final LoggerContext ctx = + try (final LoggerContext ignored = Configurator.initialize(ConsoleAppenderAnsiMessagesMain.class.getName(), config)) { LOG.fatal("Fatal message."); LOG.error("Error message."); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira272Main.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira272Main.java index 0f2844abe77..65a4ed1ee79 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira272Main.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira272Main.java @@ -23,12 +23,12 @@ import org.apache.logging.log4j.core.config.Configurator; /** - * Tests https://issues.apache.org/jira/browse/LOG4J2-272 + * Tests LOG4J2-272 *

* Running from a Windows command line from the root of the project: *

*
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%HOME%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiStyleJira272Main log4j-core/target/test-classes/log4j2-272.xml
+ * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiStyleJira272Main log4j-core/target/test-classes/log4j2-272.xml
  * 
*/ public class ConsoleAppenderAnsiStyleJira272Main { @@ -36,10 +36,8 @@ public class ConsoleAppenderAnsiStyleJira272Main { private static final Logger LOG = LogManager.getLogger(ConsoleAppenderAnsiStyleJira272Main.class); public static void main(final String[] args) { - System.setProperty("log4j.skipJansi", "false"); // LOG4J2-2087: explicitly enable - // System.out.println(System.getProperty("java.class.path")); final String config = args.length == 0 ? "target/test-classes/log4j2-272.xml" : args[0]; - try (final LoggerContext ctx = + try (final LoggerContext ignored = Configurator.initialize(ConsoleAppenderAnsiMessagesMain.class.getName(), config)) { LOG.fatal("Fatal message."); LOG.error("Error message."); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira319Main.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira319Main.java index 48681a85f71..957a58e9bf6 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira319Main.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleJira319Main.java @@ -23,13 +23,13 @@ import org.apache.logging.log4j.core.config.Configurator; /** - * Tests https://issues.apache.org/jira/browse/LOG4J2-319 + * Tests LOG4J2-319 *

* Running from a Windows command line from the root of the project: *

* *
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%HOME%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiStyleJira319Main log4j-core/target/test-classes/log4j2-319.xml
+ * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiStyleJira319Main log4j-core/target/test-classes/log4j2-319.xml
  * 
*/ public class ConsoleAppenderAnsiStyleJira319Main { @@ -37,10 +37,8 @@ public class ConsoleAppenderAnsiStyleJira319Main { private static final Logger LOG = LogManager.getLogger(ConsoleAppenderAnsiStyleJira319Main.class); public static void main(final String[] args) { - System.setProperty("log4j.skipJansi", "false"); // LOG4J2-2087: explicitly enable - // System.out.println(System.getProperty("java.class.path")); final String config = args.length == 0 ? "target/test-classes/log4j2-319.xml" : args[0]; - try (final LoggerContext ctx = + try (final LoggerContext ignored = Configurator.initialize(ConsoleAppenderAnsiMessagesMain.class.getName(), config)) { LOG.fatal("Fatal message."); LOG.error("Error message."); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleLayoutMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleLayoutMain.java index 87730d9a5a4..2ef1c52c9fa 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleLayoutMain.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleLayoutMain.java @@ -35,7 +35,7 @@ * * or: *
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%HOME%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiStyleLayoutMain log4j-core/target/test-classes/log4j2-console-style-ansi.xml
+ * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiStyleLayoutMain log4j-core/target/test-classes/log4j2-console-style-ansi.xml
  * 
* */ @@ -54,11 +54,9 @@ public void test() { } public void test(final String[] args) { - System.setProperty("log4j.skipJansi", "false"); // LOG4J2-2087: explicitly enable - // System.out.println(System.getProperty("java.class.path")); final String config = args == null || args.length == 0 ? "target/test-classes/log4j2-console-style-ansi.xml" : args[0]; - try (final LoggerContext ctx = + try (final LoggerContext ignored = Configurator.initialize(ConsoleAppenderAnsiMessagesMain.class.getName(), config)) { final Logger logger = LogManager.getLogger(ConsoleAppenderAnsiStyleLayoutMain.class); logger.fatal("Fatal message."); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleNameLayoutMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleNameLayoutMain.java index 9088b75d47a..5cdc81ee452 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleNameLayoutMain.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiStyleNameLayoutMain.java @@ -31,8 +31,7 @@ public class ConsoleAppenderAnsiStyleNameLayoutMain { private static final Logger LOG = LogManager.getLogger(ConsoleAppenderAnsiStyleNameLayoutMain.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-style-name-ansi.xml")) { LOG.fatal("Fatal message."); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJAnsiXExceptionMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiXExceptionMain.java similarity index 78% rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJAnsiXExceptionMain.java rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiXExceptionMain.java index d681e02c754..ee062add98e 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJAnsiXExceptionMain.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderAnsiXExceptionMain.java @@ -22,9 +22,7 @@ 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; /** * Shows how to use ANSI escape codes to color messages. Each message is printed to the console in color, but the rest @@ -34,21 +32,20 @@ *

* *
- * mvn -Dtest=org.apache.logging.log4j.core.appender.ConsoleAppenderJAnsiXExceptionMain test
+ * mvn -Dtest=org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiXExceptionMain test
  * 
* * or, on Windows: * *
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%USERPROFILE%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderJAnsiXExceptionMain log4j-core/src/test/resources/log4j2-console-xex-ansi.xml
+ * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes org.apache.logging.log4j.core.appender.ConsoleAppenderAnsiXExceptionMain log4j-core/src/test/resources/log4j2-console-xex-ansi.xml
  * 
* */ -@Category(Layouts.Jansi.class) -public class ConsoleAppenderJAnsiXExceptionMain { +public class ConsoleAppenderAnsiXExceptionMain { public static void main(final String[] args) { - new ConsoleAppenderJAnsiXExceptionMain().test(args); + new ConsoleAppenderAnsiXExceptionMain().test(args); } /** @@ -60,12 +57,10 @@ public void test() { } public void test(final String[] args) { - System.setProperty("log4j.skipJansi", "false"); // LOG4J2-2087: explicitly enable - // System.out.println(System.getProperty("java.class.path")); final String config = args == null || args.length == 0 ? "target/test-classes/log4j2-console-xex-ansi.xml" : args[0]; final LoggerContext ctx = Configurator.initialize(ConsoleAppenderAnsiMessagesMain.class.getName(), config); - final Logger logger = LogManager.getLogger(ConsoleAppenderJAnsiXExceptionMain.class); + final Logger logger = LogManager.getLogger(ConsoleAppenderAnsiXExceptionMain.class); try { Files.getFileStore(Paths.get("?BOGUS?")); } catch (final Exception e) { diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderDefaultSuppressedThrowable.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderDefaultSuppressedThrowable.java index 8553eb3f1d0..2432df32c93 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderDefaultSuppressedThrowable.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderDefaultSuppressedThrowable.java @@ -31,7 +31,7 @@ *

* *
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%HOME%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderNoAnsiStyleLayoutMain log4j-core/target/test-classes/log4j2-console-style-ansi.xml
+ * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes org.apache.logging.log4j.core.appender.ConsoleAppenderNoAnsiStyleLayoutMain log4j-core/target/test-classes/log4j2-console-style-ansi.xml
  * 
*/ 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
- * 
- * - * or, on Windows: - * - *
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%USERPROFILE%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderJAnsiMessageMain log4j-core/src/test/resources/log4j2-console-msg-ansi.xml
- * 
- * - */ -@Category(Layouts.Jansi.class) -public class ConsoleAppenderJAnsiMessageMain { - - public static void main(final String[] args) { - new ConsoleAppenderJAnsiMessageMain().test(args); - } - - /** - * This is a @Test method to make it easy to run from a command line with {@code mvn -Dtest=FQCN test} - */ - @Test - @ResourceLock(Resources.SYSTEM_PROPERTIES) - public void test() { - test(null); - } - - public void test(final String[] args) { - System.setProperty("log4j.skipJansi", "false"); // LOG4J2-2087: explicitly enable - // System.out.println(System.getProperty("java.class.path")); - final String config = - args == null || args.length == 0 ? "target/test-classes/log4j2-console-msg-ansi.xml" : args[0]; - try (final LoggerContext ctx = - Configurator.initialize(ConsoleAppenderAnsiMessagesMain.class.getName(), config)) { - final Logger logger = LogManager.getLogger(ConsoleAppenderJAnsiMessageMain.class); - logger.info(ansi().fg(RED).a("Hello").fg(CYAN).a(" World").reset()); - // JAnsi format: - // logger.info("@|red Hello|@ @|cyan World|@"); - for (final Entry entry : System.getProperties().entrySet()) { - logger.info("@|KeyStyle {}|@ = @|ValueStyle {}|@", entry.getKey(), entry.getValue()); - } - } - } -} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJira1002ShortThrowableLayoutMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJira1002ShortThrowableLayoutMain.java index b75e3e03259..b1177acdd25 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJira1002ShortThrowableLayoutMain.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderJira1002ShortThrowableLayoutMain.java @@ -21,7 +21,7 @@ */ public class ConsoleAppenderJira1002ShortThrowableLayoutMain { - public static void main(final String[] args) { - ConsoleAppenderNoAnsiStyleLayoutMain.test(args, "target/test-classes/log4j2-1002.xml"); + public static void main() { + ConsoleAppenderNoAnsiStyleLayoutMain.test("target/test-classes/log4j2-1002.xml"); } } diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderNoAnsiStyleLayoutMain.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderNoAnsiStyleLayoutMain.java index 1c35103862e..1165fc6a609 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderNoAnsiStyleLayoutMain.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/ConsoleAppenderNoAnsiStyleLayoutMain.java @@ -30,7 +30,7 @@ *

* *
- * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes;%HOME%\.m2\repository\org\fusesource\jansi\jansi\1.14\jansi-1.14.jar; org.apache.logging.log4j.core.appender.ConsoleAppenderNoAnsiStyleLayoutMain log4j-core/target/test-classes/log4j2-console-style-ansi.xml
+ * java -classpath log4j-core\target\test-classes;log4j-core\target\classes;log4j-api\target\classes org.apache.logging.log4j.core.appender.ConsoleAppenderNoAnsiStyleLayoutMain log4j-core/target/test-classes/log4j2-console-style-ansi.xml
  * 
*/ public class ConsoleAppenderNoAnsiStyleLayoutMain { @@ -43,12 +43,11 @@ private static void logThrowableFromMethod() { public static void main(final String[] args) { final String config = args.length == 0 ? "target/test-classes/log4j2-console-style-no-ansi.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(ConsoleAppenderNoAnsiStyleLayoutMain.class.getName(), config)) { LOG.fatal("Fatal message."); LOG.error("Error message."); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/JansiConsoleAppenderJira965.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/JansiConsoleAppenderJira965.java deleted file mode 100644 index 8bdd3e13efe..00000000000 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/JansiConsoleAppenderJira965.java +++ /dev/null @@ -1,28 +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 org.slf4j.LoggerFactory; - -public class JansiConsoleAppenderJira965 { - - public static void main(final String[] args) { - System.out.println("Able to print on Windows"); - LoggerFactory.getLogger(JansiConsoleAppenderJira965.class); - System.out.println("Unable to print on Windows"); - } -} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptionsTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptionsTest.java index 4719db85637..10dea802505 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptionsTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptionsTest.java @@ -16,26 +16,27 @@ */ package org.apache.logging.log4j.core.impl; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.apache.logging.log4j.util.Strings.toRootUpperCase; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import org.apache.logging.log4j.core.pattern.AnsiEscape; import org.apache.logging.log4j.core.pattern.JAnsiTextRenderer; import org.apache.logging.log4j.core.pattern.TextRenderer; import org.apache.logging.log4j.util.Strings; -import org.fusesource.jansi.AnsiRenderer.Code; import org.junit.jupiter.api.Test; /** * Unit tests for {@code ThrowableFormatOptions}. */ -public final class ThrowableFormatOptionsTest { +final class ThrowableFormatOptionsTest { /** * Runs a given test comparing against the expected values. @@ -71,7 +72,7 @@ private static ThrowableFormatOptions test( * Test {@code %throwable} with null options. */ @Test - public void testNull() { + void testNull() { test(null, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); } @@ -79,7 +80,7 @@ public void testNull() { * Test {@code %throwable} */ @Test - public void testEmpty() { + void testEmpty() { test(new String[] {}, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); } @@ -87,7 +88,7 @@ public void testEmpty() { * Test {@code %throwable{} } with null option value. */ @Test - public void testOneNullElement() { + void testOneNullElement() { test(new String[] {null}, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); } @@ -95,7 +96,7 @@ public void testOneNullElement() { * Test {@code %throwable{} } */ @Test - public void testOneEmptyElement() { + void testOneEmptyElement() { test(new String[] {""}, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); } @@ -103,7 +104,7 @@ public void testOneEmptyElement() { * Test {@code %throwable{full} } */ @Test - public void testFull() { + void testFull() { test(new String[] {"full"}, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); } @@ -111,7 +112,7 @@ public void testFull() { * Test {@code %throwable{full}{ansi} } */ @Test - public void testFullAnsi() { + void testFullAnsi() { final ThrowableFormatOptions tfo = test(new String[] {"full", "ansi"}, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); testFullAnsiEmptyConfig(tfo); @@ -121,7 +122,7 @@ public void testFullAnsi() { * Test {@code %throwable{full}{ansi()} } */ @Test - public void testFullAnsiEmptyConfig() { + void testFullAnsiEmptyConfig() { final ThrowableFormatOptions tfo = test(new String[] {"full", "ansi()"}, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); testFullAnsiEmptyConfig(tfo); @@ -130,34 +131,34 @@ public void testFullAnsiEmptyConfig() { private void testFullAnsiEmptyConfig(final ThrowableFormatOptions tfo) { final TextRenderer textRenderer = tfo.getTextRenderer(); assertNotNull(textRenderer); - assertTrue(textRenderer instanceof JAnsiTextRenderer); - final JAnsiTextRenderer jansiRenderer = (JAnsiTextRenderer) textRenderer; - final Map styleMap = jansiRenderer.getStyleMap(); + assertInstanceOf(JAnsiTextRenderer.class, textRenderer); + final JAnsiTextRenderer ansiRenderer = (JAnsiTextRenderer) textRenderer; + final Map styleMap = ansiRenderer.getStyleMap(); // We have defaults assertFalse(styleMap.isEmpty()); - assertNotNull(styleMap.get("Name")); + assertNotNull(styleMap.get(toRootUpperCase("Name"))); } /** * Test {@code %throwable{full}{ansi(Warning=red))} } */ @Test - public void testFullAnsiWithCustomStyle() { + void testFullAnsiWithCustomStyle() { final ThrowableFormatOptions tfo = test(new String[] {"full", "ansi(Warning=red)"}, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); final TextRenderer textRenderer = tfo.getTextRenderer(); assertNotNull(textRenderer); - assertTrue(textRenderer instanceof JAnsiTextRenderer); - final JAnsiTextRenderer jansiRenderer = (JAnsiTextRenderer) textRenderer; - final Map styleMap = jansiRenderer.getStyleMap(); - assertArrayEquals(new Code[] {Code.RED}, styleMap.get("Warning")); + assertInstanceOf(JAnsiTextRenderer.class, textRenderer); + final JAnsiTextRenderer ansiRenderer = (JAnsiTextRenderer) textRenderer; + final Map styleMap = ansiRenderer.getStyleMap(); + assertThat(styleMap.get(toRootUpperCase("Warning"))).isEqualTo(AnsiEscape.createSequence("RED")); } /** * Test {@code %throwable{full}{ansi(Warning=red Key=blue Value=cyan))} } */ @Test - public void testFullAnsiWithCustomStyles() { + void testFullAnsiWithCustomStyles() { final ThrowableFormatOptions tfo = test( new String[] {"full", "ansi(Warning=red Key=blue Value=cyan)"}, Integer.MAX_VALUE, @@ -165,19 +166,19 @@ public void testFullAnsiWithCustomStyles() { null); final TextRenderer textRenderer = tfo.getTextRenderer(); assertNotNull(textRenderer); - assertTrue(textRenderer instanceof JAnsiTextRenderer); - final JAnsiTextRenderer jansiRenderer = (JAnsiTextRenderer) textRenderer; - final Map styleMap = jansiRenderer.getStyleMap(); - assertArrayEquals(new Code[] {Code.RED}, styleMap.get("Warning")); - assertArrayEquals(new Code[] {Code.BLUE}, styleMap.get("Key")); - assertArrayEquals(new Code[] {Code.CYAN}, styleMap.get("Value")); + assertInstanceOf(JAnsiTextRenderer.class, textRenderer); + final JAnsiTextRenderer ansiRenderer = (JAnsiTextRenderer) textRenderer; + final Map styleMap = ansiRenderer.getStyleMap(); + assertThat(styleMap.get(toRootUpperCase("Warning"))).isEqualTo(AnsiEscape.createSequence("RED")); + assertThat(styleMap.get(toRootUpperCase("Key"))).isEqualTo(AnsiEscape.createSequence("BLUE")); + assertThat(styleMap.get(toRootUpperCase("Value"))).isEqualTo(AnsiEscape.createSequence("CYAN")); } /** * Test {@code %throwable{full}{ansi(Warning=red Key=blue,bg_red Value=cyan,bg_black,underline)} } */ @Test - public void testFullAnsiWithCustomComplexStyles() { + void testFullAnsiWithCustomComplexStyles() { final ThrowableFormatOptions tfo = test( new String[] {"full", "ansi(Warning=red Key=blue,bg_red Value=cyan,bg_black,underline)"}, Integer.MAX_VALUE, @@ -185,19 +186,20 @@ public void testFullAnsiWithCustomComplexStyles() { null); final TextRenderer textRenderer = tfo.getTextRenderer(); assertNotNull(textRenderer); - assertTrue(textRenderer instanceof JAnsiTextRenderer); - final JAnsiTextRenderer jansiRenderer = (JAnsiTextRenderer) textRenderer; - final Map styleMap = jansiRenderer.getStyleMap(); - assertArrayEquals(new Code[] {Code.RED}, styleMap.get("Warning")); - assertArrayEquals(new Code[] {Code.BLUE, Code.BG_RED}, styleMap.get("Key")); - assertArrayEquals(new Code[] {Code.CYAN, Code.BG_BLACK, Code.UNDERLINE}, styleMap.get("Value")); + assertInstanceOf(JAnsiTextRenderer.class, textRenderer); + final JAnsiTextRenderer ansiRenderer = (JAnsiTextRenderer) textRenderer; + final Map styleMap = ansiRenderer.getStyleMap(); + assertThat(styleMap.get(toRootUpperCase("Warning"))).isEqualTo(AnsiEscape.createSequence("RED")); + assertThat(styleMap.get(toRootUpperCase("Key"))).isEqualTo(AnsiEscape.createSequence("BLUE", "BG_RED")); + assertThat(styleMap.get(toRootUpperCase("Value"))) + .isEqualTo(AnsiEscape.createSequence("CYAN", "BG_BLACK", "UNDERLINE")); } /** * Test {@code %throwable{none} } */ @Test - public void testNone() { + void testNone() { test(new String[] {"none"}, 0, Strings.LINE_SEPARATOR, null); } @@ -205,7 +207,7 @@ public void testNone() { * Test {@code %throwable{short} } */ @Test - public void testShort() { + void testShort() { test(new String[] {"short"}, 2, Strings.LINE_SEPARATOR, null); } @@ -213,7 +215,7 @@ public void testShort() { * Test {@code %throwable{10} } */ @Test - public void testDepth() { + void testDepth() { test(new String[] {"10"}, 10, Strings.LINE_SEPARATOR, null); } @@ -221,7 +223,7 @@ public void testDepth() { * Test {@code %throwable{separator(|)} } */ @Test - public void testSeparator() { + void testSeparator() { test(new String[] {"separator(|)"}, Integer.MAX_VALUE, "|", null); } @@ -229,7 +231,7 @@ public void testSeparator() { * Test {@code %throwable{separator()} } */ @Test - public void testSeparatorAsEmpty() { + void testSeparatorAsEmpty() { test(new String[] {"separator()"}, Integer.MAX_VALUE, Strings.EMPTY, null); } @@ -237,7 +239,7 @@ public void testSeparatorAsEmpty() { * Test {@code %throwable{separator(\n)} } */ @Test - public void testSeparatorAsDefaultLineSeparator() { + void testSeparatorAsDefaultLineSeparator() { test( new String[] {"separator(" + Strings.LINE_SEPARATOR + ')'}, Integer.MAX_VALUE, @@ -249,7 +251,7 @@ public void testSeparatorAsDefaultLineSeparator() { * Test {@code %throwable{separator( | )} } */ @Test - public void testSeparatorAsMultipleCharacters() { + void testSeparatorAsMultipleCharacters() { test(new String[] {"separator( | )"}, Integer.MAX_VALUE, " | ", null); } @@ -257,7 +259,7 @@ public void testSeparatorAsMultipleCharacters() { * Test {@code %throwable{full}{separator(|)} } */ @Test - public void testFullAndSeparator() { + void testFullAndSeparator() { test(new String[] {"full", "separator(|)"}, Integer.MAX_VALUE, "|", null); } @@ -265,7 +267,7 @@ public void testFullAndSeparator() { * Test {@code %throwable{full}{filters(org.junit)}{separator(|)} } */ @Test - public void testFullAndFiltersAndSeparator() { + void testFullAndFiltersAndSeparator() { test( new String[] {"full", "filters(org.junit)", "separator(|)"}, Integer.MAX_VALUE, @@ -277,7 +279,7 @@ public void testFullAndFiltersAndSeparator() { * Test {@code %throwable{none}{separator(|)} } */ @Test - public void testNoneAndSeparator() { + void testNoneAndSeparator() { test(new String[] {"none", "separator(|)"}, 0, "|", null); } @@ -285,7 +287,7 @@ public void testNoneAndSeparator() { * Test {@code %throwable{short}{separator(|)} } */ @Test - public void testShortAndSeparator() { + void testShortAndSeparator() { test(new String[] {"short", "separator(|)"}, 2, "|", null); } @@ -293,7 +295,7 @@ public void testShortAndSeparator() { * Test {@code %throwable{10}{separator(|)} } */ @Test - public void testDepthAndSeparator() { + void testDepthAndSeparator() { test(new String[] {"10", "separator(|)"}, 10, "|", null); } @@ -301,7 +303,7 @@ public void testDepthAndSeparator() { * Test {@code %throwable{filters(packages)} } */ @Test - public void testFilters() { + void testFilters() { test( new String[] {"filters(packages)"}, Integer.MAX_VALUE, @@ -313,7 +315,7 @@ public void testFilters() { * Test {@code %throwable{filters()} } */ @Test - public void testFiltersAsEmpty() { + void testFiltersAsEmpty() { test(new String[] {"filters()"}, Integer.MAX_VALUE, Strings.LINE_SEPARATOR, null); } @@ -321,7 +323,7 @@ public void testFiltersAsEmpty() { * Test {@code %throwable{filters(package1,package2)} } */ @Test - public void testFiltersAsMultiplePackages() { + void testFiltersAsMultiplePackages() { test( new String[] {"filters(package1,package2)"}, Integer.MAX_VALUE, @@ -333,7 +335,7 @@ public void testFiltersAsMultiplePackages() { * Test {@code %throwable{full}{filters(packages)} } */ @Test - public void testFullAndFilters() { + void testFullAndFilters() { test( new String[] {"full", "filters(packages)"}, Integer.MAX_VALUE, @@ -345,7 +347,7 @@ public void testFullAndFilters() { * Test {@code %throwable{none}{filters(packages)} } */ @Test - public void testNoneAndFilters() { + void testNoneAndFilters() { test( new String[] {"none", "filters(packages)"}, 0, @@ -357,7 +359,7 @@ public void testNoneAndFilters() { * Test {@code %throwable{short}{filters(packages)} } */ @Test - public void testShortAndFilters() { + void testShortAndFilters() { test( new String[] {"short", "filters(packages)"}, 2, @@ -369,7 +371,7 @@ public void testShortAndFilters() { * Test {@code %throwable{10}{filters(packages)} } */ @Test - public void testDepthAndFilters() { + void testDepthAndFilters() { test( new String[] {"10", "filters(packages)"}, 10, @@ -381,7 +383,7 @@ public void testDepthAndFilters() { * Test {@code %throwable{full}{separator(|)}{filters(packages)} } */ @Test - public void testFullAndSeparatorAndFilter() { + void testFullAndSeparatorAndFilter() { test( new String[] {"full", "separator(|)", "filters(packages)"}, Integer.MAX_VALUE, @@ -393,7 +395,7 @@ public void testFullAndSeparatorAndFilter() { * Test {@code %throwable{full}{separator(|)}{filters(package1,package2)} } */ @Test - public void testFullAndSeparatorAndFilters() { + void testFullAndSeparatorAndFilters() { test( new String[] {"full", "separator(|)", "filters(package1,package2)"}, Integer.MAX_VALUE, @@ -405,7 +407,7 @@ public void testFullAndSeparatorAndFilters() { * Test {@code %throwable{none}{separator(|)}{filters(packages)} } */ @Test - public void testNoneAndSeparatorAndFilters() { + void testNoneAndSeparatorAndFilters() { test(new String[] {"none", "separator(|)", "filters(packages)"}, 0, "|", Collections.singletonList("packages")); } @@ -413,7 +415,7 @@ public void testNoneAndSeparatorAndFilters() { * Test {@code %throwable{short}{separator(|)}{filters(packages)} } */ @Test - public void testShortAndSeparatorAndFilters() { + void testShortAndSeparatorAndFilters() { test( new String[] {"short", "separator(|)", "filters(packages)"}, 2, @@ -425,7 +427,7 @@ public void testShortAndSeparatorAndFilters() { * Test {@code %throwable{10}{separator(|)}{filters(packages)} } */ @Test - public void testDepthAndSeparatorAndFilters() { + void testDepthAndSeparatorAndFilters() { test(new String[] {"10", "separator(|)", "filters(packages)"}, 10, "|", Collections.singletonList("packages")); } @@ -433,7 +435,7 @@ public void testDepthAndSeparatorAndFilters() { * Test {@code %throwable{full,filters(packages)} } */ @Test - public void testSingleOptionFullAndFilters() { + void testSingleOptionFullAndFilters() { test( new String[] {"full,filters(packages)"}, Integer.MAX_VALUE, @@ -445,7 +447,7 @@ public void testSingleOptionFullAndFilters() { * Test {@code %throwable{none,filters(packages)} } */ @Test - public void testSingleOptionNoneAndFilters() { + void testSingleOptionNoneAndFilters() { test(new String[] {"none,filters(packages)"}, 0, Strings.LINE_SEPARATOR, Collections.singletonList("packages")); } @@ -453,7 +455,7 @@ public void testSingleOptionNoneAndFilters() { * Test {@code %throwable{short,filters(packages)} } */ @Test - public void testSingleOptionShortAndFilters() { + void testSingleOptionShortAndFilters() { test( new String[] {"short,filters(packages)"}, 2, @@ -465,7 +467,7 @@ public void testSingleOptionShortAndFilters() { * Test {@code %throwable{none,filters(packages)} } */ @Test - public void testSingleOptionDepthAndFilters() { + void testSingleOptionDepthAndFilters() { test(new String[] {"10,filters(packages)"}, 10, Strings.LINE_SEPARATOR, Collections.singletonList("packages")); } @@ -473,7 +475,7 @@ public void testSingleOptionDepthAndFilters() { * Test {@code %throwable{full,filters(package1,package2)} } */ @Test - public void testSingleOptionFullAndMultipleFilters() { + void testSingleOptionFullAndMultipleFilters() { test( new String[] {"full,filters(package1,package2)"}, Integer.MAX_VALUE, @@ -485,7 +487,7 @@ public void testSingleOptionFullAndMultipleFilters() { * Test {@code %throwable{none,filters(package1,package2)} } */ @Test - public void testSingleOptionNoneAndMultipleFilters() { + void testSingleOptionNoneAndMultipleFilters() { test( new String[] {"none,filters(package1,package2)"}, 0, @@ -497,7 +499,7 @@ public void testSingleOptionNoneAndMultipleFilters() { * Test {@code %throwable{short,filters(package1,package2)} } */ @Test - public void testSingleOptionShortAndMultipleFilters() { + void testSingleOptionShortAndMultipleFilters() { test( new String[] {"short,filters(package1,package2)"}, 2, @@ -509,7 +511,7 @@ public void testSingleOptionShortAndMultipleFilters() { * Test {@code %throwable{none,filters(package1,package2)} } */ @Test - public void testSingleOptionDepthAndMultipleFilters() { + void testSingleOptionDepthAndMultipleFilters() { test( new String[] {"10,filters(package1,package2)"}, 10, diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/JAnsiTextRendererTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/JAnsiTextRendererTest.java new file mode 100644 index 00000000000..0595f68533d --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/JAnsiTextRendererTest.java @@ -0,0 +1,58 @@ +/* + * 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.pattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.presentation.HexadecimalRepresentation.HEXA_REPRESENTATION; + +import java.util.Collections; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class JAnsiTextRendererTest { + + public static Stream testRendering() { + return Stream.of( + // Use style names + Arguments.of( + "KeyStyle=white ValueStyle=cyan,bold", + "@|KeyStyle key|@ = @|ValueStyle some value|@", + "\u001b[37mkey\u001b[m = \u001b[36;1msome value\u001b[m"), + // Use AnsiEscape codes directly + Arguments.of( + "", + "@|white key|@ = @|cyan,bold some value|@", + "\u001b[37mkey\u001b[m = \u001b[36;1msome value\u001b[m"), + // Return broken escapes as is + Arguments.of("", "Hello @|crazy|@ world!", "Hello @|crazy|@ world!"), + Arguments.of("", "Hello @|world!", "Hello @|world!")); + } + + @ParameterizedTest + @MethodSource + void testRendering(final String format, final String text, final String expected) { + final JAnsiTextRenderer renderer = new JAnsiTextRenderer(new String[] {"ansi", format}, Collections.emptyMap()); + final StringBuilder actual = new StringBuilder(); + renderer.render(new StringBuilder(text), actual); + assertThat(actual.toString()) + .as("Rendering text '%s'", text) + .withRepresentation(HEXA_REPRESENTATION) + .isEqualTo(expected); + } +} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageJansiConverterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageAnsiConverterTest.java similarity index 94% rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageJansiConverterTest.java rename to log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageAnsiConverterTest.java index 294355a173f..51278a04d6e 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageJansiConverterTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageAnsiConverterTest.java @@ -31,7 +31,7 @@ import org.junit.jupiter.api.Test; @LoggerContextSource("log4j-message-ansi.xml") -public class MessageJansiConverterTest { +public class MessageAnsiConverterTest { private static final String EXPECTED = "\u001B[31;1mWarning!\u001B[m Pants on \u001B[31mfire!\u001B[m" + Strings.LINE_SEPARATOR; @@ -47,7 +47,7 @@ public void setUp(final LoggerContext context, @Named("List") final ListAppender @Test public void testReplacement() { - // See org.fusesource.jansi.AnsiRenderer + // See https://www.javadoc.io/doc/org.jline/jline/latest/org/jline/jansi/AnsiRenderer.html logger.error("@|red,bold Warning!|@ Pants on @|red fire!|@"); final List msgs = app.getMessages(); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageStyledConverterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageStyledConverterTest.java index 71fb6fbc77b..06ab681fedd 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageStyledConverterTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/MessageStyledConverterTest.java @@ -47,7 +47,7 @@ public void setUp(final LoggerContext context, @Named("List") final ListAppender @Test public void testReplacement() { - // See org.fusesource.jansi.AnsiRenderer + // See https://www.javadoc.io/doc/org.jline/jline/latest/org/jline/jansi/AnsiRenderer.html logger.error("@|WarningStyle Warning!|@ Pants on @|WarningStyle fire!|@"); final List msgs = app.getMessages(); diff --git a/log4j-core-test/src/test/resources/log4j2-console-msg-ansi.xml b/log4j-core-test/src/test/resources/log4j2-console-msg-ansi.xml deleted file mode 100644 index d30b0fb406b..00000000000 --- a/log4j-core-test/src/test/resources/log4j2-console-msg-ansi.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml index 1e35ebce729..f5693dc2e92 100644 --- a/log4j-core/pom.xml +++ b/log4j-core/pom.xml @@ -66,7 +66,6 @@ org.apache.commons.csv;resolution:=optional, org.apache.kafka.*;resolution:=optional, org.codehaus.stax2;resolution:=optional, - org.fusesource.jansi;resolution:=optional, org.jctools.*;resolution:=optional, org.zeromq;resolution:=optional, javax.lang.model.*;resolution:=optional, @@ -97,7 +96,6 @@ java.management;transitive=false;static=true, java.naming;transitive=false, org.apache.commons.csv;transitive=false, - org.fusesource.jansi;transitive=false, org.jspecify;transitive=false, org.zeromq.jeromq;transitive=false, @@ -194,12 +192,6 @@ jackson-dataformat-yaml true - - - org.fusesource.jansi - jansi - true - org.jctools diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/ConsoleAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/ConsoleAppender.java index 986c82863f3..1b1bf5e2bdb 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/ConsoleAppender.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/ConsoleAppender.java @@ -20,10 +20,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.PrintStream; import java.io.Serializable; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Constructor; import java.nio.charset.Charset; import java.util.concurrent.atomic.AtomicInteger; import org.apache.logging.log4j.core.Appender; @@ -35,12 +32,8 @@ import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required; -import org.apache.logging.log4j.core.layout.PatternLayout; import org.apache.logging.log4j.core.util.Booleans; import org.apache.logging.log4j.core.util.CloseShieldOutputStream; -import org.apache.logging.log4j.core.util.Loader; -import org.apache.logging.log4j.core.util.Throwables; -import org.apache.logging.log4j.util.Chars; import org.apache.logging.log4j.util.PropertiesUtil; /** @@ -61,8 +54,7 @@ public final class ConsoleAppender extends AbstractOutputStreamAppender { public static final String PLUGIN_NAME = "Console"; - private static final String JANSI_CLASS = "org.fusesource.jansi.WindowsAnsiOutputStream"; - private static ConsoleManagerFactory factory = new ConsoleManagerFactory(); + private static final ConsoleManagerFactory factory = new ConsoleManagerFactory(); private static final Target DEFAULT_TARGET = Target.SYSTEM_OUT; private static final AtomicInteger COUNT = new AtomicInteger(); @@ -116,10 +108,10 @@ private ConsoleAppender( * * @param layout The layout to use (required). * @param filter The Filter or null. - * @param targetStr The target ("SYSTEM_OUT" or "SYSTEM_ERR"). The default is "SYSTEM_OUT". + * @param target The target ("SYSTEM_OUT" or "SYSTEM_ERR"). The default is "SYSTEM_OUT". * @param name The name of the Appender (required). * @param follow If true will follow changes to the underlying output stream. - * @param ignore If {@code "true"} (default) exceptions encountered when appending events are logged; otherwise they + * @param ignoreExceptions If {@code "true"} (default) exceptions encountered when appending events are logged; otherwise they * are propagated to the caller. * @return The ConsoleAppender. * @deprecated Deprecated in 2.7; use {@link #newBuilder()}. @@ -128,22 +120,18 @@ private ConsoleAppender( public static ConsoleAppender createAppender( Layout layout, final Filter filter, - final String targetStr, + final String target, final String name, final String follow, - final String ignore) { - if (name == null) { - LOGGER.error("No name provided for ConsoleAppender"); - return null; - } - if (layout == null) { - layout = PatternLayout.createDefaultLayout(); - } - final boolean isFollow = Boolean.parseBoolean(follow); - final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true); - final Target target = targetStr == null ? DEFAULT_TARGET : Target.valueOf(targetStr); - return new ConsoleAppender( - name, layout, filter, getManager(target, isFollow, false, layout), ignoreExceptions, target, null); + final String ignoreExceptions) { + return newBuilder() + .setLayout(layout) + .setFilter(filter) + .setTarget(target == null ? DEFAULT_TARGET : Target.valueOf(target)) + .setName(name) + .setFollow(Boolean.parseBoolean(follow)) + .setIgnoreExceptions(Booleans.parseBoolean(ignoreExceptions, true)) + .build(); } /** @@ -171,21 +159,15 @@ public static ConsoleAppender createAppender( final boolean follow, final boolean direct, final boolean ignoreExceptions) { - // @formatter:on - if (name == null) { - LOGGER.error("No name provided for ConsoleAppender"); - return null; - } - if (layout == null) { - layout = PatternLayout.createDefaultLayout(); - } - target = target == null ? Target.SYSTEM_OUT : target; - if (follow && direct) { - LOGGER.error("Cannot use both follow and direct on ConsoleAppender"); - return null; - } - return new ConsoleAppender( - name, layout, filter, getManager(target, follow, direct, layout), ignoreExceptions, target, null); + return newBuilder() + .setLayout(layout) + .setFilter(filter) + .setTarget(target) + .setName(name) + .setFollow(follow) + .setDirect(direct) + .setIgnoreExceptions(ignoreExceptions) + .build(); } public static ConsoleAppender createDefaultAppenderForLayout(final Layout layout) { @@ -194,7 +176,7 @@ public static ConsoleAppender createDefaultAppenderForLayout(final Layout layout = getOrCreateLayout(target.getDefaultCharset()); + + OutputStream stream = direct + ? getDirectOutputStream(target) + : follow ? getFollowOutputStream(target) : getDefaultOutputStream(target); + + final String managerName = target.name() + '.' + follow + '.' + direct; + final OutputStreamManager manager = + OutputStreamManager.getManager(managerName, new FactoryData(stream, managerName, layout), factory); return new ConsoleAppender( - getName(), - layout, - getFilter(), - getManager(target, follow, direct, layout), - isIgnoreExceptions(), - target, - getPropertyArray()); + getName(), layout, getFilter(), manager, isIgnoreExceptions(), target, getPropertyArray()); } } - private static OutputStreamManager getDefaultManager( - final Target target, - final boolean follow, - final boolean direct, - final Layout layout) { - final OutputStream os = getOutputStream(follow, direct, target); - + private static OutputStreamManager getDefaultManager(final Layout layout) { + final OutputStream os = getDefaultOutputStream(ConsoleAppender.DEFAULT_TARGET); // LOG4J2-1176 DefaultConfiguration should not share OutputStreamManager instances to avoid memory leaks. - final String managerName = target.name() + '.' + follow + '.' + direct + "-" + COUNT.get(); + final String managerName = ConsoleAppender.DEFAULT_TARGET.name() + ".false.false-" + COUNT.get(); return OutputStreamManager.getManager(managerName, new FactoryData(os, managerName, layout), factory); } - private static OutputStreamManager getManager( - final Target target, - final boolean follow, - final boolean direct, - final Layout layout) { - final OutputStream os = getOutputStream(follow, direct, target); - final String managerName = target.name() + '.' + follow + '.' + direct; - return OutputStreamManager.getManager(managerName, new FactoryData(os, managerName, layout), factory); + private static OutputStream getDefaultOutputStream(Target target) { + return new CloseShieldOutputStream(target == Target.SYSTEM_OUT ? System.out : System.err); } - private static OutputStream getOutputStream(final boolean follow, final boolean direct, final Target target) { - final String enc = Charset.defaultCharset().name(); - OutputStream outputStream; - try { - // @formatter:off - outputStream = target == Target.SYSTEM_OUT - ? direct - ? new FileOutputStream(FileDescriptor.out) - : (follow ? new PrintStream(new SystemOutStream(), true, enc) : System.out) - : direct - ? new FileOutputStream(FileDescriptor.err) - : (follow ? new PrintStream(new SystemErrStream(), true, enc) : System.err); - // @formatter:on - outputStream = new CloseShieldOutputStream(outputStream); - } catch (final UnsupportedEncodingException ex) { // should never happen - throw new IllegalStateException("Unsupported default encoding " + enc, ex); - } - final PropertiesUtil propsUtil = PropertiesUtil.getProperties(); - if (!propsUtil.isOsWindows() || propsUtil.getBooleanProperty("log4j.skipJansi", true) || direct) { - return outputStream; - } - try { - // We type the parameter as a wildcard to avoid a hard reference to Jansi. - final Class clazz = Loader.loadClass(JANSI_CLASS); - final Constructor constructor = clazz.getConstructor(OutputStream.class); - return new CloseShieldOutputStream((OutputStream) constructor.newInstance(outputStream)); - } catch (final ClassNotFoundException cnfe) { - LOGGER.debug("Jansi is not installed, cannot find {}", JANSI_CLASS); - } catch (final NoSuchMethodException nsme) { - LOGGER.warn("{} is missing the proper constructor", JANSI_CLASS); - } catch (final Exception ex) { - LOGGER.warn( - "Unable to instantiate {} due to {}", - JANSI_CLASS, - clean(Throwables.getRootCause(ex).toString()).trim()); - } - return outputStream; + private static OutputStream getDirectOutputStream(Target target) { + return new CloseShieldOutputStream( + new FileOutputStream(target == Target.SYSTEM_OUT ? FileDescriptor.out : FileDescriptor.err)); } - private static String clean(final String string) { - return string.replace(Chars.NUL, Chars.SPACE); + private static OutputStream getFollowOutputStream(Target target) { + return target == Target.SYSTEM_OUT ? new SystemOutStream() : new SystemErrStream(); } /** diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptions.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptions.java index 5df74e5d9cd..74bc937ab61 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptions.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptions.java @@ -23,9 +23,7 @@ import org.apache.logging.log4j.core.pattern.PlainTextRenderer; import org.apache.logging.log4j.core.pattern.TextRenderer; import org.apache.logging.log4j.core.util.Integers; -import org.apache.logging.log4j.core.util.Loader; import org.apache.logging.log4j.core.util.Patterns; -import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.Strings; /** @@ -290,17 +288,11 @@ public static ThrowableFormatOptions newInstance(String[] options) { || option.equalsIgnoreCase(LOCALIZED_MESSAGE)) { lines = 2; } else if (option.startsWith("ansi(") && option.endsWith(")") || option.equals("ansi")) { - if (Loader.isJansiAvailable()) { - final String styleMapStr = option.equals("ansi") - ? Strings.EMPTY - : option.substring("ansi(".length(), option.length() - 1); - ansiRenderer = new JAnsiTextRenderer( - new String[] {null, styleMapStr}, JAnsiTextRenderer.DefaultExceptionStyleMap); - } else { - StatusLogger.getLogger() - .warn( - "You requested ANSI exception rendering but JANSI is not on the classpath. Please see https://logging.apache.org/log4j/2.x/runtime-dependencies.html"); - } + final String styleMapStr = option.equals("ansi") + ? Strings.EMPTY + : option.substring("ansi(".length(), option.length() - 1); + ansiRenderer = new JAnsiTextRenderer( + new String[] {null, styleMapStr}, JAnsiTextRenderer.DefaultExceptionStyleMap); } else if (option.startsWith("S(") && option.endsWith(")")) { suffix = option.substring("S(".length(), option.length() - 1); } else if (option.startsWith("suffix(") && option.endsWith(")")) { diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java index 2d4bdcd199c..666e8325ed8 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java @@ -18,7 +18,7 @@ * Log4j 2 private implementation classes. */ @Export -@Version("2.25.0") +@Version("2.24.1") package org.apache.logging.log4j.core.impl; import org.osgi.annotation.bundle.Export; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java index a915cdadfbc..5f6e4299277 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java @@ -38,7 +38,6 @@ import org.apache.logging.log4j.core.pattern.PatternFormatter; import org.apache.logging.log4j.core.pattern.PatternParser; import org.apache.logging.log4j.core.pattern.RegexReplacement; -import org.apache.logging.log4j.util.PropertiesUtil; import org.apache.logging.log4j.util.Strings; /** @@ -652,7 +651,7 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde private boolean alwaysWriteExceptions = true; @PluginBuilderAttribute - private boolean disableAnsi = !useAnsiEscapeCodes(); + private boolean disableAnsi; @PluginBuilderAttribute private boolean noConsoleNoAnsi; @@ -665,13 +664,6 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde private Builder() {} - private boolean useAnsiEscapeCodes() { - final PropertiesUtil propertiesUtil = PropertiesUtil.getProperties(); - final boolean isPlatformSupportsAnsi = !propertiesUtil.isOsWindows(); - final boolean isJansiRequested = !propertiesUtil.getBooleanProperty("log4j.skipJansi", true); - return isPlatformSupportsAnsi || isJansiRequested; - } - /** * @param pattern * The pattern. If not specified, defaults to DEFAULT_CONVERSION_PATTERN. @@ -731,8 +723,7 @@ public Builder withAlwaysWriteExceptions(final boolean alwaysWriteExceptions) { /** * @param disableAnsi - * If {@code "true"} (default is value of system property `log4j.skipJansi`, or `true` if undefined), - * do not output ANSI escape codes + * If {@code true}, do not output ANSI escape codes. */ public Builder withDisableAnsi(final boolean disableAnsi) { this.disableAnsi = disableAnsi; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/AnsiEscape.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/AnsiEscape.java index 74633b10a1d..34213373dfb 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/AnsiEscape.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/AnsiEscape.java @@ -421,6 +421,11 @@ public static Map createMap(final String values, final String[] * @return a new map */ public static Map createMap(final String[] values, final String[] dontEscapeKeys) { + return createMap(values, dontEscapeKeys, "\\s"); + } + + static Map createMap( + final String[] values, final String[] dontEscapeKeys, final String separatorRegex) { final String[] sortedIgnoreKeys = dontEscapeKeys != null ? dontEscapeKeys.clone() : Strings.EMPTY_ARRAY; Arrays.sort(sortedIgnoreKeys); final Map map = new HashMap<>(); @@ -430,7 +435,7 @@ public static Map createMap(final String[] values, final String[ final String key = toRootUpperCase(keyValue[0]); final String value = keyValue[1]; final boolean escape = Arrays.binarySearch(sortedIgnoreKeys, key) < 0; - map.put(key, escape ? createSequence(value.split("\\s")) : value); + map.put(key, escape ? createSequence(value.split(separatorRegex)) : value); } else { LOGGER.warn("Syntax error, missing '=': Expected \"{KEY1=VALUE, KEY2=VALUE, ...}"); } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/JAnsiTextRenderer.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/JAnsiTextRenderer.java index b9529573452..072dfeb87ff 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/JAnsiTextRenderer.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/JAnsiTextRenderer.java @@ -16,28 +16,31 @@ */ package org.apache.logging.log4j.core.pattern; +import static org.apache.logging.log4j.core.pattern.AnsiEscape.BG_RED; +import static org.apache.logging.log4j.core.pattern.AnsiEscape.BOLD; +import static org.apache.logging.log4j.core.pattern.AnsiEscape.RED; +import static org.apache.logging.log4j.core.pattern.AnsiEscape.WHITE; +import static org.apache.logging.log4j.core.pattern.AnsiEscape.YELLOW; import static org.apache.logging.log4j.util.Strings.toRootUpperCase; -import static org.fusesource.jansi.AnsiRenderer.Code.BG_RED; -import static org.fusesource.jansi.AnsiRenderer.Code.BOLD; -import static org.fusesource.jansi.AnsiRenderer.Code.RED; -import static org.fusesource.jansi.AnsiRenderer.Code.WHITE; -import static org.fusesource.jansi.AnsiRenderer.Code.YELLOW; +import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.status.StatusLogger; -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.AnsiRenderer; -import org.fusesource.jansi.AnsiRenderer.Code; /** * Renders an input as ANSI escaped output. - * - * Uses the JAnsi rendering syntax as the default to render a message into an ANSI escaped string. - * + *

+ * Uses the + * JLine AnsiRenderer syntax + * to render a message into an ANSI escaped string. + *

+ *

* 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.2 4.0.1 2.3.3 - 2.4.1 3.3.4 0.22.1 1.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.0 1.11.0 2.18.0 - 1.18 1.6.2 4.0.5 18.3.12 @@ -769,12 +768,6 @@ pom - - org.fusesource.jansi - jansi - ${site-jansi.version} - - com.sun.mail javax.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 + *

+ *
    + *
  1. Sequence the pattern and assign a time precision to each part (e.g., {@code MM} is of month precision)
  2. + *
  3. 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
  4. + *
  5. Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss} and {@code SSS})
  6. + *
+ * + * @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: + *

+ * + *
{@code
+     * [
+     *     static(literal="["),
+     *     dynamic(pattern="MM", precision=MONTHS),
+     *     static(literal="-"),
+     *     dynamic(pattern="dd", precision=DAYS),
+     *     static(literal="]"),
+     *     static(literal=" "),
+     *     dynamic(pattern="HH", precision=HOURS),
+     *     static(literal=":"),
+     *     dynamic(pattern="mm", precision=MINUTES)
+     * ]
+     * }
+ * + *

+ * The above sequencing implies creation of 9 {@link AbstractFormatter}s. + * This method transforms it to the following: + *

+ * + *
{@code
+     * [
+     *     static(literal="["),
+     *     dynamic(pattern="MM", precision=MONTHS),
+     *     static(literal="-"),
+     *     dynamic(pattern="dd", precision=DAYS),
+     *     static(literal="] "),
+     *     dynamic(pattern="HH", precision=HOURS),
+     *     static(literal=":"),
+     *     dynamic(pattern="mm", precision=MINUTES)
+     * ]
+     * }
+ * + *

+ * 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: + *

+ * + *
{@code
+     * [
+     *     dynamic(pattern="yyyy", precision=YEARS),
+     *     static(literal="-"),
+     *     dynamic(pattern="MM", precision=MONTHS),
+     *     static(literal="-"),
+     *     dynamic(pattern="dd", precision=DAYS),
+     *     static(literal="T"),
+     *     dynamic(pattern="HH", precision=HOURS),
+     *     static(literal=":"),
+     *     dynamic(pattern="mm", precision=MINUTES),
+     *     static(literal=":"),
+     *     dynamic(pattern="ss", precision=SECONDS),
+     *     static(literal="."),
+     *     dynamic(pattern="SSS", precision=MILLISECONDS),
+     *     dynamic(pattern="X", precision=HOURS),
+     * ]
+     * }
+ * + *

+ * 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: + *

+ * + *
{@code
+     * [
+     *     dynamic(pattern="yyyy", precision=YEARS),
+     *     static(literal="-"),
+     *     dynamic(pattern="MM", precision=MONTHS),
+     *     static(literal="-"),
+     *     dynamic(pattern="dd", precision=DAYS),
+     *     static(literal="T"),
+     *     dynamic(pattern="HH", precision=HOURS),
+     *     static(literal=":"),
+     *     dynamic(pattern="mm", precision=MINUTES),
+     *     static(literal=":"),
+     *     composite(
+     *         sequences=[
+     *             dynamic(pattern="ss", precision=SECONDS),
+     *             static(literal="."),
+     *             dynamic(pattern="SSS", precision=MILLISECONDS)
+     *         ],
+     *         precision=MILLISECONDS),
+     *     dynamic(pattern="X", precision=HOURS),
+     * ]
+     * }
+ * + *

+ * 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: + *

+ * + *
{@code
+     * [
+     *     dynamic(pattern="yyyy", precision=YEARS),
+     *     static(literal="-"),
+     *     dynamic(pattern="MM", precision=MONTHS),
+     *     static(literal="-"),
+     *     dynamic(pattern="dd", precision=DAYS),
+     *     static(literal="T"),
+     *     dynamic(pattern="HH", precision=HOURS),
+     *     static(literal=":"),
+     *     dynamic(pattern="mm", precision=MINUTES),
+     *     static(literal=":"),
+     *     composite(
+     *         sequences=[
+     *             dynamic(pattern="ss", precision=SECONDS),
+     *             static(literal="."),
+     *             dynamic(pattern="SSS", precision=MILLISECONDS)
+     *         ],
+     *         precision=MILLISECONDS),
+     *     dynamic(pattern="X", precision=HOURS),
+     * ]
+     * }
+ * + *

+ * The above sequencing implies creation of 12 {@link AbstractFormatter}s. + * This method transforms it to the following: + *

+ * + *
{@code
+     * [
+     *     composite(
+     *         sequences=[
+     *             dynamic(pattern="yyyy", precision=YEARS),
+     *             static(literal="-"),
+     *             dynamic(pattern="MM", precision=MONTHS),
+     *             static(literal="-"),
+     *             dynamic(pattern="dd", precision=DAYS),
+     *             static(literal="T"),
+     *             dynamic(pattern="HH", precision=HOURS),
+     *             static(literal=":"),
+     *             dynamic(pattern="mm", precision=MINUTES),
+     *             static(literal=":")
+     *         ],
+     *         precision=MINUTES),
+     *     composite(
+     *         sequences=[
+     *             dynamic(pattern="ss", precision=SECONDS),
+     *             static(literal="."),
+     *             dynamic(pattern="SSS", precision=MILLISECONDS)
+     *         ],
+     *         precision=MILLISECONDS),
+     *     dynamic(pattern="X", precision=HOURS),
+     * ]
+     * }
+ * + *

+ * 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 @@ *

*
    *
  1. Sequence the pattern and assign a time precision to each part (e.g., {@code MM} is of month precision)
  2. - *
  3. 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
  4. - *
  5. Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss} and {@code SSS})
  6. + *
  7. 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
  8. + *
  9. Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss.SSS})
  10. *
* * @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. + *

    * * @since 2.25.0 */