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 99bd9c706de..16fd89ac30b 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 @@ -26,6 +26,7 @@ import java.util.Calendar; import java.util.Date; import java.util.TimeZone; +import java.util.stream.Stream; import org.apache.logging.log4j.core.AbstractLogEvent; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.time.Instant; @@ -329,29 +330,8 @@ void testNewInstanceAllowsNullParameter() { DatePatternConverter.newInstance(null); // no errors } - 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" - }; + private static final String[] PATTERN_NAMES = + Stream.of(NamedInstantPattern.values()).map(Enum::name).toArray(String[]::new); @Test void testPredefinedFormatWithoutTimezone() { diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/NamedInstantPatternTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/NamedInstantPatternTest.java new file mode 100644 index 00000000000..bbe5e6e45eb --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/NamedInstantPatternTest.java @@ -0,0 +1,45 @@ +/* + * 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 java.time.Instant; +import org.apache.logging.log4j.core.time.MutableInstant; +import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class NamedInstantPatternTest { + + @ParameterizedTest + @EnumSource(NamedInstantPattern.class) + void compatibilityOfLegacyPattern(NamedInstantPattern namedPattern) { + InstantPatternFormatter legacyFormatter = InstantPatternFormatter.newBuilder() + .setPattern(namedPattern.getLegacyPattern()) + .setLegacyFormattersEnabled(true) + .build(); + InstantPatternFormatter formatter = InstantPatternFormatter.newBuilder() + .setPattern(namedPattern.getPattern()) + .setLegacyFormattersEnabled(false) + .build(); + Instant javaTimeInstant = Instant.now(); + MutableInstant instant = new MutableInstant(); + instant.initFromEpochSecond(javaTimeInstant.getEpochSecond(), javaTimeInstant.getNano()); + assertThat(legacyFormatter.format(instant)).isEqualTo(formatter.format(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 f26a6d54c56..8b598c3de85 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 @@ -49,8 +49,6 @@ public final class DatePatternConverter extends LogEventPatternConverter impleme private static final String CLASS_NAME = DatePatternConverter.class.getSimpleName(); - private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS"; - private final InstantFormatter formatter; private DatePatternConverter(@Nullable final String[] options) { @@ -64,7 +62,9 @@ private static InstantFormatter createFormatter(@Nullable final String[] options } catch (final Exception error) { logOptionReadFailure(options, error, "failed for options: {}, falling back to the default instance"); } - return InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build(); + return InstantPatternFormatter.newBuilder() + .setPattern(NamedInstantPattern.DEFAULT.getPattern()) + .build(); } private static InstantFormatter createFormatterUnsafely(@Nullable final String[] options) { @@ -94,7 +94,7 @@ private static InstantFormatter createFormatterUnsafely(@Nullable final String[] private static String readPattern(@Nullable final String[] options) { return options != null && options.length > 0 && options[0] != null ? decodeNamedPattern(options[0]) - : DEFAULT_PATTERN; + : NamedInstantPattern.DEFAULT.getPattern(); } /** @@ -109,84 +109,16 @@ private static String readPattern(@Nullable final String[] options) { * @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"; + // See `NamedInstantPattern.getLegacyPattern()` + // for the difference between legacy and `DateTimeFormatter` patterns. + try { + NamedInstantPattern namedInstantPattern = NamedInstantPattern.valueOf(pattern); + return InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED + ? namedInstantPattern.getLegacyPattern() + : namedInstantPattern.getPattern(); + } catch (IllegalArgumentException ignored) { + return pattern; } - return pattern; } private static TimeZone readTimeZone(@Nullable final String[] options) { diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedInstantPattern.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedInstantPattern.java new file mode 100644 index 00000000000..4ce82f15f82 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedInstantPattern.java @@ -0,0 +1,161 @@ +/* + * 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 org.jspecify.annotations.NullMarked; + +/** + * Represents named date & time patterns for formatting log timestamps. + * + * @see DatePatternConverter + * @since 2.26.0 + */ +@NullMarked +public enum NamedInstantPattern { + ABSOLUTE("HH:mm:ss,SSS"), + + ABSOLUTE_MICROS("HH:mm:ss,SSSSSS", "HH:mm:ss,nnnnnn"), + + ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS", "HH:mm:ss,nnnnnnnnn"), + + ABSOLUTE_PERIOD("HH:mm:ss.SSS"), + + COMPACT("yyyyMMddHHmmssSSS"), + + DATE("dd MMM yyyy HH:mm:ss,SSS"), + + DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS"), + + DEFAULT("yyyy-MM-dd HH:mm:ss,SSS"), + + DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnn"), + + DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"), + + DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS"), + + ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS"), + + ISO8601_BASIC_PERIOD("yyyyMMdd'T'HHmmss.SSS"), + + ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS"), + + ISO8601_OFFSET_DATE_TIME_HH("yyyy-MM-dd'T'HH:mm:ss,SSSx", "yyyy-MM-dd'T'HH:mm:ss,SSSX"), + + ISO8601_OFFSET_DATE_TIME_HHMM("yyyy-MM-dd'T'HH:mm:ss,SSSxx", "yyyy-MM-dd'T'HH:mm:ss,SSSXX"), + + ISO8601_OFFSET_DATE_TIME_HHCMM("yyyy-MM-dd'T'HH:mm:ss,SSSxxx", "yyyy-MM-dd'T'HH:mm:ss,SSSXXX"), + + ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS"), + + ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", "yyyy-MM-dd'T'HH:mm:ss.nnnnnn"), + + US_MONTH_DAY_YEAR2_TIME("dd/MM/yy HH:mm:ss.SSS"), + + US_MONTH_DAY_YEAR4_TIME("dd/MM/yyyy HH:mm:ss.SSS"); + + private final String pattern; + private final String legacyPattern; + + NamedInstantPattern(String pattern) { + this(pattern, pattern); + } + + NamedInstantPattern(String pattern, String legacyPattern) { + this.pattern = pattern; + this.legacyPattern = legacyPattern; + } + + /** + * Returns the date-time pattern string compatible with {@link java.time.format.DateTimeFormatter} + * that is associated with this named pattern. + * + * @return the date-time pattern string for use with {@code DateTimeFormatter} + */ + public String getPattern() { + return pattern; + } + + /** + * Returns the legacy {@link org.apache.logging.log4j.core.util.datetime.FixedDateFormat} pattern + * associated with this named pattern. + *
+ * If legacy formatters are enabled, output is produced for + * {@code FixedDateFormat} and {@code FastDateFormat}. To convert the {@code DateTimeFormatter} + * to its legacy counterpart, the following transformations need to be applied: + *
+ *+ * | Microseconds | + *Nanoseconds | + *Time-zone | + *
---|---|---|---|
Legacy formatter directive | + *nnnnnn |
+ * nnnnnnnnn |
+ * X , XX , XXX |
+ *
{@code DateTimeFormatter} directive | + *SSSSSS |
+ * SSSSSSSSS |
+ * x , xx , xxx |
+ *
+ * Legacy formatters are largely compatible with the {@code SimpleDateFormat} specification, + * but introduce a custom {@code n} pattern letter, unique to Log4j, to represent sub-millisecond precision. + * This {@code n} is not part of the standard {@code SimpleDateFormat}. + *
+ *+ * In legacy formatters, repeating {@code n} increases the precision, similar to how repeated {@code S} + * is used for fractional seconds in {@code DateTimeFormatter}. + *
+ *+ * In contrast, {@code DateTimeFormatter} interprets {@code n} as nano-of-second. + * In Java 17, both {@code n} and {@code N} always output nanosecond precision, + * regardless of the number of pattern letters. + *
+ *
+ * Legacy formatters use X
, XX
, and XXX
to format time zones as
+ * +00
, +0000
, or +00:00
, following {@code SimpleDateFormat} conventions.
+ * In contrast, {@code DateTimeFormatter} outputs Z
for zero-offset when using X
.
+ * To ensure numeric output for zero-offset (e.g., +00
),
+ * we use x
, xx
, or xxx
instead.
+ *