You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2024/10/31 15:19:10 UTC

(logging-log4j2) 01/01: Port #3121 to `main`

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

vy pushed a commit to branch feature/main/instant-formatter
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit f2c7dfcbc9b1d5299d1f8da6e76397981d6c60f9
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Thu Oct 31 16:18:49 2024 +0100

    Port #3121 to `main`
---
 .../rolling/OnStartupTriggeringPolicyTest.java     |   10 +-
 ...RollingAppenderDeleteAccumulatedCount1Test.java |    6 +-
 ...RollingAppenderDeleteAccumulatedCount2Test.java |    6 +-
 .../RollingAppenderDeleteAccumulatedSizeTest.java  |    6 +-
 .../rolling/RollingAppenderDeleteNestedTest.java   |    6 +-
 .../logging/log4j/core/layout/HtmlLayoutTest.java  |   69 -
 .../core/pattern/DatePatternConverterTest.java     |  340 +----
 .../log4j/core/time/InstantFormatterTest.java      |   90 --
 .../time/internal/format/FixedDateFormatTest.java  |  404 ------
 .../instant/InstantNumberFormatterTest.java        |   70 +
 .../InstantPatternDynamicFormatterTest.java        |  424 ++++++
 ...stantPatternThreadLocalCachedFormatterTest.java |  292 ++++
 .../resources/log4j-rolling-size-with-time.xml     |    2 +-
 .../logging/log4j/core/layout/HtmlLayout.java      |    2 +-
 .../log4j/core/pattern/DatePatternConverter.java   |  329 ++---
 .../core/pattern/FileDatePatternConverter.java     |   11 +-
 .../logging/log4j/core/time/InstantFormatter.java  |  317 -----
 .../core/time/internal/format/DatePrinter.java     |  144 --
 .../core/time/internal/format/FastDateFormat.java  |  588 --------
 .../core/time/internal/format/FastDatePrinter.java | 1503 --------------------
 .../core/time/internal/format/FixedDateFormat.java |  760 ----------
 .../log4j/core/time/internal/format/Format.java    |   31 -
 .../core/time/internal/format/FormatCache.java     |  265 ----
 .../core/time/internal/format/package-info.java    |   20 -
 .../log4j/core/util/datetime/DatePrinter.java      |  133 --
 .../log4j/core/util/datetime/FastDateFormat.java   |  207 ---
 .../instant/InstantFormatter.java}                 |   28 +-
 .../internal/instant/InstantNumberFormatter.java   |  128 ++
 .../instant/InstantPatternDynamicFormatter.java    |  831 +++++++++++
 .../internal/instant/InstantPatternFormatter.java  |  166 +++
 .../InstantPatternThreadLocalCachedFormatter.java  |  134 ++
 .../instant}/package-info.java                     |   18 +-
 .../layout/template/json/LogEventFixture.java      |   27 +-
 .../log4j/layout/template/json/package-info.java   |   22 -
 .../template/json/resolver/TimestampResolver.java  |  247 +---
 .../log4j/perf/jmh/DateTimeFormatBenchmark.java    |  117 --
 .../perf/jmh/ThreadsafeDateFormatBenchmark.java    |  241 ----
 .../log4j/perf/jmh/TimeFormatBenchmark.java        |  310 ----
 ...PatternDynamicFormatterSequencingBenchmark.java |   96 ++
 .../instant/InstantPatternFormatterBenchmark.java  |  151 ++
 .../InstantPatternFormatterImpactBenchmark.java    |  125 ++
 .../.3.x.x/3149_remove_named_date_patterns.xml     |    8 +
 src/changelog/.index.adoc.ftl                      |    2 +-
 .../modules/ROOT/pages/manual/pattern-layout.adoc  |   77 +-
 44 files changed, 2712 insertions(+), 6051 deletions(-)

diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
index 0254773222..df7d585e3d 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
@@ -35,7 +35,8 @@ import java.util.stream.Stream;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.DefaultConfiguration;
 import org.apache.logging.log4j.core.layout.PatternLayout;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternFormatter;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -46,7 +47,8 @@ public class OnStartupTriggeringPolicyTest {
 
     private static final String TARGET_PATTERN = "/test1-%d{MM-dd-yyyy}-%i.log";
     private static final String TEST_DATA = "Hello world!";
-    private static final FastDateFormat formatter = FastDateFormat.getInstance("MM-dd-yyyy");
+    private static final InstantPatternFormatter formatter =
+            InstantPatternFormatter.newBuilder().setPattern("MM-dd-yyyy").build();
 
     @TempDir
     Path tempDir;
@@ -56,7 +58,9 @@ public class OnStartupTriggeringPolicyTest {
         final Configuration configuration = new DefaultConfiguration();
         final Path target = tempDir.resolve("testfile");
         final long timeStamp = Instant.now().minus(Duration.ofDays(1)).toEpochMilli();
-        final String expectedDate = formatter.format(timeStamp);
+        final MutableInstant instant = new MutableInstant();
+        instant.initFromEpochMilli(timeStamp, 0);
+        final String expectedDate = formatter.format(instant);
         final Path rolled = tempDir.resolve("test1-" + expectedDate + "-1.log");
         final long copied;
         try (final InputStream is = new ByteArrayInputStream(TEST_DATA.getBytes(StandardCharsets.UTF_8))) {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount1Test.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount1Test.java
index 51fead166a..423e91fd63 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount1Test.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount1Test.java
@@ -35,8 +35,6 @@ import java.util.List;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
 import org.apache.logging.log4j.test.junit.CleanUpDirectories;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -76,9 +74,7 @@ public class RollingAppenderDeleteAccumulatedCount1Test {
         assertTrue(files.length > 0, "Dir " + DIR + " should contain files");
         for (final File file : files) {
             final BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
-            System.out.println(file + " (" + fileAttributes.size() + "B) "
-                    + FixedDateFormat.create(FixedFormat.ABSOLUTE)
-                            .format(fileAttributes.lastModifiedTime().toMillis()));
+            System.out.println(file + " (" + fileAttributes.size() + "B) " + fileAttributes.lastModifiedTime());
         }
         final List<String> expected = List.of("my-1.log", "my-2.log", "my-3.log", "my-4.log", "my-5.log");
         assertEquals(expected.size() + 6, files.length, Arrays.toString(files));
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount2Test.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount2Test.java
index 0fd8772135..d0e2153585 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount2Test.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedCount2Test.java
@@ -35,8 +35,6 @@ import java.util.List;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
 import org.apache.logging.log4j.test.junit.CleanUpDirectories;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -77,9 +75,7 @@ public class RollingAppenderDeleteAccumulatedCount2Test {
         assertTrue(files.length > 0, "Dir " + DIR + " should contain files");
         for (final File file : files) {
             final BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
-            System.out.println(file + " (" + fileAttributes.size() + "B) "
-                    + FixedDateFormat.create(FixedFormat.ABSOLUTE)
-                            .format(fileAttributes.lastModifiedTime().toMillis()));
+            System.out.println(file + " (" + fileAttributes.size() + "B) " + fileAttributes.lastModifiedTime());
         }
         // sometimes "test-9.log", sometimes "test-10.log" remains
         final List<String> expected = List.of("my-1.log", "my-2.log", "my-3.log", "my-4.log", "my-5.log");
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedSizeTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedSizeTest.java
index 15240841f9..f0fdffc758 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedSizeTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteAccumulatedSizeTest.java
@@ -27,8 +27,6 @@ import java.util.Arrays;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
 import org.apache.logging.log4j.test.junit.CleanUpDirectories;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -61,9 +59,7 @@ public class RollingAppenderDeleteAccumulatedSizeTest {
         assertTrue(files.length > 0, "Dir " + DIR + " should contain files");
         for (final File file : files) {
             final BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
-            System.out.println(file + " (" + fileAttributes.size() + "B) "
-                    + FixedDateFormat.create(FixedFormat.ABSOLUTE)
-                            .format(fileAttributes.lastModifiedTime().toMillis()));
+            System.out.println(file + " (" + fileAttributes.size() + "B) " + fileAttributes.lastModifiedTime());
         }
         assertEquals(4, files.length, Arrays.toString(files));
         long total = 0;
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteNestedTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteNestedTest.java
index 38f7417a95..a8133732b0 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteNestedTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderDeleteNestedTest.java
@@ -35,8 +35,6 @@ import java.util.List;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
 import org.apache.logging.log4j.test.junit.CleanUpDirectories;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -76,9 +74,7 @@ public class RollingAppenderDeleteNestedTest {
         assertTrue(files.length > 0, "Dir " + DIR + " should contain files");
         for (final File file : files) {
             final BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
-            System.out.println(file + " (" + fileAttributes.size() + "B) "
-                    + FixedDateFormat.create(FixedFormat.ABSOLUTE)
-                            .format(fileAttributes.lastModifiedTime().toMillis()));
+            System.out.println(file + " (" + fileAttributes.size() + "B) " + fileAttributes.lastModifiedTime());
         }
 
         final List<String> expected = Arrays.asList("my-1.log", "my-2.log", "my-3.log", "my-4.log", "my-5.log");
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 cb14f727ea..bab2f93072 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
@@ -16,26 +16,19 @@
  */
 package org.apache.logging.log4j.core.layout;
 
-import static org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.lang.management.ManagementFactory;
 import java.nio.charset.StandardCharsets;
-import java.text.MessageFormat;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
 import java.util.Calendar;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.AbstractLogEvent;
 import org.apache.logging.log4j.core.Appender;
-import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.Logger;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.config.Configuration;
@@ -45,7 +38,6 @@ import org.apache.logging.log4j.core.test.appender.ListAppender;
 import org.apache.logging.log4j.core.test.junit.ConfigurationFactoryType;
 import org.apache.logging.log4j.core.time.Instant;
 import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.test.junit.UsingAnyThreadContext;
@@ -243,68 +235,7 @@ public class HtmlLayoutTest {
         assertEquals("<td>" + event.getTimeMillis() + "</td>", actual, "Incorrect date:" + actual);
     }
 
-    @Test
-    public void testLayoutWithDatePatternFixedFormat() {
-        for (final String timeZone : new String[] {"GMT+8", "GMT+0530", "UTC", null}) {
-            for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
-                testLayoutWithDatePatternFixedFormat(format, timeZone);
-            }
-        }
-    }
-
     private String getDateLine(final String logEventString) {
         return logEventString.split(System.lineSeparator())[2];
     }
-
-    private void testLayoutWithDatePatternFixedFormat(final FixedFormat format, final String timezone) {
-        final HtmlLayout layout = HtmlLayout.newBuilder()
-                .setConfiguration(new DefaultConfiguration())
-                .setDatePattern(format.name())
-                .setTimezone(timezone)
-                .build();
-
-        final LogEvent event = new MyLogEvent();
-        final String actual = getDateLine(layout.toSerializable(event));
-
-        // build expected date string
-        final java.time.Instant instant = java.time.Instant.ofEpochSecond(
-                event.getInstant().getEpochSecond(), event.getInstant().getNanoOfSecond());
-        ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
-        if (timezone != null) {
-            zonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of(timezone));
-        }
-
-        // LOG4J2-3019 HtmlLayoutTest.testLayoutWithDatePatternFixedFormat test fails on windows
-        // https://issues.apache.org/jira/browse/LOG4J2-3019
-        // java.time.format.DateTimeFormatterBuilder.toFormatter() defaults to using
-        // Locale.getDefault(Locale.Category.FORMAT)
-        final Locale formatLocale = Locale.getDefault(Locale.Category.FORMAT);
-        final Locale locale = Locale.getDefault().equals(formatLocale) ? formatLocale : Locale.getDefault();
-
-        // For DateTimeFormatter of jdk,
-        // Pattern letter 'S' means fraction-of-second, 'n' means nano-of-second. Log4j2 needs S.
-        // Pattern letter 'X' (upper case) will output 'Z' when the offset to be output would be zero,
-        // whereas pattern letter 'x' (lower case) will output '+00', '+0000', or '+00:00'. Log4j2 needs x.
-        final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(
-                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.getTimeZoneFormat())
-                && offset.contains(":")
-                && !"00".equals(offset.split(":")[1])) {
-            expected = expected.substring(0, expected.length() - 2);
-        }
-
-        assertEquals(
-                "<td>" + expected + "</td>",
-                actual,
-                MessageFormat.format("Incorrect date={0}, format={1}, timezone={2}", actual, format.name(), timezone));
-    }
 }
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
index d908f8013f..961a30f61a 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
@@ -19,17 +19,12 @@ package org.apache.logging.log4j.core.pattern;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 
-import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Date;
-import java.util.TimeZone;
 import org.apache.logging.log4j.core.AbstractLogEvent;
 import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.config.NullConfiguration;
 import org.apache.logging.log4j.core.time.Instant;
 import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedTimeZoneFormat;
 import org.apache.logging.log4j.util.Strings;
 import org.junit.jupiter.api.Test;
 
@@ -53,29 +48,7 @@ class DatePatternConverterTest {
         }
     }
 
-    /**
-     * SimpleTimePattern for DEFAULT.
-     */
-    private static final String DEFAULT_PATTERN = FixedDateFormat.FixedFormat.DEFAULT.getPattern();
-
-    /**
-     * ISO8601 string literal.
-     */
-    private static final String ISO8601 = FixedDateFormat.FixedFormat.ISO8601.name();
-
-    /**
-     * ISO8601_OFFSE_DATE_TIME_XX string literal.
-     */
-    private static final String ISO8601_OFFSE_DATE_TIME_HHMM =
-            FixedDateFormat.FixedFormat.ISO8601_OFFSET_DATE_TIME_HHMM.name();
-
-    /**
-     * 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_FORMAT_OPTIONS = {ISO8601};
+    private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
 
     private static Date date(final int year, final int month, final int date) {
         final Calendar cal = Calendar.getInstance();
@@ -84,55 +57,9 @@ class DatePatternConverterTest {
         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
     public void testFormatDateStringBuilderDefaultPattern() {
-        assertDatePattern(null, date(2001, 1, 1), "2001-02-01 14:15:16,123");
-    }
-
-    @Test
-    public void testFormatDateStringBuilderIso8601() {
-        final DatePatternConverter converter = createConverter(ISO8601_FORMAT_OPTIONS);
-        final StringBuilder sb = new StringBuilder();
-        converter.format(date(2001, 1, 1), sb);
-
-        final String expected = "2001-02-01T14:15:16,123";
-        assertEquals(expected, sb.toString());
-    }
-
-    @Test
-    public void testFormatDateStringBuilderIso8601BasicWithPeriod() {
-        assertDatePattern(
-                FixedDateFormat.FixedFormat.ISO8601_BASIC_PERIOD.name(), 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");
-    }
-
-    @Test
-    public void testFormatDateStringBuilderIso8601WithPeriodMicroseconds() {
-        final String[] pattern = {FixedDateFormat.FixedFormat.ISO8601_PERIOD_MICROS.name(), "Z"};
-        final DatePatternConverter converter = createConverter(pattern);
-        final StringBuilder sb = new StringBuilder();
-        final MutableInstant instant = new MutableInstant();
-        instant.initFromEpochMilli(
-                1577225134559L,
-                // One microsecond
-                1000);
-        converter.format(instant, sb);
-
-        final String expected = "2019-12-24T22:05:34.559001";
-        assertEquals(expected, sb.toString());
+        assertDatePattern(null, date(2001, 1, 1), "2001-02-01 14:15:16.123");
     }
 
     @Test
@@ -143,126 +70,34 @@ class DatePatternConverterTest {
     @Test
     public void testFormatLogEventStringBuilderDefaultPattern() {
         final LogEvent event = new MyLogEvent();
-        final DatePatternConverter converter = createConverter(null);
-        final StringBuilder sb = new StringBuilder();
-        converter.format(event, sb);
-
-        final String expected = "2011-12-30 10:56:35,987";
-        assertEquals(expected, sb.toString());
-    }
-
-    @Test
-    public void testFormatLogEventStringBuilderIso8601() {
-        final LogEvent event = new MyLogEvent();
-        final DatePatternConverter converter = createConverter(ISO8601_FORMAT_OPTIONS);
+        final DatePatternConverter converter = DatePatternConverter.newInstance(null);
         final StringBuilder sb = new StringBuilder();
         converter.format(event, sb);
 
-        final String expected = "2011-12-30T10:56:35,987";
+        final String expected = "2011-12-30 10:56:35.987";
         assertEquals(expected, sb.toString());
     }
 
     @Test
     public void testFormatAmericanPatterns() {
         final Date date = date(2011, 2, 11);
-        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");
     }
 
     private static void assertDatePattern(final String format, final Date date, final String expected) {
-        final DatePatternConverter converter = createConverter(new String[] {format});
+        final DatePatternConverter converter = DatePatternConverter.newInstance(new String[] {format});
         final StringBuilder sb = new StringBuilder();
         converter.format(date, sb);
 
         assertEquals(expected, sb.toString());
     }
 
-    @Test
-    public void testFormatLogEventStringBuilderIso8601TimezoneJST() {
-        final LogEvent event = new MyLogEvent();
-        final String[] optionsWithTimezone = {ISO8601, "JST"};
-        final DatePatternConverter converter = createConverter(optionsWithTimezone);
-        final StringBuilder sb = new StringBuilder();
-        converter.format(event, sb);
-
-        // JST=Japan Standard Time: UTC+9:00
-        final TimeZone tz = TimeZone.getTimeZone("JST");
-        final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
-        sdf.setTimeZone(tz);
-        final long adjusted = event.getTimeMillis() + tz.getDSTSavings();
-        final String expected = sdf.format(new Date(adjusted));
-        // final String expected = "2011-12-30T18:56:35,987"; // in CET (Central Eastern Time: Amsterdam)
-        assertEquals(expected, sb.toString());
-    }
-
-    @Test
-    public void testFormatLogEventStringBuilderIso8601TimezoneOffsetHHCMM() {
-        final LogEvent event = new MyLogEvent();
-        final String[] optionsWithTimezone = {ISO8601_OFFSET_DATE_TIME_HHCMM};
-        final DatePatternConverter converter = createConverter(optionsWithTimezone);
-        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;
-        assertEquals(expected, sb.toString());
-    }
-
-    @Test
-    public void testFormatLogEventStringBuilderIso8601TimezoneOffsetHHMM() {
-        final LogEvent event = new MyLogEvent();
-        final String[] optionsWithTimezone = {ISO8601_OFFSE_DATE_TIME_HHMM};
-        final DatePatternConverter converter = createConverter(optionsWithTimezone);
-        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;
-        assertEquals(expected, sb.toString());
-    }
-
-    @Test
-    public void testFormatLogEventStringBuilderIso8601TimezoneUTC() {
-        final LogEvent event = new MyLogEvent();
-        final DatePatternConverter converter = createConverter(new String[] {"ISO8601", "UTC"});
-        final StringBuilder sb = new StringBuilder();
-        converter.format(event, sb);
-
-        final TimeZone tz = TimeZone.getTimeZone("UTC");
-        final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
-        sdf.setTimeZone(tz);
-        final long adjusted = event.getTimeMillis() + tz.getDSTSavings();
-        final String expected = sdf.format(new Date(adjusted));
-        // final String expected = "2011-12-30T09:56:35,987";
-        assertEquals(expected, sb.toString());
-    }
-
-    @Test
-    public void testFormatLogEventStringBuilderIso8601TimezoneZ() {
-        final LogEvent event = new MyLogEvent();
-        final String[] optionsWithTimezone = {ISO8601, "Z"};
-        final DatePatternConverter converter = createConverter(optionsWithTimezone);
-        final StringBuilder sb = new StringBuilder();
-        converter.format(event, sb);
-
-        final TimeZone tz = TimeZone.getTimeZone("UTC");
-        final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
-        sdf.setTimeZone(tz);
-        final long adjusted = event.getTimeMillis() + tz.getDSTSavings();
-        final String expected = sdf.format(new Date(adjusted));
-        // final String expected = "2011-12-30T17:56:35,987"; // in UTC
-        assertEquals(expected, sb.toString());
-    }
-
     @Test
     public void testFormatObjectStringBuilderDefaultPattern() {
-        final DatePatternConverter converter = createConverter(null);
+        final DatePatternConverter converter = DatePatternConverter.newInstance(null);
         final StringBuilder sb = new StringBuilder();
         converter.format("nondate", sb);
 
@@ -272,187 +107,58 @@ class DatePatternConverterTest {
 
     @Test
     public void testFormatStringBuilderObjectArrayDefaultPattern() {
-        final DatePatternConverter converter = createConverter(null);
+        final DatePatternConverter converter = DatePatternConverter.newInstance(null);
         final StringBuilder sb = new StringBuilder();
         converter.format(sb, date(2001, 1, 1), date(2002, 2, 2), date(2003, 3, 3));
 
-        final String expected = "2001-02-01 14:15:16,123"; // only process first date
-        assertEquals(expected, sb.toString());
-    }
-
-    @Test
-    public void testFormatStringBuilderObjectArrayIso8601() {
-        final DatePatternConverter converter = createConverter(ISO8601_FORMAT_OPTIONS);
-        final StringBuilder sb = new StringBuilder();
-        converter.format(sb, date(2001, 1, 1), date(2002, 2, 2), date(2003, 3, 3));
-
-        final String expected = "2001-02-01T14:15:16,123"; // only process first date
+        final String expected = "2001-02-01 14:15:16.123"; // only process first date
         assertEquals(expected, sb.toString());
     }
 
     @Test
     public void testGetPatternReturnsDefaultForEmptyOptionsArray() {
-        assertEquals(DEFAULT_PATTERN, createConverter(new String[0]).getPattern());
+        assertEquals(
+                DEFAULT_PATTERN,
+                DatePatternConverter.newInstance(Strings.EMPTY_ARRAY).getPattern());
     }
 
     @Test
     public void testGetPatternReturnsDefaultForInvalidPattern() {
-        final String[] invalid = {"ABC I am not a valid date pattern"};
-        assertEquals(DEFAULT_PATTERN, createConverter(invalid).getPattern());
+        final String[] invalid = {"A single `V` is not allow by `DateTimeFormatter` and should cause an exception"};
+        assertEquals(DEFAULT_PATTERN, DatePatternConverter.newInstance(invalid).getPattern());
     }
 
     @Test
     public void testGetPatternReturnsDefaultForNullOptions() {
-        assertEquals(DEFAULT_PATTERN, createConverter(null).getPattern());
+        assertEquals(DEFAULT_PATTERN, DatePatternConverter.newInstance(null).getPattern());
     }
 
     @Test
     public void testGetPatternReturnsDefaultForSingleNullElementOptionsArray() {
-        assertEquals(DEFAULT_PATTERN, createConverter(new String[1]).getPattern());
+        assertEquals(
+                DEFAULT_PATTERN, DatePatternConverter.newInstance(new String[1]).getPattern());
     }
 
     @Test
     public void testGetPatternReturnsDefaultForTwoNullElementsOptionsArray() {
-        assertEquals(DEFAULT_PATTERN, createConverter(new String[2]).getPattern());
+        assertEquals(
+                DEFAULT_PATTERN, DatePatternConverter.newInstance(new String[2]).getPattern());
     }
 
     @Test
     public void testGetPatternReturnsNullForUnix() {
         final String[] options = {"UNIX"};
-        assertNull(createConverter(options).getPattern());
+        assertNull(DatePatternConverter.newInstance(options).getPattern());
     }
 
     @Test
     public void testGetPatternReturnsNullForUnixMillis() {
         final String[] options = {"UNIX_MILLIS"};
-        assertNull(createConverter(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;
-                preciseConverter = createConverter(new String[] {precisePattern});
-            } 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 = createConverter(new String[] {precisePattern});
-            }
-            preciseConverter.format(event, preciseBuilder);
-
-            final String[] milliOptions = {pattern};
-            createConverter(milliOptions).format(event, milliBuilder);
-            final FixedTimeZoneFormat timeZoneFormat = format.getTimeZoneFormat();
-            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);
-        }
+        assertNull(DatePatternConverter.newInstance(options).getPattern());
     }
 
     @Test
     public void testNewInstanceAllowsNullParameter() {
-        createConverter(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 = createConverter(preciseOptions);
-                    preciseConverter.format(event, preciseBuilder);
-
-                    final String[] milliOptions = {pattern, timeZone};
-                    createConverter(milliOptions).format(event, milliBuilder);
-                    final FixedTimeZoneFormat timeZoneFormat = format.getTimeZoneFormat();
-                    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);
-                }
-            }
-        }
-    }
-
-    @Test
-    public void testPredefinedFormatWithoutTimezone() {
-        for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
-            final String[] options = {format.name()};
-            final DatePatternConverter converter = createConverter(options);
-            assertEquals(format.getPattern(), 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
-            final DatePatternConverter converter = createConverter(options);
-            assertEquals(format.getPattern(), converter.getPattern());
-        }
-    }
-
-    private static DatePatternConverter createConverter(final String[] options) {
-        return DatePatternConverter.newInstance(new NullConfiguration(), options);
+        DatePatternConverter.newInstance(null); // no errors
     }
 }
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/InstantFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/InstantFormatterTest.java
deleted file mode 100644
index 783e438078..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/InstantFormatterTest.java
+++ /dev/null
@@ -1,90 +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.time;
-
-import java.util.Locale;
-import java.util.TimeZone;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-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 <a href="https://issues.apache.org/jira/browse/LOG4J2-3614">LOG4J2-3614</a>.
-     */
-    @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");
-    }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java
deleted file mode 100644
index 691483c029..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java
+++ /dev/null
@@ -1,404 +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.time.internal.format;
-
-import static org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat.DEFAULT;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
-import org.junit.Test;
-
-/**
- * Tests {@link FixedDateFormat}.
- */
-public class FixedDateFormatTest {
-
-    private boolean containsNanos(final FixedFormat fixedFormat) {
-        final String pattern = fixedFormat.getPattern();
-        return pattern.endsWith("n") || pattern.matches(".+n+X*") || pattern.matches(".+n+Z*");
-    }
-
-    @Test
-    public void testFixedFormat_getDatePatternNullIfNoDateInPattern() {
-        assertNull(FixedFormat.ABSOLUTE.getDatePattern());
-        assertNull(FixedFormat.ABSOLUTE_PERIOD.getDatePattern());
-    }
-
-    @Test
-    public void testFixedFormat_getDatePatternLengthZeroIfNoDateInPattern() {
-        assertEquals(0, FixedFormat.ABSOLUTE.getDatePatternLength());
-        assertEquals(0, FixedFormat.ABSOLUTE_PERIOD.getDatePatternLength());
-    }
-
-    @Test
-    public void testFixedFormat_getFastDateFormatNullIfNoDateInPattern() {
-        assertNull(FixedFormat.ABSOLUTE.getFastDateFormat());
-        assertNull(FixedFormat.ABSOLUTE_PERIOD.getFastDateFormat());
-    }
-
-    @Test
-    public void testFixedFormat_getDatePatternReturnsDatePatternIfExists() {
-        assertEquals("yyyyMMdd", FixedFormat.COMPACT.getDatePattern());
-        assertEquals("yyyy-MM-dd ", DEFAULT.getDatePattern());
-    }
-
-    @Test
-    public void testFixedFormat_getDatePatternLengthReturnsDatePatternLength() {
-        assertEquals("yyyyMMdd".length(), FixedFormat.COMPACT.getDatePatternLength());
-        assertEquals("yyyy-MM-dd ".length(), DEFAULT.getDatePatternLength());
-    }
-
-    @Test
-    public void testFixedFormat_getFastDateFormatNonNullIfDateInPattern() {
-        assertNotNull(FixedFormat.COMPACT.getFastDateFormat());
-        assertNotNull(DEFAULT.getFastDateFormat());
-        assertEquals("yyyyMMdd", FixedFormat.COMPACT.getFastDateFormat().getPattern());
-        assertEquals("yyyy-MM-dd ", DEFAULT.getFastDateFormat().getPattern());
-    }
-
-    @Test
-    public void testCreateIfSupported_nonNullIfNameMatches() {
-        for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
-            final String[] options = {format.name()};
-            assertNotNull(format.name(), FixedDateFormat.createIfSupported(options));
-        }
-    }
-
-    @Test
-    public void testCreateIfSupported_nonNullIfPatternMatches() {
-        for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
-            final String[] options = {format.getPattern()};
-            assertNotNull(format.name(), FixedDateFormat.createIfSupported(options));
-        }
-    }
-
-    @Test
-    public void testCreateIfSupported_nullIfNameDoesNotMatch() {
-        final String[] options = {"DEFAULT3"};
-        assertNull("DEFAULT3", FixedDateFormat.createIfSupported(options));
-    }
-
-    @Test
-    public void testCreateIfSupported_nullIfPatternDoesNotMatch() {
-        final String[] options = {"y M d H m s"};
-        assertNull("y M d H m s", FixedDateFormat.createIfSupported(options));
-    }
-
-    @Test
-    public void testCreateIfSupported_defaultIfOptionsArrayNull() {
-        final FixedDateFormat fmt = FixedDateFormat.createIfSupported((String[]) null);
-        assertEquals(DEFAULT.getPattern(), fmt.getFormat());
-    }
-
-    @Test
-    public void testCreateIfSupported_defaultIfOptionsArrayEmpty() {
-        final FixedDateFormat fmt = FixedDateFormat.createIfSupported(new String[0]);
-        assertEquals(DEFAULT.getPattern(), fmt.getFormat());
-    }
-
-    @Test
-    public void testCreateIfSupported_defaultIfOptionsArrayWithSingleNullElement() {
-        final FixedDateFormat fmt = FixedDateFormat.createIfSupported(new String[1]);
-        assertEquals(DEFAULT.getPattern(), fmt.getFormat());
-        assertEquals(TimeZone.getDefault(), fmt.getTimeZone());
-    }
-
-    @Test
-    public void testCreateIfSupported_defaultTimeZoneIfOptionsArrayWithSecondNullElement() {
-        final FixedDateFormat fmt = FixedDateFormat.createIfSupported(new String[] {DEFAULT.getPattern(), null, ""});
-        assertEquals(DEFAULT.getPattern(), fmt.getFormat());
-        assertEquals(TimeZone.getDefault(), fmt.getTimeZone());
-    }
-
-    @Test
-    public void testCreateIfSupported_customTimeZoneIfOptionsArrayWithTimeZoneElement() {
-        final FixedDateFormat fmt =
-                FixedDateFormat.createIfSupported(new String[] {DEFAULT.getPattern(), "+08:00", ""});
-        assertEquals(DEFAULT.getPattern(), fmt.getFormat());
-        assertEquals(TimeZone.getTimeZone("GMT+08:00"), fmt.getTimeZone());
-    }
-
-    @Test(expected = NullPointerException.class)
-    public void testConstructorDisallowsNullFormat() {
-        new FixedDateFormat(null, TimeZone.getDefault());
-    }
-
-    @Test(expected = NullPointerException.class)
-    public void testConstructorDisallowsNullTimeZone() {
-        new FixedDateFormat(FixedFormat.ABSOLUTE, null);
-    }
-
-    @Test
-    public void testGetFormatReturnsConstructorFixedFormatPattern() {
-        final FixedDateFormat format = new FixedDateFormat(FixedDateFormat.FixedFormat.ABSOLUTE, TimeZone.getDefault());
-        assertSame(FixedDateFormat.FixedFormat.ABSOLUTE.getPattern(), format.getFormat());
-    }
-
-    @Test
-    public void testFormatLong() {
-        final long now = System.currentTimeMillis();
-        final long start = now - TimeUnit.HOURS.toMillis(25);
-        final long end = now + TimeUnit.HOURS.toMillis(25);
-        for (final FixedFormat format : FixedFormat.values()) {
-            final String pattern = format.getPattern();
-            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
-                continue; // cannot compile precise timestamp formats with SimpleDateFormat
-            }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
-            final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
-            for (long time = start; time < end; time += 12345) {
-                final String actual = customTF.format(time);
-                final String expected = simpleDF.format(new Date(time));
-                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
-            }
-        }
-    }
-
-    @Test
-    public void testFormatLong_goingBackInTime() {
-        final long now = System.currentTimeMillis();
-        final long start = now - TimeUnit.HOURS.toMillis(25);
-        final long end = now + TimeUnit.HOURS.toMillis(25);
-        for (final FixedFormat format : FixedFormat.values()) {
-            final String pattern = format.getPattern();
-            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
-                continue; // cannot compile precise timestamp formats with SimpleDateFormat
-            }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
-            final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
-            for (long time = end; time > start; time -= 12345) {
-                final String actual = customTF.format(time);
-                final String expected = simpleDF.format(new Date(time));
-                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
-            }
-        }
-    }
-
-    @Test
-    public void testFormatLongCharArrayInt() {
-        final long now = System.currentTimeMillis();
-        final long start = now - TimeUnit.HOURS.toMillis(25);
-        final long end = now + TimeUnit.HOURS.toMillis(25);
-        final char[] buffer = new char[128];
-        for (final FixedFormat format : FixedFormat.values()) {
-            final String pattern = format.getPattern();
-            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
-                continue; // cannot compile precise timestamp formats with SimpleDateFormat
-            }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
-            final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
-            for (long time = start; time < end; time += 12345) {
-                final int length = customTF.format(time, buffer, 23);
-                final String actual = new String(buffer, 23, length);
-                final String expected = simpleDF.format(new Date(time));
-                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
-            }
-        }
-    }
-
-    @Test
-    public void testFormatLongCharArrayInt_goingBackInTime() {
-        final long now = System.currentTimeMillis();
-        final long start = now - TimeUnit.HOURS.toMillis(25);
-        final long end = now + TimeUnit.HOURS.toMillis(25);
-        final char[] buffer = new char[128];
-        for (final FixedFormat format : FixedFormat.values()) {
-            final String pattern = format.getPattern();
-            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
-                continue; // cannot compile precise timestamp formats with SimpleDateFormat
-            }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
-            final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
-            for (long time = end; time > start; time -= 12345) {
-                final int length = customTF.format(time, buffer, 23);
-                final String actual = new String(buffer, 23, length);
-                final String expected = simpleDF.format(new Date(time));
-                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
-            }
-        }
-    }
-
-    @Test
-    public void testDaylightSavingToSummerTime() throws Exception {
-        final Calendar calendar = Calendar.getInstance();
-        calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse("2017-03-12 00:00:00 UTC"));
-
-        final SimpleDateFormat usCentral = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS", Locale.US);
-        usCentral.setTimeZone(TimeZone.getTimeZone("US/Central"));
-
-        final SimpleDateFormat utc = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS", Locale.US);
-        utc.setTimeZone(TimeZone.getTimeZone("UTC"));
-
-        final FixedDateFormat fixedUsCentral = FixedDateFormat.create(DEFAULT, TimeZone.getTimeZone("US/Central"));
-        final FixedDateFormat fixedUtc = FixedDateFormat.create(DEFAULT, TimeZone.getTimeZone("UTC"));
-
-        final String[][] expectedDstAndNoDst = {
-            // US/Central, UTC
-            {"2017-03-11 18:00:00,000", "2017-03-12 00:00:00,000"}, //
-            {"2017-03-11 19:00:00,000", "2017-03-12 01:00:00,000"}, //
-            {"2017-03-11 20:00:00,000", "2017-03-12 02:00:00,000"}, //
-            {"2017-03-11 21:00:00,000", "2017-03-12 03:00:00,000"}, //
-            {"2017-03-11 22:00:00,000", "2017-03-12 04:00:00,000"}, //
-            {"2017-03-11 23:00:00,000", "2017-03-12 05:00:00,000"}, //
-            {"2017-03-12 00:00:00,000", "2017-03-12 06:00:00,000"}, //
-            {"2017-03-12 01:00:00,000", "2017-03-12 07:00:00,000"}, //
-            {"2017-03-12 03:00:00,000", "2017-03-12 08:00:00,000"}, //  DST jump at 2am US central time
-            {"2017-03-12 04:00:00,000", "2017-03-12 09:00:00,000"}, //
-            {"2017-03-12 05:00:00,000", "2017-03-12 10:00:00,000"}, //
-            {"2017-03-12 06:00:00,000", "2017-03-12 11:00:00,000"}, //
-            {"2017-03-12 07:00:00,000", "2017-03-12 12:00:00,000"}, //
-            {"2017-03-12 08:00:00,000", "2017-03-12 13:00:00,000"}, //
-            {"2017-03-12 09:00:00,000", "2017-03-12 14:00:00,000"}, //
-            {"2017-03-12 10:00:00,000", "2017-03-12 15:00:00,000"}, //
-            {"2017-03-12 11:00:00,000", "2017-03-12 16:00:00,000"}, //
-            {"2017-03-12 12:00:00,000", "2017-03-12 17:00:00,000"}, //
-            {"2017-03-12 13:00:00,000", "2017-03-12 18:00:00,000"}, //
-            {"2017-03-12 14:00:00,000", "2017-03-12 19:00:00,000"}, //
-            {"2017-03-12 15:00:00,000", "2017-03-12 20:00:00,000"}, //
-            {"2017-03-12 16:00:00,000", "2017-03-12 21:00:00,000"}, //
-            {"2017-03-12 17:00:00,000", "2017-03-12 22:00:00,000"}, //
-            {"2017-03-12 18:00:00,000", "2017-03-12 23:00:00,000"}, // 24
-            {"2017-03-12 19:00:00,000", "2017-03-13 00:00:00,000"}, //
-            {"2017-03-12 20:00:00,000", "2017-03-13 01:00:00,000"}, //
-            {"2017-03-12 21:00:00,000", "2017-03-13 02:00:00,000"}, //
-            {"2017-03-12 22:00:00,000", "2017-03-13 03:00:00,000"}, //
-            {"2017-03-12 23:00:00,000", "2017-03-13 04:00:00,000"}, //
-            {"2017-03-13 00:00:00,000", "2017-03-13 05:00:00,000"}, //
-            {"2017-03-13 01:00:00,000", "2017-03-13 06:00:00,000"}, //
-            {"2017-03-13 02:00:00,000", "2017-03-13 07:00:00,000"}, //
-            {"2017-03-13 03:00:00,000", "2017-03-13 08:00:00,000"}, //
-            {"2017-03-13 04:00:00,000", "2017-03-13 09:00:00,000"}, //
-            {"2017-03-13 05:00:00,000", "2017-03-13 10:00:00,000"}, //
-            {"2017-03-13 06:00:00,000", "2017-03-13 11:00:00,000"}, //
-        };
-
-        final TimeZone tz = TimeZone.getTimeZone("US/Central");
-        for (int i = 0; i < 36; i++) {
-            final Date date = calendar.getTime();
-            assertEquals("SimpleDateFormat TZ=US Central", expectedDstAndNoDst[i][0], usCentral.format(date));
-            assertEquals("SimpleDateFormat TZ=UTC", expectedDstAndNoDst[i][1], utc.format(date));
-            assertEquals(
-                    "FixedDateFormat TZ=US Central", expectedDstAndNoDst[i][0], fixedUsCentral.format(date.getTime()));
-            assertEquals("FixedDateFormat TZ=UTC", expectedDstAndNoDst[i][1], fixedUtc.format(date.getTime()));
-            calendar.add(Calendar.HOUR_OF_DAY, 1);
-        }
-    }
-
-    @Test
-    public void testDaylightSavingToWinterTime() throws Exception {
-        final Calendar calendar = Calendar.getInstance();
-        calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse("2017-11-05 00:00:00 UTC"));
-
-        final SimpleDateFormat usCentral = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS", Locale.US);
-        usCentral.setTimeZone(TimeZone.getTimeZone("US/Central"));
-
-        final SimpleDateFormat utc = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS", Locale.US);
-        utc.setTimeZone(TimeZone.getTimeZone("UTC"));
-
-        final FixedDateFormat fixedUsCentral = FixedDateFormat.create(DEFAULT, TimeZone.getTimeZone("US/Central"));
-        final FixedDateFormat fixedUtc = FixedDateFormat.create(DEFAULT, TimeZone.getTimeZone("UTC"));
-
-        final String[][] expectedDstAndNoDst = {
-            // US/Central, UTC
-            {"2017-11-04 19:00:00,000", "2017-11-05 00:00:00,000"}, //
-            {"2017-11-04 20:00:00,000", "2017-11-05 01:00:00,000"}, //
-            {"2017-11-04 21:00:00,000", "2017-11-05 02:00:00,000"}, //
-            {"2017-11-04 22:00:00,000", "2017-11-05 03:00:00,000"}, //
-            {"2017-11-04 23:00:00,000", "2017-11-05 04:00:00,000"}, //
-            {"2017-11-05 00:00:00,000", "2017-11-05 05:00:00,000"}, //
-            {"2017-11-05 01:00:00,000", "2017-11-05 06:00:00,000"}, //  DST jump at 2am US central time
-            {"2017-11-05 01:00:00,000", "2017-11-05 07:00:00,000"}, //
-            {"2017-11-05 02:00:00,000", "2017-11-05 08:00:00,000"}, //
-            {"2017-11-05 03:00:00,000", "2017-11-05 09:00:00,000"}, //
-            {"2017-11-05 04:00:00,000", "2017-11-05 10:00:00,000"}, //
-            {"2017-11-05 05:00:00,000", "2017-11-05 11:00:00,000"}, //
-            {"2017-11-05 06:00:00,000", "2017-11-05 12:00:00,000"}, //
-            {"2017-11-05 07:00:00,000", "2017-11-05 13:00:00,000"}, //
-            {"2017-11-05 08:00:00,000", "2017-11-05 14:00:00,000"}, //
-            {"2017-11-05 09:00:00,000", "2017-11-05 15:00:00,000"}, //
-            {"2017-11-05 10:00:00,000", "2017-11-05 16:00:00,000"}, //
-            {"2017-11-05 11:00:00,000", "2017-11-05 17:00:00,000"}, //
-            {"2017-11-05 12:00:00,000", "2017-11-05 18:00:00,000"}, //
-            {"2017-11-05 13:00:00,000", "2017-11-05 19:00:00,000"}, //
-            {"2017-11-05 14:00:00,000", "2017-11-05 20:00:00,000"}, //
-            {"2017-11-05 15:00:00,000", "2017-11-05 21:00:00,000"}, //
-            {"2017-11-05 16:00:00,000", "2017-11-05 22:00:00,000"}, //
-            {"2017-11-05 17:00:00,000", "2017-11-05 23:00:00,000"}, // 24
-            {"2017-11-05 18:00:00,000", "2017-11-06 00:00:00,000"}, //
-            {"2017-11-05 19:00:00,000", "2017-11-06 01:00:00,000"}, //
-            {"2017-11-05 20:00:00,000", "2017-11-06 02:00:00,000"}, //
-            {"2017-11-05 21:00:00,000", "2017-11-06 03:00:00,000"}, //
-            {"2017-11-05 22:00:00,000", "2017-11-06 04:00:00,000"}, //
-            {"2017-11-05 23:00:00,000", "2017-11-06 05:00:00,000"}, //
-            {"2017-11-06 00:00:00,000", "2017-11-06 06:00:00,000"}, //
-            {"2017-11-06 01:00:00,000", "2017-11-06 07:00:00,000"}, //
-            {"2017-11-06 02:00:00,000", "2017-11-06 08:00:00,000"}, //
-            {"2017-11-06 03:00:00,000", "2017-11-06 09:00:00,000"}, //
-            {"2017-11-06 04:00:00,000", "2017-11-06 10:00:00,000"}, //
-            {"2017-11-06 05:00:00,000", "2017-11-06 11:00:00,000"}, //
-        };
-
-        final TimeZone tz = TimeZone.getTimeZone("US/Central");
-        for (int i = 0; i < 36; i++) {
-            final Date date = calendar.getTime();
-            // System.out.println(usCentral.format(date) + ", Fixed: " + fixedUsCentral.format(date.getTime()) + ", utc:
-            // " + utc.format(date));
-            assertEquals("SimpleDateFormat TZ=US Central", expectedDstAndNoDst[i][0], usCentral.format(date));
-            assertEquals("SimpleDateFormat TZ=UTC", expectedDstAndNoDst[i][1], utc.format(date));
-            assertEquals(
-                    "FixedDateFormat TZ=US Central", expectedDstAndNoDst[i][0], fixedUsCentral.format(date.getTime()));
-            assertEquals("FixedDateFormat TZ=UTC", expectedDstAndNoDst[i][1], fixedUtc.format(date.getTime()));
-            calendar.add(Calendar.HOUR_OF_DAY, 1);
-        }
-    }
-    /**
-     * This test case validates date pattern before and after DST
-     * Base Date : 12 Mar 2017
-     * Daylight Savings started on : 02:00 AM
-     */
-    @Test
-    public void testFormatLong_goingBackInTime_DST() {
-        final Calendar instance = Calendar.getInstance(TimeZone.getTimeZone("EST"));
-        instance.set(2017, 2, 12, 2, 0);
-        final long now = instance.getTimeInMillis();
-        final long start = now - TimeUnit.HOURS.toMillis(1);
-        final long end = now + TimeUnit.HOURS.toMillis(1);
-
-        for (final FixedFormat format : FixedFormat.values()) {
-            final String pattern = format.getPattern();
-            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
-                continue; // cannot compile precise timestamp formats with SimpleDateFormat
-            }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(pattern, Locale.getDefault());
-            final FixedDateFormat customTF = new FixedDateFormat(format, TimeZone.getDefault());
-            for (long time = end; time > start; time -= 12345) {
-                final String actual = customTF.format(time);
-                final String expected = simpleDF.format(new Date(time));
-                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
-            }
-        }
-    }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatterTest.java
new file mode 100644
index 0000000000..3521d4d410
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/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.instant;
+
+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<Object[]> 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<Object[]> 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/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 0000000000..ddb8c31103
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
@@ -0,0 +1,424 @@
+/*
+ * 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.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.apache.logging.log4j.util.Constants;
+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<PatternSequence> expectedSequences) {
+        final List<PatternSequence> actualSequences = sequencePattern(pattern, thresholdPrecision);
+        assertThat(actualSequences).isEqualTo(expectedSequences);
+    }
+
+    static List<Arguments> sequencingTestCases() {
+        final List<Arguments> 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
+    @MethodSource("hourPrecisionPatterns")
+    void should_recognize_patterns_of_hour_precision(final String pattern) {
+        assertPatternPrecision(pattern, ChronoUnit.HOURS);
+    }
+
+    static List<String> hourPrecisionPatterns() {
+        final List<String> java8Patterns = new ArrayList<>(asList(
+                // Basics
+                "H",
+                "HH",
+                "a",
+                "h",
+                "K",
+                "k",
+                "H",
+                "Z",
+                "x",
+                "X",
+                "O",
+                "z",
+                "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"));
+        if (Constants.JAVA_MAJOR_VERSION > 8) {
+            java8Patterns.add("B");
+            java8Patterns.add("v");
+        }
+        return java8Patterns;
+    }
+
+    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<Arguments> 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<Arguments> 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();
+    }
+
+    @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-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
new file mode 100644
index 0000000000..b25fb85d74
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/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.instant;
+
+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<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter> 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<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter>)
+                        InstantPatternThreadLocalCachedFormatter::ofMilliPrecision,
+                "HH:mm.SSS",
+                locale1,
+                timeZone1
+            },
+            {
+                (Function<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter>)
+                        InstantPatternThreadLocalCachedFormatter::ofMilliPrecision,
+                "HH:mm.SSS",
+                locale2,
+                timeZone2
+            },
+            // For `ofSecondPrecision()`
+            {
+                (Function<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter>)
+                        InstantPatternThreadLocalCachedFormatter::ofSecondPrecision,
+                "yyyy",
+                locale1,
+                timeZone1
+            },
+            {
+                (Function<InstantPatternFormatter, InstantPatternThreadLocalCachedFormatter>)
+                        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-test/src/test/resources/log4j-rolling-size-with-time.xml b/log4j-core-test/src/test/resources/log4j-rolling-size-with-time.xml
index d109ef9581..64898bb1dd 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 @@
 
   <Appenders>
     <RollingFile name="RollingFile" fileName="${filename}"
-                 filePattern="target/rolling-size-test/rollingtest-%d{yyyy-MM-DD'T'HH-mm-ss-SSS}.log">
+                 filePattern="target/rolling-size-test/rollingtest-%d{yyyy-MM-dd'T'HH-mm-ss-SSS}.log">
       <PatternLayout>
         <Pattern>%m%n</Pattern>
       </PatternLayout>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/HtmlLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/HtmlLayout.java
index 6459e57cd9..9212a9bb8e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/HtmlLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/HtmlLayout.java
@@ -131,7 +131,7 @@ public final class HtmlLayout extends AbstractStringLayout {
         this.headerSize = headerSize;
         this.datePatternConverter = DEFAULT_DATE_PATTERN.equals(datePattern)
                 ? null
-                : DatePatternConverter.newInstance(configuration, new String[] {datePattern, timezone});
+                : DatePatternConverter.newInstance(new String[] {datePattern, timezone});
     }
 
     /**
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 09cc6c7ddd..d4a0bda04b 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,22 +16,24 @@
  */
 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.stream.Collectors;
 import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.time.Instant;
 import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedFormat;
-import org.apache.logging.log4j.kit.recycler.Recycler;
+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.plugins.Namespace;
 import org.apache.logging.log4j.plugins.Plugin;
 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.
@@ -40,147 +42,91 @@ import org.apache.logging.log4j.util.PerformanceSensitive;
 @Plugin("DatePatternConverter")
 @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();
 
-        abstract void formatToBuffer(final Instant instant, StringBuilder destination);
+    private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
 
-        public String toPattern() {
-            return null;
-        }
+    private final InstantFormatter formatter;
 
-        public TimeZone getTimeZone() {
-            return TimeZone.getDefault();
-        }
+    private DatePatternConverter(@Nullable final String[] options) {
+        super("Date", "date");
+        this.formatter = createFormatter(options);
     }
 
-    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
-        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);
-        }
-
-        @Override
-        public String toPattern() {
-            return fastDateFormat.getPattern();
-        }
-
-        @Override
-        public TimeZone getTimeZone() {
-            return fastDateFormat.getTimeZone();
+    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");
         }
+        return InstantPatternFormatter.newBuilder().setPattern(DEFAULT_PATTERN).build();
     }
 
-    private static final class FixedFormatter extends Formatter {
-        private final FixedDateFormat fixedDateFormat;
+    private static InstantFormatter createFormatterUnsafely(@Nullable final String[] options) {
 
-        // 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;
+        // Read options
+        final String pattern = readPattern(options);
+        final TimeZone timeZone = readTimeZone(options);
+        final Locale locale = readLocale(options);
 
-        FixedFormatter(final FixedDateFormat fixedDateFormat) {
-            this.fixedDateFormat = fixedDateFormat;
+        // Is it epoch seconds?
+        if ("UNIX".equals(pattern)) {
+            return InstantNumberFormatter.EPOCH_SECONDS_ROUNDED;
         }
 
-        @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);
+        // Is it epoch milliseconds?
+        if ("UNIX_MILLIS".equals(pattern)) {
+            return InstantNumberFormatter.EPOCH_MILLIS_ROUNDED;
         }
 
-        @Override
-        public String toPattern() {
-            return fixedDateFormat.getFormat();
-        }
-
-        @Override
-        public TimeZone getTimeZone() {
-            return fixedDateFormat.getTimeZone();
-        }
+        return InstantPatternFormatter.newBuilder()
+                .setPattern(pattern)
+                .setTimeZone(timeZone)
+                .setLocale(locale)
+                .build();
     }
 
-    private static final class UnixFormatter extends Formatter {
-
-        @Override
-        void formatToBuffer(final Instant instant, final StringBuilder destination) {
-            destination.append(instant.getEpochSecond()); // no need for caching
-        }
+    private static String readPattern(@Nullable final String[] options) {
+        return options != null && options.length > 0 && options[0] != null ? options[0] : DEFAULT_PATTERN;
     }
 
-    private static final class UnixMillisFormatter extends Formatter {
-
-        @Override
-        void formatToBuffer(final Instant instant, final StringBuilder destination) {
-            destination.append(instant.getEpochMillisecond()); // no need for caching
+    private static TimeZone readTimeZone(@Nullable final String[] options) {
+        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();
     }
 
-    /**
-     * 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 pattern;
-
-    private final TimeZone timeZone;
-
-    private final Recycler<MutableInstant> mutableInstantRecycler;
-
-    private final Recycler<Formatter> formatterRecycler;
-
-    /**
-     * Private constructor.
-     *
-     * @param options options, may be null.
-     */
-    private DatePatternConverter(final Configuration configuration, final String[] options) {
-        super("Date", "date");
-        final String[] safeOptions = options == null ? null : Arrays.copyOf(options, options.length);
-        this.mutableInstantRecycler = configuration.getRecyclerFactory().create(MutableInstant::new);
-        this.formatterRecycler = configuration.getRecyclerFactory().create(() -> createFormatter(safeOptions));
-        final Formatter formatter = formatterRecycler.acquire();
+    private static Locale readLocale(@Nullable final String[] options) {
         try {
-            this.pattern = formatter.toPattern();
-            this.timeZone = formatter.getTimeZone();
-        } finally {
-            formatterRecycler.release(formatter);
+            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 Formatter createFormatter(final String[] options) {
-        final FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(options);
-        if (fixedDateFormat != null) {
-            return createFixedFormatter(fixedDateFormat);
+    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);
         }
-        return createNonFixedFormatter(options);
     }
 
     /**
@@ -189,127 +135,70 @@ public final class DatePatternConverter extends LogEventPatternConverter impleme
      * @param options options, may be null.
      * @return instance of pattern converter.
      */
-    public static DatePatternConverter newInstance(final Configuration configuration, final String[] options) {
-        return new DatePatternConverter(configuration, options);
+    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.
-     *
-     * @param date date
-     * @param toAppendTo buffer to which formatted date is appended.
-     */
-    void format(final Date date, final StringBuilder toAppendTo) {
-        format(date.getTime(), toAppendTo);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
     @Override
-    public void format(final LogEvent event, final StringBuilder output) {
-        format(event.getInstant(), output);
-    }
-
-    private void format(final long epochMilli, final StringBuilder output) {
-        final MutableInstant instant = mutableInstantRecycler.acquire();
-        try {
-            instant.initFromEpochMilli(epochMilli, 0);
-            format(instant, output);
-        } finally {
-            mutableInstantRecycler.release(instant);
-        }
+    public void format(final LogEvent event, final StringBuilder buffer) {
+        formatter.formatTo(buffer, event.getInstant());
     }
 
-    void format(final Instant instant, final StringBuilder output) {
-        final Formatter formatter = formatterRecycler.acquire();
-        try {
-            formatter.formatToBuffer(instant, output);
-        } finally {
-            formatterRecycler.release(formatter);
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
     @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 logEvent) {
+            format(logEvent, buffer);
+        } else if (object instanceof Date date) {
+            final MutableInstant instant = new MutableInstant();
+            instant.initFromEpochMilli(date.getTime(), 0);
+            formatter.formatTo(buffer, instant);
+        } else if (object instanceof Instant instant) {
+            formatter.formatTo(buffer, instant);
+        } else if (object instanceof Long epochMillis) {
+            final MutableInstant instant = new MutableInstant();
+            instant.initFromEpochMilli(epochMillis, 0);
+            formatter.formatTo(buffer, instant);
+        }
+        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 date) {
+                    format(date, buffer);
+                    break;
+                }
             }
         }
     }
 
     /**
-     * Gets the pattern string describing this date format.
-     *
-     * @return 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.
      */
+    @Nullable
     public String getPattern() {
-        return pattern;
+        return (formatter instanceof InstantPatternFormatter)
+                ? ((InstantPatternFormatter) formatter).getPattern()
+                : null;
     }
 
     /**
-     * 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
      */
+    @Nullable
     public TimeZone getTimeZone() {
-        return timeZone;
+        return (formatter instanceof InstantPatternFormatter)
+                ? ((InstantPatternFormatter) formatter).getTimeZone()
+                : null;
     }
 }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FileDatePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FileDatePatternConverter.java
index 0e2fbd3efd..4bdda72055 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FileDatePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/FileDatePatternConverter.java
@@ -17,7 +17,6 @@
 package org.apache.logging.log4j.core.pattern;
 
 import java.util.TimeZone;
-import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.plugins.Namespace;
 import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.util.PerformanceSensitive;
@@ -38,8 +37,8 @@ public final class FileDatePatternConverter implements ArrayPatternConverter {
     /**
      * Private constructor.
      */
-    private FileDatePatternConverter(final Configuration configuration, final String... options) {
-        delegate = DatePatternConverter.newInstance(configuration, options);
+    private FileDatePatternConverter(final String... options) {
+        delegate = DatePatternConverter.newInstance(options);
     }
 
     /**
@@ -48,12 +47,12 @@ public final class FileDatePatternConverter implements ArrayPatternConverter {
      * @param options options, may be null.
      * @return instance of pattern converter.
      */
-    public static FileDatePatternConverter newInstance(final Configuration configuration, final String[] options) {
+    public static FileDatePatternConverter newInstance(final String[] options) {
         if (options == null || options.length == 0) {
-            return new FileDatePatternConverter(configuration, "yyyy-MM-dd");
+            return new FileDatePatternConverter("yyyy-MM-dd");
         }
 
-        return new FileDatePatternConverter(configuration, options);
+        return new FileDatePatternConverter(options);
     }
 
     @Override
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/InstantFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/InstantFormatter.java
deleted file mode 100644
index 02544f1d71..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/InstantFormatter.java
+++ /dev/null
@@ -1,317 +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.time;
-
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.TimeZone;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-import org.apache.logging.log4j.status.StatusLogger;
-import org.apache.logging.log4j.util.Strings;
-
-/**
- * A composite {@link Instant} formatter trying to employ either {@link FixedDateFormat}, {@link FastDateFormat}, or {@link DateTimeFormatter} in the given order due to performance reasons.
- * <p>
- * Note that {@code FixedDateFormat} and {@code FastDateFormat} only support millisecond precision.
- * If the pattern asks for a higher precision, {@code DateTimeFormatter} will be employed, which is significantly slower.
- * </p>
- */
-public final class InstantFormatter {
-
-    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 Formatter formatter;
-
-    private InstantFormatter(final Builder builder) {
-        this.formatter = Arrays.stream(FORMATTER_FACTORIES)
-                .map(formatterFactory -> {
-                    try {
-                        return formatterFactory.createIfSupported(
-                                builder.getPattern(), builder.getLocale(), builder.getTimeZone());
-                    } 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"));
-    }
-
-    public String format(final Instant instant) {
-        Objects.requireNonNull(instant, "instant");
-        final StringBuilder stringBuilder = new StringBuilder();
-        formatter.format(instant, stringBuilder);
-        return stringBuilder.toString();
-    }
-
-    public void format(final Instant instant, final StringBuilder stringBuilder) {
-        Objects.requireNonNull(instant, "instant");
-        Objects.requireNonNull(stringBuilder, "stringBuilder");
-        formatter.format(instant, stringBuilder);
-    }
-
-    /**
-     * Checks if the given {@link Instant}s are equal from the point of view of the employed formatter.
-     * <p>
-     * This method should be preferred over {@link Object#equals(Object)}.
-     * For instance, {@link FixedDateFormat} and {@link FastDateFormat} discard nanoseconds, hence, from their point of view, two different {@code Instant}s are equal if they match up to millisecond precision.
-     * </p>
-     */
-    public boolean isInstantMatching(final Instant instant1, final Instant instant2) {
-        return formatter.isInstantMatching(instant1, instant2);
-    }
-
-    public Class<?> getInternalImplementationClass() {
-        return formatter.getInternalImplementationClass();
-    }
-
-    public static Builder newBuilder() {
-        return new Builder();
-    }
-
-    public static final class Builder {
-
-        private String pattern;
-
-        private Locale locale = Locale.getDefault();
-
-        private TimeZone timeZone = TimeZone.getDefault();
-
-        private Builder() {}
-
-        public String getPattern() {
-            return pattern;
-        }
-
-        public Builder setPattern(final String pattern) {
-            this.pattern = pattern;
-            return this;
-        }
-
-        public Locale getLocale() {
-            return locale;
-        }
-
-        public Builder setLocale(final Locale locale) {
-            this.locale = locale;
-            return this;
-        }
-
-        public TimeZone getTimeZone() {
-            return timeZone;
-        }
-
-        public Builder setTimeZone(final TimeZone timeZone) {
-            this.timeZone = timeZone;
-            return this;
-        }
-
-        public InstantFormatter build() {
-            validate();
-            return new InstantFormatter(this);
-        }
-
-        private void validate() {
-            if (Strings.isBlank(pattern)) {
-                throw new IllegalArgumentException("blank pattern");
-            }
-            Objects.requireNonNull(locale, "locale");
-            Objects.requireNonNull(timeZone, "timeZone");
-        }
-    }
-
-    private interface FormatterFactory {
-
-        Formatter createIfSupported(String pattern, Locale locale, TimeZone timeZone);
-    }
-
-    private interface Formatter {
-
-        Class<?> getInternalImplementationClass();
-
-        void format(Instant instant, StringBuilder stringBuilder);
-
-        boolean isInstantMatching(Instant instant1, Instant instant2);
-    }
-
-    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 implements Formatter {
-
-        private final DateTimeFormatter formatter;
-
-        private final MutableInstant mutableInstant;
-
-        private JavaDateTimeFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
-            this.formatter =
-                    DateTimeFormatter.ofPattern(pattern).withLocale(locale).withZone(timeZone.toZoneId());
-            this.mutableInstant = new MutableInstant();
-        }
-
-        @Override
-        public Class<?> getInternalImplementationClass() {
-            return DateTimeFormatter.class;
-        }
-
-        @Override
-        public void format(final Instant instant, final StringBuilder stringBuilder) {
-            if (instant instanceof MutableInstant) {
-                formatMutableInstant((MutableInstant) instant, stringBuilder);
-            } else {
-                formatInstant(instant, stringBuilder);
-            }
-        }
-
-        private void formatMutableInstant(final MutableInstant instant, final StringBuilder stringBuilder) {
-            formatter.formatTo(instant, stringBuilder);
-        }
-
-        private void formatInstant(final Instant instant, final StringBuilder stringBuilder) {
-            mutableInstant.initFrom(instant);
-            formatMutableInstant(mutableInstant, stringBuilder);
-        }
-
-        @Override
-        public boolean isInstantMatching(final Instant instant1, final Instant instant2) {
-            return instant1.getEpochSecond() == instant2.getEpochSecond()
-                    && instant1.getNanoOfSecond() == instant2.getNanoOfSecond();
-        }
-    }
-
-    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 implements Formatter {
-
-        private final FastDateFormat formatter;
-
-        private final Calendar calendar;
-
-        private Log4jFastFormatter(final String pattern, final Locale locale, final TimeZone timeZone) {
-            this.formatter = FastDateFormat.getInstance(pattern, timeZone, locale);
-            this.calendar = Calendar.getInstance(timeZone, locale);
-        }
-
-        @Override
-        public Class<?> getInternalImplementationClass() {
-            return FastDateFormat.class;
-        }
-
-        @Override
-        public void format(final Instant instant, final StringBuilder stringBuilder) {
-            calendar.setTimeInMillis(instant.getEpochMillisecond());
-            formatter.format(calendar, stringBuilder);
-        }
-
-        @Override
-        public boolean isInstantMatching(final Instant instant1, final Instant instant2) {
-            return instant1.getEpochMillisecond() == instant2.getEpochMillisecond();
-        }
-    }
-
-    private static final class Log4jFixedFormatterFactory implements FormatterFactory {
-
-        @Override
-        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 implements Formatter {
-
-        private final FixedDateFormat formatter;
-
-        private final char[] buffer;
-
-        private Log4jFixedFormatter(final FixedDateFormat formatter) {
-            this.formatter = formatter;
-            this.buffer = new char[formatter.getFormat().length()];
-        }
-
-        @Override
-        public Class<?> getInternalImplementationClass() {
-            return FixedDateFormat.class;
-        }
-
-        @Override
-        public void format(final Instant instant, final StringBuilder stringBuilder) {
-            final int length = formatter.formatInstant(instant, buffer, 0);
-            stringBuilder.append(buffer, 0, length);
-        }
-
-        @Override
-        public boolean isInstantMatching(final Instant instant1, final Instant instant2) {
-            return formatter.isEquivalent(
-                    instant1.getEpochSecond(),
-                    instant1.getNanoOfSecond(),
-                    instant2.getEpochSecond(),
-                    instant2.getNanoOfSecond());
-        }
-    }
-
-    /**
-     * 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
-                1621280470,
-                // Using the highest nanosecond precision possible to differentiate formatters only supporting
-                // millisecond precision.
-                123_456_789);
-        final String expectedFormat = javaFormatter.format(instant);
-        final StringBuilder stringBuilder = new StringBuilder();
-        formatter.format(instant, stringBuilder);
-        final String actualFormat = stringBuilder.toString();
-        return expectedFormat.equals(actualFormat);
-    }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/DatePrinter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/DatePrinter.java
deleted file mode 100644
index 7cebb78f60..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/DatePrinter.java
+++ /dev/null
@@ -1,144 +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.time.internal.format;
-
-import java.text.FieldPosition;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * DatePrinter is the "missing" interface for the format methods of
- * {@link java.text.DateFormat}. You can obtain an object implementing this
- * interface by using one of the FastDateFormat factory methods.
- * <p>
- * Warning: Since binary compatible methods may be added to this interface in any
- * release, developers are not expected to implement this interface.
- * </p>
- *
- * <p>
- * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
- * </p>
- *
- * @since Apache Commons Lang 3.2
- */
-public interface DatePrinter {
-
-    /**
-     * <p>Formats a millisecond {@code long} value.</p>
-     *
-     * @param millis  the millisecond value to format
-     * @return the formatted string
-     * @since 2.1
-     */
-    String format(long millis);
-
-    /**
-     * <p>Formats a {@code Date} object using a {@code GregorianCalendar}.</p>
-     *
-     * @param date  the date to format
-     * @return the formatted string
-     */
-    String format(Date date);
-
-    /**
-     * <p>Formats a {@code Calendar} object.</p>
-     * The TimeZone set on the Calendar is only used to adjust the time offset.
-     * The TimeZone specified during the construction of the Parser will determine the TimeZone
-     * used in the formatted string.
-     *
-     * @param calendar  the calendar to format.
-     * @return the formatted string
-     */
-    String format(Calendar calendar);
-
-    /**
-     * <p>Formats a millisecond {@code long} value into the
-     * supplied {@code Appendable}.</p>
-     *
-     * @param millis  the millisecond value to format
-     * @param buf  the buffer to format into
-     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    <B extends Appendable> B format(long millis, B buf);
-
-    /**
-     * <p>Formats a {@code Date} object into the
-     * supplied {@code Appendable} using a {@code GregorianCalendar}.</p>
-     *
-     * @param date  the date to format
-     * @param buf  the buffer to format into
-     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    <B extends Appendable> B format(Date date, B buf);
-
-    /**
-     * <p>Formats a {@code Calendar} object into the supplied {@code Appendable}.</p>
-     * The TimeZone set on the Calendar is only used to adjust the time offset.
-     * The TimeZone specified during the construction of the Parser will determine the TimeZone
-     * used in the formatted string.
-     *
-     * @param calendar  the calendar to format
-     * @param buf  the buffer to format into
-     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    <B extends Appendable> B format(Calendar calendar, B buf);
-
-    // Accessors
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Gets the pattern used by this printer.</p>
-     *
-     * @return the pattern, {@link java.text.SimpleDateFormat} compatible
-     */
-    String getPattern();
-
-    /**
-     * <p>Gets the time zone used by this printer.</p>
-     *
-     * <p>This zone is always used for {@code Date} printing. </p>
-     *
-     * @return the time zone
-     */
-    TimeZone getTimeZone();
-
-    /**
-     * <p>Gets the locale used by this printer.</p>
-     *
-     * @return the locale
-     */
-    Locale getLocale();
-
-    /**
-     * <p>Formats a {@code Date}, {@code Calendar} or
-     * {@code Long} (milliseconds) object.</p>
-     *
-     * @param obj  the object to format
-     * @param toAppendTo  the buffer to append to
-     * @param pos  the position - ignored
-     * @return the buffer passed in
-     * @see java.text.DateFormat#format(Object, StringBuffer, FieldPosition)
-     */
-    StringBuilder format(Object obj, StringBuilder toAppendTo, FieldPosition pos);
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDateFormat.java
deleted file mode 100644
index b20046ea06..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDateFormat.java
+++ /dev/null
@@ -1,588 +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.time.internal.format;
-
-import java.text.DateFormat;
-import java.text.FieldPosition;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * <p>FastDateFormat is a fast and thread-safe version of
- * {@link java.text.SimpleDateFormat}.</p>
- *
- * <p>To obtain an instance of FastDateFormat, use one of the static factory methods:
- * {@link #getInstance(String, TimeZone, Locale)}, {@link #getDateInstance(int, TimeZone, Locale)},
- * {@link #getTimeInstance(int, TimeZone, Locale)}, or {@link #getDateTimeInstance(int, int, TimeZone, Locale)}
- * </p>
- *
- * <p>Since FastDateFormat is thread safe, you can use a static member instance:</p>
- * <code>
- *   private static final FastDateFormat DATE_FORMATTER = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.SHORT);
- * </code>
- *
- * <p>This class can be used as a direct replacement to
- * {@code SimpleDateFormat} in most formatting and parsing situations.
- * This class is especially useful in multi-threaded server environments.
- * {@code SimpleDateFormat} is not thread-safe in any JDK version,
- * nor will it be as Sun have closed the bug/RFE.
- * </p>
- *
- * <p>All patterns are compatible with
- * SimpleDateFormat (except time zones and some year patterns - see below).</p>
- *
- * <p>Since 3.2, FastDateFormat supports parsing as well as printing.</p>
- *
- * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
- * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
- * This pattern letter can be used here (on all JDK versions).</p>
- *
- * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
- * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}).
- * This introduces a minor incompatibility with Java 1.4, but at a gain of
- * useful functionality.</p>
- *
- * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
- * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
- * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
- * 'YYY' will be formatted as '2003', while it was '03' in former Java
- * versions. FastDateFormat implements the behavior of Java 7.</p>
- *
- * <p>
- * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
- * </p>
- *
- * @since Apache Commons Lang 2.0
- */
-public class FastDateFormat extends Format implements DatePrinter {
-
-    /**
-     * FULL locale dependent date or time style.
-     */
-    public static final int FULL = DateFormat.FULL;
-
-    /**
-     * LONG locale dependent date or time style.
-     */
-    public static final int LONG = DateFormat.LONG;
-
-    /**
-     * MEDIUM locale dependent date or time style.
-     */
-    public static final int MEDIUM = DateFormat.MEDIUM;
-
-    /**
-     * SHORT locale dependent date or time style.
-     */
-    public static final int SHORT = DateFormat.SHORT;
-
-    private static final FormatCache<FastDateFormat> cache = new FormatCache<FastDateFormat>() {
-        @Override
-        protected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
-            return new FastDateFormat(pattern, timeZone, locale);
-        }
-    };
-
-    private final FastDatePrinter printer;
-
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Gets a formatter instance using the default pattern in the
-     * default locale.</p>
-     *
-     * @return a date/time formatter
-     */
-    public static FastDateFormat getInstance() {
-        return cache.getInstance();
-    }
-
-    /**
-     * <p>Gets a formatter instance using the specified pattern in the
-     * default locale.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible
-     *  pattern
-     * @return a pattern based date/time formatter
-     * @throws IllegalArgumentException if pattern is invalid
-     */
-    public static FastDateFormat getInstance(final String pattern) {
-        return cache.getInstance(pattern, null, null);
-    }
-
-    /**
-     * <p>Gets a formatter instance using the specified pattern and
-     * time zone.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible
-     *  pattern
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date
-     * @return a pattern based date/time formatter
-     * @throws IllegalArgumentException if pattern is invalid
-     */
-    public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone) {
-        return cache.getInstance(pattern, timeZone, null);
-    }
-
-    /**
-     * <p>Gets a formatter instance using the specified pattern and
-     * locale.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible
-     *  pattern
-     * @param locale  optional locale, overrides system locale
-     * @return a pattern based date/time formatter
-     * @throws IllegalArgumentException if pattern is invalid
-     */
-    public static FastDateFormat getInstance(final String pattern, final Locale locale) {
-        return cache.getInstance(pattern, null, locale);
-    }
-
-    /**
-     * <p>Gets a formatter instance using the specified pattern, time zone
-     * and locale.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible
-     *  pattern
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date
-     * @param locale  optional locale, overrides system locale
-     * @return a pattern based date/time formatter
-     * @throws IllegalArgumentException if pattern is invalid
-     *  or {@code null}
-     */
-    public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
-        return cache.getInstance(pattern, timeZone, locale);
-    }
-
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Gets a date formatter instance using the specified style in the
-     * default time zone and locale.</p>
-     *
-     * @param style  date style: FULL, LONG, MEDIUM, or SHORT
-     * @return a localized standard date formatter
-     * @throws IllegalArgumentException if the Locale has no date
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getDateInstance(final int style) {
-        return cache.getDateInstance(style, null, null);
-    }
-
-    /**
-     * <p>Gets a date formatter instance using the specified style and
-     * locale in the default time zone.</p>
-     *
-     * @param style  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard date formatter
-     * @throws IllegalArgumentException if the Locale has no date
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getDateInstance(final int style, final Locale locale) {
-        return cache.getDateInstance(style, null, locale);
-    }
-
-    /**
-     * <p>Gets a date formatter instance using the specified style and
-     * time zone in the default locale.</p>
-     *
-     * @param style  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date
-     * @return a localized standard date formatter
-     * @throws IllegalArgumentException if the Locale has no date
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) {
-        return cache.getDateInstance(style, timeZone, null);
-    }
-
-    /**
-     * <p>Gets a date formatter instance using the specified style, time
-     * zone and locale.</p>
-     *
-     * @param style  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard date formatter
-     * @throws IllegalArgumentException if the Locale has no date
-     *  pattern defined
-     */
-    public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone, final Locale locale) {
-        return cache.getDateInstance(style, timeZone, locale);
-    }
-
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Gets a time formatter instance using the specified style in the
-     * default time zone and locale.</p>
-     *
-     * @param style  time style: FULL, LONG, MEDIUM, or SHORT
-     * @return a localized standard time formatter
-     * @throws IllegalArgumentException if the Locale has no time
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getTimeInstance(final int style) {
-        return cache.getTimeInstance(style, null, null);
-    }
-
-    /**
-     * <p>Gets a time formatter instance using the specified style and
-     * locale in the default time zone.</p>
-     *
-     * @param style  time style: FULL, LONG, MEDIUM, or SHORT
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard time formatter
-     * @throws IllegalArgumentException if the Locale has no time
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getTimeInstance(final int style, final Locale locale) {
-        return cache.getTimeInstance(style, null, locale);
-    }
-
-    /**
-     * <p>Gets a time formatter instance using the specified style and
-     * time zone in the default locale.</p>
-     *
-     * @param style  time style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted time
-     * @return a localized standard time formatter
-     * @throws IllegalArgumentException if the Locale has no time
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone) {
-        return cache.getTimeInstance(style, timeZone, null);
-    }
-
-    /**
-     * <p>Gets a time formatter instance using the specified style, time
-     * zone and locale.</p>
-     *
-     * @param style  time style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted time
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard time formatter
-     * @throws IllegalArgumentException if the Locale has no time
-     *  pattern defined
-     */
-    public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone, final Locale locale) {
-        return cache.getTimeInstance(style, timeZone, locale);
-    }
-
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Gets a date/time formatter instance using the specified style
-     * in the default time zone and locale.</p>
-     *
-     * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
-     * @return a localized standard date/time formatter
-     * @throws IllegalArgumentException if the Locale has no date/time
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle) {
-        return cache.getDateTimeInstance(dateStyle, timeStyle, null, null);
-    }
-
-    /**
-     * <p>Gets a date/time formatter instance using the specified style and
-     * locale in the default time zone.</p>
-     *
-     * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard date/time formatter
-     * @throws IllegalArgumentException if the Locale has no date/time
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final Locale locale) {
-        return cache.getDateTimeInstance(dateStyle, timeStyle, null, locale);
-    }
-
-    /**
-     * <p>Gets a date/time formatter instance using the specified style and
-     * time zone in the default locale.</p>
-     *
-     * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date
-     * @return a localized standard date/time formatter
-     * @throws IllegalArgumentException if the Locale has no date/time
-     *  pattern defined
-     * @since 2.1
-     */
-    public static FastDateFormat getDateTimeInstance(
-            final int dateStyle, final int timeStyle, final TimeZone timeZone) {
-        return getDateTimeInstance(dateStyle, timeStyle, timeZone, null);
-    }
-    /**
-     * <p>Gets a date/time formatter instance using the specified style,
-     * time zone and locale.</p>
-     *
-     * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard date/time formatter
-     * @throws IllegalArgumentException if the Locale has no date/time
-     *  pattern defined
-     */
-    public static FastDateFormat getDateTimeInstance(
-            final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
-        return cache.getDateTimeInstance(dateStyle, timeStyle, timeZone, locale);
-    }
-
-    /**
-     * <p>Constructs a new FastDateFormat.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
-     * @param timeZone  non-null time zone to use
-     * @param locale  non-null locale to use
-     * @param centuryStart The start of the 100 year period to use as the "default century" for 2 digit year parsing.  If centuryStart is null, defaults to now - 80 years
-     * @throws NullPointerException if pattern, timeZone, or locale is null.
-     * @since 3.0
-     */
-    public static FastDateFormat getDateTimeInstance(
-            final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
-        return new FastDateFormat(pattern, timeZone, locale, centuryStart);
-    }
-
-    // Constructor
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Constructs a new FastDateFormat.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
-     * @param timeZone  non-null time zone to use
-     * @param locale  non-null locale to use
-     * @throws NullPointerException if pattern, timeZone, or locale is null.
-     */
-    protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) {
-        this(pattern, timeZone, locale, null);
-    }
-
-    // Constructor
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Constructs a new FastDateFormat.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
-     * @param timeZone  non-null time zone to use
-     * @param locale  non-null locale to use
-     * @param centuryStart The start of the 100 year period to use as the "default century" for 2 digit year parsing.  If centuryStart is null, defaults to now - 80 years
-     * @throws NullPointerException if pattern, timeZone, or locale is null.
-     */
-    protected FastDateFormat(
-            final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
-        printer = new FastDatePrinter(pattern, timeZone, locale);
-    }
-
-    // Format methods
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Formats a {@code Date}, {@code Calendar} or
-     * {@code Long} (milliseconds) object.</p>
-     * This method is an implementation of {@link Format#format(Object, StringBuilder, FieldPosition)}
-     *
-     * @param obj  the object to format
-     * @param toAppendTo  the buffer to append to
-     * @param pos  the position - ignored
-     * @return the buffer passed in
-     */
-    @Override
-    public StringBuilder format(final Object obj, final StringBuilder toAppendTo, final FieldPosition pos) {
-        return toAppendTo.append(printer.format(obj));
-    }
-
-    /**
-     * <p>Formats a millisecond {@code long} value.</p>
-     *
-     * @param millis  the millisecond value to format
-     * @return the formatted string
-     * @since 2.1
-     */
-    @Override
-    public String format(final long millis) {
-        return printer.format(millis);
-    }
-
-    /**
-     * <p>Formats a {@code Date} object using a {@code GregorianCalendar}.</p>
-     *
-     * @param date  the date to format
-     * @return the formatted string
-     */
-    @Override
-    public String format(final Date date) {
-        return printer.format(date);
-    }
-
-    /**
-     * <p>Formats a {@code Calendar} object.</p>
-     *
-     * @param calendar  the calendar to format
-     * @return the formatted string
-     */
-    @Override
-    public String format(final Calendar calendar) {
-        return printer.format(calendar);
-    }
-
-    /**
-     * <p>Formats a millisecond {@code long} value into the
-     * supplied {@code StringBuffer}.</p>
-     *
-     * @param millis  the millisecond value to format
-     * @param buf  the buffer to format into
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    @Override
-    public <B extends Appendable> B format(final long millis, final B buf) {
-        return printer.format(millis, buf);
-    }
-
-    /**
-     * <p>Formats a {@code Date} object into the
-     * supplied {@code StringBuffer} using a {@code GregorianCalendar}.</p>
-     *
-     * @param date  the date to format
-     * @param buf  the buffer to format into
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    @Override
-    public <B extends Appendable> B format(final Date date, final B buf) {
-        return printer.format(date, buf);
-    }
-
-    /**
-     * <p>Formats a {@code Calendar} object into the
-     * supplied {@code StringBuffer}.</p>
-     *
-     * @param calendar  the calendar to format
-     * @param buf  the buffer to format into
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    @Override
-    public <B extends Appendable> B format(final Calendar calendar, final B buf) {
-        return printer.format(calendar, buf);
-    }
-
-    // Accessors
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Gets the pattern used by this formatter.</p>
-     *
-     * @return the pattern, {@link java.text.SimpleDateFormat} compatible
-     */
-    @Override
-    public String getPattern() {
-        return printer.getPattern();
-    }
-
-    /**
-     * <p>Gets the time zone used by this formatter.</p>
-     *
-     * <p>This zone is always used for {@code Date} formatting. </p>
-     *
-     * @return the time zone
-     */
-    @Override
-    public TimeZone getTimeZone() {
-        return printer.getTimeZone();
-    }
-
-    /**
-     * <p>Gets the locale used by this formatter.</p>
-     *
-     * @return the locale
-     */
-    @Override
-    public Locale getLocale() {
-        return printer.getLocale();
-    }
-
-    /**
-     * <p>Gets an estimate for the maximum string length that the
-     * formatter will produce.</p>
-     *
-     * <p>The actual formatted length will almost always be less than or
-     * equal to this amount.</p>
-     *
-     * @return the maximum formatted length
-     */
-    public int getMaxLengthEstimate() {
-        return printer.getMaxLengthEstimate();
-    }
-
-    // Basics
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Compares two objects for equality.</p>
-     *
-     * @param obj  the object to compare to
-     * @return {@code true} if equal
-     */
-    @Override
-    public boolean equals(final Object obj) {
-        if (obj instanceof FastDateFormat == false) {
-            return false;
-        }
-        final FastDateFormat other = (FastDateFormat) obj;
-        // no need to check parser, as it has same invariants as printer
-        return printer.equals(other.printer);
-    }
-
-    /**
-     * <p>Returns a hash code compatible with equals.</p>
-     *
-     * @return a hash code compatible with equals
-     */
-    @Override
-    public int hashCode() {
-        return printer.hashCode();
-    }
-
-    /**
-     * <p>Gets a debugging string version of this formatter.</p>
-     *
-     * @return a debugging string
-     */
-    @Override
-    public String toString() {
-        return "FastDateFormat[" + printer.getPattern() + "," + printer.getLocale() + ","
-                + printer.getTimeZone().getID() + "]";
-    }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDatePrinter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDatePrinter.java
deleted file mode 100644
index dff8e4a870..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FastDatePrinter.java
+++ /dev/null
@@ -1,1503 +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.time.internal.format;
-
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.DateFormatSymbols;
-import java.text.FieldPosition;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import org.apache.logging.log4j.core.util.Throwables;
-
-/**
- * <p>FastDatePrinter is a fast and thread-safe version of
- * {@link java.text.SimpleDateFormat}.</p>
- *
- * <p>To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}
- * or another variation of the factory methods of {@link FastDateFormat}.</p>
- *
- * <p>Since FastDatePrinter is thread safe, you can use a static member instance:</p>
- * <code>
- *     private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd");
- * </code>
- *
- * <p>This class can be used as a direct replacement to
- * {@code SimpleDateFormat} in most formatting situations.
- * This class is especially useful in multi-threaded server environments.
- * {@code SimpleDateFormat} is not thread-safe in any JDK version,
- * nor will it be as Sun have closed the bug/RFE.
- * </p>
- *
- * <p>Only formatting is supported by this class, but all patterns are compatible with
- * SimpleDateFormat (except time zones and some year patterns - see below).</p>
- *
- * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
- * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
- * This pattern letter can be used here (on all JDK versions).</p>
- *
- * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
- * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}).
- * This introduces a minor incompatibility with Java 1.4, but at a gain of
- * useful functionality.</p>
- *
- * <p>Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}.
- * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using
- * one of the {@code 'X'} formats is recommended.
- *
- * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
- * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
- * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
- * 'YYY' will be formatted as '2003', while it was '03' in former Java
- * versions. FastDatePrinter implements the behavior of Java 7.</p>
- *
- * <p>
- * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
- * </p>
- *
- * @since Apache Commons Lang 3.2
- */
-public class FastDatePrinter implements DatePrinter {
-    // A lot of the speed in this class comes from caching, but some comes
-    // from the special int to StringBuffer conversion.
-    //
-    // The following produces a padded 2 digit number:
-    //   buffer.append((char)(value / 10 + '0'));
-    //   buffer.append((char)(value % 10 + '0'));
-    //
-    // Note that the fastest append to StringBuffer is a single char (used here).
-    // Note that Integer.toString() is not called, the conversion is simply
-    // taking the value and adding (mathematically) the ASCII value for '0'.
-    // So, don't change this code! It works and is very fast.
-
-    /**
-     * FULL locale dependent date or time style.
-     */
-    public static final int FULL = DateFormat.FULL;
-    /**
-     * LONG locale dependent date or time style.
-     */
-    public static final int LONG = DateFormat.LONG;
-    /**
-     * MEDIUM locale dependent date or time style.
-     */
-    public static final int MEDIUM = DateFormat.MEDIUM;
-    /**
-     * SHORT locale dependent date or time style.
-     */
-    public static final int SHORT = DateFormat.SHORT;
-
-    /**
-     * The pattern.
-     */
-    private final String mPattern;
-    /**
-     * The time zone.
-     */
-    private final TimeZone mTimeZone;
-    /**
-     * The locale.
-     */
-    private final Locale mLocale;
-    /**
-     * The parsed rules.
-     */
-    private transient Rule[] mRules;
-    /**
-     * The estimated maximum length.
-     */
-    private transient int mMaxLengthEstimate;
-
-    // Constructor
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Constructs a new FastDatePrinter.</p>
-     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}  or another variation of the
-     * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance.
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
-     * @param timeZone  non-null time zone to use
-     * @param locale  non-null locale to use
-     * @throws NullPointerException if pattern, timeZone, or locale is null.
-     */
-    protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) {
-        mPattern = pattern;
-        mTimeZone = timeZone;
-        mLocale = locale;
-
-        init();
-    }
-
-    /**
-     * <p>Initializes the instance for first use.</p>
-     */
-    private void init() {
-        final List<Rule> rulesList = parsePattern();
-        mRules = rulesList.toArray(Rule.EMPTY_ARRAY);
-
-        int len = 0;
-        for (int i = mRules.length; --i >= 0; ) {
-            len += mRules[i].estimateLength();
-        }
-
-        mMaxLengthEstimate = len;
-    }
-
-    // Parse the pattern
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Returns a list of Rules given a pattern.</p>
-     *
-     * @return a {@code List} of Rule objects
-     * @throws IllegalArgumentException if pattern is invalid
-     */
-    protected List<Rule> parsePattern() {
-        final DateFormatSymbols symbols = new DateFormatSymbols(mLocale);
-        final List<Rule> rules = new ArrayList<>();
-
-        final String[] ERAs = symbols.getEras();
-        final String[] months = symbols.getMonths();
-        final String[] shortMonths = symbols.getShortMonths();
-        final String[] weekdays = symbols.getWeekdays();
-        final String[] shortWeekdays = symbols.getShortWeekdays();
-        final String[] AmPmStrings = symbols.getAmPmStrings();
-
-        final int length = mPattern.length();
-        final int[] indexRef = new int[1];
-
-        for (int i = 0; i < length; i++) {
-            indexRef[0] = i;
-            final String token = parseToken(mPattern, indexRef);
-            i = indexRef[0];
-
-            final int tokenLen = token.length();
-            if (tokenLen == 0) {
-                break;
-            }
-
-            Rule rule;
-            final char c = token.charAt(0);
-
-            switch (c) {
-                case 'G': // era designator (text)
-                    rule = new TextField(Calendar.ERA, ERAs);
-                    break;
-                case 'y': // year (number)
-                case 'Y': // week year
-                    if (tokenLen == 2) {
-                        rule = TwoDigitYearField.INSTANCE;
-                    } else {
-                        rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen);
-                    }
-                    if (c == 'Y') {
-                        rule = new WeekYear((NumberRule) rule);
-                    }
-                    break;
-                case 'M': // month in year (text and number)
-                    if (tokenLen >= 4) {
-                        rule = new TextField(Calendar.MONTH, months);
-                    } else if (tokenLen == 3) {
-                        rule = new TextField(Calendar.MONTH, shortMonths);
-                    } else if (tokenLen == 2) {
-                        rule = TwoDigitMonthField.INSTANCE;
-                    } else {
-                        rule = UnpaddedMonthField.INSTANCE;
-                    }
-                    break;
-                case 'd': // day in month (number)
-                    rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);
-                    break;
-                case 'h': // hour in am/pm (number, 1..12)
-                    rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));
-                    break;
-                case 'H': // hour in day (number, 0..23)
-                    rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);
-                    break;
-                case 'm': // minute in hour (number)
-                    rule = selectNumberRule(Calendar.MINUTE, tokenLen);
-                    break;
-                case 's': // second in minute (number)
-                    rule = selectNumberRule(Calendar.SECOND, tokenLen);
-                    break;
-                case 'S': // millisecond (number)
-                    rule = selectNumberRule(Calendar.MILLISECOND, tokenLen);
-                    break;
-                case 'E': // day in week (text)
-                    rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays);
-                    break;
-                case 'u': // day in week (number)
-                    rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen));
-                    break;
-                case 'D': // day in year (number)
-                    rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);
-                    break;
-                case 'F': // day of week in month (number)
-                    rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);
-                    break;
-                case 'w': // week in year (number)
-                    rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);
-                    break;
-                case 'W': // week in month (number)
-                    rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);
-                    break;
-                case 'a': // am/pm marker (text)
-                    rule = new TextField(Calendar.AM_PM, AmPmStrings);
-                    break;
-                case 'k': // hour in day (1..24)
-                    rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));
-                    break;
-                case 'K': // hour in am/pm (0..11)
-                    rule = selectNumberRule(Calendar.HOUR, tokenLen);
-                    break;
-                case 'X': // ISO 8601
-                    rule = Iso8601_Rule.getRule(tokenLen);
-                    break;
-                case 'z': // time zone (text)
-                    if (tokenLen >= 4) {
-                        rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.LONG);
-                    } else {
-                        rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.SHORT);
-                    }
-                    break;
-                case 'Z': // time zone (value)
-                    if (tokenLen == 1) {
-                        rule = TimeZoneNumberRule.INSTANCE_NO_COLON;
-                    } else if (tokenLen == 2) {
-                        rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;
-                    } else {
-                        rule = TimeZoneNumberRule.INSTANCE_COLON;
-                    }
-                    break;
-                case '\'': // literal text
-                    final String sub = token.substring(1);
-                    if (sub.length() == 1) {
-                        rule = new CharacterLiteral(sub.charAt(0));
-                    } else {
-                        rule = new StringLiteral(sub);
-                    }
-                    break;
-                default:
-                    throw new IllegalArgumentException("Illegal pattern component: " + token);
-            }
-
-            rules.add(rule);
-        }
-
-        return rules;
-    }
-
-    /**
-     * <p>Performs the parsing of tokens.</p>
-     *
-     * @param pattern  the pattern
-     * @param indexRef  index references
-     * @return parsed token
-     */
-    protected String parseToken(final String pattern, final int[] indexRef) {
-        final StringBuilder buf = new StringBuilder();
-
-        int i = indexRef[0];
-        final int length = pattern.length();
-
-        char c = pattern.charAt(i);
-        if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
-            // Scan a run of the same character, which indicates a time
-            // pattern.
-            buf.append(c);
-
-            while (i + 1 < length) {
-                final char peek = pattern.charAt(i + 1);
-                if (peek == c) {
-                    buf.append(c);
-                    i++;
-                } else {
-                    break;
-                }
-            }
-        } else {
-            // This will identify token as text.
-            buf.append('\'');
-
-            boolean inLiteral = false;
-
-            for (; i < length; i++) {
-                c = pattern.charAt(i);
-
-                if (c == '\'') {
-                    if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
-                        // '' is treated as escaped '
-                        i++;
-                        buf.append(c);
-                    } else {
-                        inLiteral = !inLiteral;
-                    }
-                } else if (!inLiteral && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
-                    i--;
-                    break;
-                } else {
-                    buf.append(c);
-                }
-            }
-        }
-
-        indexRef[0] = i;
-        return buf.toString();
-    }
-
-    /**
-     * <p>Gets an appropriate rule for the padding required.</p>
-     *
-     * @param field  the field to get a rule for
-     * @param padding  the padding required
-     * @return a new rule with the correct padding
-     */
-    protected NumberRule selectNumberRule(final int field, final int padding) {
-        switch (padding) {
-            case 1:
-                return new UnpaddedNumberField(field);
-            case 2:
-                return new TwoDigitNumberField(field);
-            default:
-                return new PaddedNumberField(field, padding);
-        }
-    }
-
-    // Format methods
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Formats a {@code Date}, {@code Calendar} or
-     * {@code Long} (milliseconds) object.</p>
-     * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}, or {{@link #format(Object)}
-     * @param obj  the object to format
-     * @param toAppendTo  the buffer to append to
-     * @param pos  the position - ignored
-     * @return the buffer passed in
-     */
-    @Deprecated
-    @Override
-    public StringBuilder format(final Object obj, final StringBuilder toAppendTo, final FieldPosition pos) {
-        if (obj instanceof Date) {
-            return format((Date) obj, toAppendTo);
-        } else if (obj instanceof Calendar) {
-            return format((Calendar) obj, toAppendTo);
-        } else if (obj instanceof Long) {
-            return format(((Long) obj).longValue(), toAppendTo);
-        } else {
-            throw new IllegalArgumentException(
-                    "Unknown class: " + (obj == null ? "<null>" : obj.getClass().getName()));
-        }
-    }
-
-    /**
-     * <p>Formats a {@code Date}, {@code Calendar} or
-     * {@code Long} (milliseconds) object.</p>
-     * @since 3.5
-     * @param obj  the object to format
-     * @return The formatted value.
-     */
-    String format(final Object obj) {
-        if (obj instanceof Date) {
-            return format((Date) obj);
-        } else if (obj instanceof Calendar) {
-            return format((Calendar) obj);
-        } else if (obj instanceof Long) {
-            return format(((Long) obj).longValue());
-        } else {
-            throw new IllegalArgumentException(
-                    "Unknown class: " + (obj == null ? "<null>" : obj.getClass().getName()));
-        }
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#format(long)
-     */
-    @Override
-    public String format(final long millis) {
-        final Calendar c = newCalendar();
-        c.setTimeInMillis(millis);
-        return applyRulesToString(c);
-    }
-
-    /**
-     * Creates a String representation of the given Calendar by applying the rules of this printer to it.
-     * @param c the Calender to apply the rules to.
-     * @return a String representation of the given Calendar.
-     */
-    private String applyRulesToString(final Calendar c) {
-        return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString();
-    }
-
-    /**
-     * Creation method for new calender instances.
-     * @return a new Calendar instance.
-     */
-    private Calendar newCalendar() {
-        return Calendar.getInstance(mTimeZone, mLocale);
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date)
-     */
-    @Override
-    public String format(final Date date) {
-        final Calendar c = newCalendar();
-        c.setTime(date);
-        return applyRulesToString(c);
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar)
-     */
-    @Override
-    public String format(final Calendar calendar) {
-        return format(calendar, new StringBuilder(mMaxLengthEstimate)).toString();
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.Appendable)
-     */
-    @Override
-    public <B extends Appendable> B format(final long millis, final B buf) {
-        final Calendar c = newCalendar();
-        c.setTimeInMillis(millis);
-        return applyRules(c, buf);
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.Appendable)
-     */
-    @Override
-    public <B extends Appendable> B format(final Date date, final B buf) {
-        final Calendar c = newCalendar();
-        c.setTime(date);
-        return applyRules(c, buf);
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.Appendable)
-     */
-    @Override
-    public <B extends Appendable> B format(Calendar calendar, final B buf) {
-        // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
-        if (!calendar.getTimeZone().equals(mTimeZone)) {
-            calendar = (Calendar) calendar.clone();
-            calendar.setTimeZone(mTimeZone);
-        }
-        return applyRules(calendar, buf);
-    }
-
-    /**
-     * <p>Performs the formatting by applying the rules to the
-     * specified calendar.</p>
-     *
-     * @param calendar  the calendar to format
-     * @param buf  the buffer to format into
-     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
-     * @return the specified string buffer
-     */
-    private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) {
-        try {
-            for (final Rule rule : mRules) {
-                rule.appendTo(buf, calendar);
-            }
-        } catch (final IOException ioe) {
-            Throwables.rethrow(ioe);
-        }
-        return buf;
-    }
-
-    // Accessors
-    // -----------------------------------------------------------------------
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#getPattern()
-     */
-    @Override
-    public String getPattern() {
-        return mPattern;
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone()
-     */
-    @Override
-    public TimeZone getTimeZone() {
-        return mTimeZone;
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.commons.lang3.time.DatePrinter#getLocale()
-     */
-    @Override
-    public Locale getLocale() {
-        return mLocale;
-    }
-
-    /**
-     * <p>Gets an estimate for the maximum string length that the
-     * formatter will produce.</p>
-     *
-     * <p>The actual formatted length will almost always be less than or
-     * equal to this amount.</p>
-     *
-     * @return the maximum formatted length
-     */
-    public int getMaxLengthEstimate() {
-        return mMaxLengthEstimate;
-    }
-
-    // Basics
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Compares two objects for equality.</p>
-     *
-     * @param obj  the object to compare to
-     * @return {@code true} if equal
-     */
-    @Override
-    public boolean equals(final Object obj) {
-        if (!(obj instanceof FastDatePrinter)) {
-            return false;
-        }
-        final FastDatePrinter other = (FastDatePrinter) obj;
-        return mPattern.equals(other.mPattern) && mTimeZone.equals(other.mTimeZone) && mLocale.equals(other.mLocale);
-    }
-
-    /**
-     * <p>Returns a hash code compatible with equals.</p>
-     *
-     * @return a hash code compatible with equals
-     */
-    @Override
-    public int hashCode() {
-        return mPattern.hashCode() + 13 * (mTimeZone.hashCode() + 13 * mLocale.hashCode());
-    }
-
-    /**
-     * <p>Gets a debugging string version of this formatter.</p>
-     *
-     * @return a debugging string
-     */
-    @Override
-    public String toString() {
-        return "FastDatePrinter[" + mPattern + "," + mLocale + "," + mTimeZone.getID() + "]";
-    }
-
-    /**
-     * Appends two digits to the given buffer.
-     *
-     * @param buffer the buffer to append to.
-     * @param value the value to append digits from.
-     */
-    private static void appendDigits(final Appendable buffer, final int value) throws IOException {
-        buffer.append((char) (value / 10 + '0'));
-        buffer.append((char) (value % 10 + '0'));
-    }
-
-    private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3
-
-    /**
-     * Appends all digits to the given buffer.
-     *
-     * @param buffer the buffer to append to.
-     * @param value the value to append digits from.
-     */
-    private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException {
-        // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array
-        // see LANG-1248
-        if (value < 10000) {
-            // less memory allocation path works for four digits or less
-
-            int nDigits = 4;
-            if (value < 1000) {
-                --nDigits;
-                if (value < 100) {
-                    --nDigits;
-                    if (value < 10) {
-                        --nDigits;
-                    }
-                }
-            }
-            // left zero pad
-            for (int i = minFieldWidth - nDigits; i > 0; --i) {
-                buffer.append('0');
-            }
-
-            switch (nDigits) {
-                case 4:
-                    buffer.append((char) (value / 1000 + '0'));
-                    value %= 1000;
-                case 3:
-                    if (value >= 100) {
-                        buffer.append((char) (value / 100 + '0'));
-                        value %= 100;
-                    } else {
-                        buffer.append('0');
-                    }
-                case 2:
-                    if (value >= 10) {
-                        buffer.append((char) (value / 10 + '0'));
-                        value %= 10;
-                    } else {
-                        buffer.append('0');
-                    }
-                case 1:
-                    buffer.append((char) (value + '0'));
-            }
-        } else {
-            // more memory allocation path works for any digits
-
-            // build up decimal representation in reverse
-            final char[] work = new char[MAX_DIGITS];
-            int digit = 0;
-            while (value != 0) {
-                work[digit++] = (char) (value % 10 + '0');
-                value = value / 10;
-            }
-
-            // pad with zeros
-            while (digit < minFieldWidth) {
-                buffer.append('0');
-                --minFieldWidth;
-            }
-
-            // reverse
-            while (--digit >= 0) {
-                buffer.append(work[digit]);
-            }
-        }
-    }
-
-    // Rules
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Inner class defining a rule.</p>
-     */
-    private interface Rule {
-
-        Rule[] EMPTY_ARRAY = {};
-
-        /**
-         * Returns the estimated length of the result.
-         *
-         * @return the estimated length
-         */
-        int estimateLength();
-
-        /**
-         * Appends the value of the specified calendar to the output buffer based on the rule implementation.
-         *
-         * @param buf the output buffer
-         * @param calendar calendar to be appended
-         * @throws IOException if an I/O error occurs
-         */
-        void appendTo(Appendable buf, Calendar calendar) throws IOException;
-    }
-
-    /**
-     * <p>Inner class defining a numeric rule.</p>
-     */
-    private interface NumberRule extends Rule {
-        /**
-         * Appends the specified value to the output buffer based on the rule implementation.
-         *
-         * @param buffer the output buffer
-         * @param value the value to be appended
-         * @throws IOException if an I/O error occurs
-         */
-        void appendTo(Appendable buffer, int value) throws IOException;
-    }
-
-    /**
-     * <p>Inner class to output a constant single character.</p>
-     */
-    private static class CharacterLiteral implements Rule {
-        private final char mValue;
-
-        /**
-         * Constructs a new instance of {@code CharacterLiteral}
-         * to hold the specified value.
-         *
-         * @param value the character literal
-         */
-        CharacterLiteral(final char value) {
-            mValue = value;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return 1;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            buffer.append(mValue);
-        }
-    }
-
-    /**
-     * <p>Inner class to output a constant string.</p>
-     */
-    private static class StringLiteral implements Rule {
-        private final String mValue;
-
-        /**
-         * Constructs a new instance of {@code StringLiteral}
-         * to hold the specified value.
-         *
-         * @param value the string literal
-         */
-        StringLiteral(final String value) {
-            mValue = value;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return mValue.length();
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            buffer.append(mValue);
-        }
-    }
-
-    /**
-     * <p>Inner class to output one of a set of values.</p>
-     */
-    private static class TextField implements Rule {
-        private final int mField;
-        private final String[] mValues;
-
-        /**
-         * Constructs an instance of {@code TextField}
-         * with the specified field and values.
-         *
-         * @param field the field
-         * @param values the field values
-         */
-        TextField(final int field, final String[] values) {
-            mField = field;
-            mValues = values;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            int max = 0;
-            for (int i = mValues.length; --i >= 0; ) {
-                final int len = mValues[i].length();
-                if (len > max) {
-                    max = len;
-                }
-            }
-            return max;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            buffer.append(mValues[calendar.get(mField)]);
-        }
-    }
-
-    /**
-     * <p>Inner class to output an unpadded number.</p>
-     */
-    private static class UnpaddedNumberField implements NumberRule {
-        private final int mField;
-
-        /**
-         * Constructs an instance of {@code UnpadedNumberField} with the specified field.
-         *
-         * @param field the field
-         */
-        UnpaddedNumberField(final int field) {
-            mField = field;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return 4;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            appendTo(buffer, calendar.get(mField));
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public final void appendTo(final Appendable buffer, final int value) throws IOException {
-            if (value < 10) {
-                buffer.append((char) (value + '0'));
-            } else if (value < 100) {
-                appendDigits(buffer, value);
-            } else {
-                appendFullDigits(buffer, value, 1);
-            }
-        }
-    }
-
-    /**
-     * <p>Inner class to output an unpadded month.</p>
-     */
-    private static class UnpaddedMonthField implements NumberRule {
-        static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField();
-
-        /**
-         * Constructs an instance of {@code UnpaddedMonthField}.
-         *
-         */
-        UnpaddedMonthField() {
-            super();
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return 2;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public final void appendTo(final Appendable buffer, final int value) throws IOException {
-            if (value < 10) {
-                buffer.append((char) (value + '0'));
-            } else {
-                appendDigits(buffer, value);
-            }
-        }
-    }
-
-    /**
-     * <p>Inner class to output a padded number.</p>
-     */
-    private static class PaddedNumberField implements NumberRule {
-        private final int mField;
-        private final int mSize;
-
-        /**
-         * Constructs an instance of {@code PaddedNumberField}.
-         *
-         * @param field the field
-         * @param size size of the output field
-         */
-        PaddedNumberField(final int field, final int size) {
-            if (size < 3) {
-                // Should use UnpaddedNumberField or TwoDigitNumberField.
-                throw new IllegalArgumentException();
-            }
-            mField = field;
-            mSize = size;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return mSize;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            appendTo(buffer, calendar.get(mField));
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public final void appendTo(final Appendable buffer, final int value) throws IOException {
-            appendFullDigits(buffer, value, mSize);
-        }
-    }
-
-    /**
-     * <p>Inner class to output a two digit number.</p>
-     */
-    private static class TwoDigitNumberField implements NumberRule {
-        private final int mField;
-
-        /**
-         * Constructs an instance of {@code TwoDigitNumberField} with the specified field.
-         *
-         * @param field the field
-         */
-        TwoDigitNumberField(final int field) {
-            mField = field;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return 2;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            appendTo(buffer, calendar.get(mField));
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public final void appendTo(final Appendable buffer, final int value) throws IOException {
-            if (value < 100) {
-                appendDigits(buffer, value);
-            } else {
-                appendFullDigits(buffer, value, 2);
-            }
-        }
-    }
-
-    /**
-     * <p>Inner class to output a two digit year.</p>
-     */
-    private static class TwoDigitYearField implements NumberRule {
-        static final TwoDigitYearField INSTANCE = new TwoDigitYearField();
-
-        /**
-         * Constructs an instance of {@code TwoDigitYearField}.
-         */
-        TwoDigitYearField() {
-            super();
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return 2;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            appendTo(buffer, calendar.get(Calendar.YEAR) % 100);
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public final void appendTo(final Appendable buffer, final int value) throws IOException {
-            appendDigits(buffer, value);
-        }
-    }
-
-    /**
-     * <p>Inner class to output a two digit month.</p>
-     */
-    private static class TwoDigitMonthField implements NumberRule {
-        static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField();
-
-        /**
-         * Constructs an instance of {@code TwoDigitMonthField}.
-         */
-        TwoDigitMonthField() {
-            super();
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return 2;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public final void appendTo(final Appendable buffer, final int value) throws IOException {
-            appendDigits(buffer, value);
-        }
-    }
-
-    /**
-     * <p>Inner class to output the twelve hour field.</p>
-     */
-    private static class TwelveHourField implements NumberRule {
-        private final NumberRule mRule;
-
-        /**
-         * Constructs an instance of {@code TwelveHourField} with the specified
-         * {@code NumberRule}.
-         *
-         * @param rule the rule
-         */
-        TwelveHourField(final NumberRule rule) {
-            mRule = rule;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return mRule.estimateLength();
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            int value = calendar.get(Calendar.HOUR);
-            if (value == 0) {
-                value = calendar.getLeastMaximum(Calendar.HOUR) + 1;
-            }
-            mRule.appendTo(buffer, value);
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final int value) throws IOException {
-            mRule.appendTo(buffer, value);
-        }
-    }
-
-    /**
-     * <p>Inner class to output the twenty four hour field.</p>
-     */
-    private static class TwentyFourHourField implements NumberRule {
-        private final NumberRule mRule;
-
-        /**
-         * Constructs an instance of {@code TwentyFourHourField} with the specified
-         * {@code NumberRule}.
-         *
-         * @param rule the rule
-         */
-        TwentyFourHourField(final NumberRule rule) {
-            mRule = rule;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return mRule.estimateLength();
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            int value = calendar.get(Calendar.HOUR_OF_DAY);
-            if (value == 0) {
-                value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1;
-            }
-            mRule.appendTo(buffer, value);
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final int value) throws IOException {
-            mRule.appendTo(buffer, value);
-        }
-    }
-
-    /**
-     * <p>Inner class to output the numeric day in week.</p>
-     */
-    private static class DayInWeekField implements NumberRule {
-        private final NumberRule mRule;
-
-        DayInWeekField(final NumberRule rule) {
-            mRule = rule;
-        }
-
-        @Override
-        public int estimateLength() {
-            return mRule.estimateLength();
-        }
-
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            final int value = calendar.get(Calendar.DAY_OF_WEEK);
-            mRule.appendTo(buffer, value != Calendar.SUNDAY ? value - 1 : 7);
-        }
-
-        @Override
-        public void appendTo(final Appendable buffer, final int value) throws IOException {
-            mRule.appendTo(buffer, value);
-        }
-    }
-
-    /**
-     * <p>Inner class to output the numeric day in week.</p>
-     */
-    private static class WeekYear implements NumberRule {
-        private final NumberRule mRule;
-
-        WeekYear(final NumberRule rule) {
-            mRule = rule;
-        }
-
-        @Override
-        public int estimateLength() {
-            return mRule.estimateLength();
-        }
-
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            mRule.appendTo(buffer, calendar.getWeekYear());
-        }
-
-        @Override
-        public void appendTo(final Appendable buffer, final int value) throws IOException {
-            mRule.appendTo(buffer, value);
-        }
-    }
-
-    // -----------------------------------------------------------------------
-
-    private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache = new ConcurrentHashMap<>(7);
-    /**
-     * <p>Gets the time zone display name, using a cache for performance.</p>
-     *
-     * @param tz  the zone to query
-     * @param daylight  true if daylight savings
-     * @param style  the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT}
-     * @param locale  the locale to use
-     * @return the textual name of the time zone
-     */
-    static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) {
-        final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale);
-        String value = cTimeZoneDisplayCache.get(key);
-        if (value == null) {
-            // This is a very slow call, so cache the results.
-            value = tz.getDisplayName(daylight, style, locale);
-            final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value);
-            if (prior != null) {
-                value = prior;
-            }
-        }
-        return value;
-    }
-
-    /**
-     * <p>Inner class to output a time zone name.</p>
-     */
-    private static class TimeZoneNameRule implements Rule {
-        private final Locale mLocale;
-        private final int mStyle;
-        private final String mStandard;
-        private final String mDaylight;
-
-        /**
-         * Constructs an instance of {@code TimeZoneNameRule} with the specified properties.
-         *
-         * @param timeZone the time zone
-         * @param locale the locale
-         * @param style the style
-         */
-        TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) {
-            mLocale = locale;
-            mStyle = style;
-
-            mStandard = getTimeZoneDisplay(timeZone, false, style, locale);
-            mDaylight = getTimeZoneDisplay(timeZone, true, style, locale);
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            // We have no access to the Calendar object that will be passed to
-            // appendTo so base estimate on the TimeZone passed to the
-            // constructor
-            return Math.max(mStandard.length(), mDaylight.length());
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            final TimeZone zone = calendar.getTimeZone();
-            if (calendar.get(Calendar.DST_OFFSET) != 0) {
-                buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale));
-            } else {
-                buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale));
-            }
-        }
-    }
-
-    /**
-     * <p>Inner class to output a time zone as a number {@code +/-HHMM}
-     * or {@code +/-HH:MM}.</p>
-     */
-    private static class TimeZoneNumberRule implements Rule {
-        static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true);
-        static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false);
-
-        final boolean mColon;
-
-        /**
-         * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties.
-         *
-         * @param colon add colon between HH and MM in the output if {@code true}
-         */
-        TimeZoneNumberRule(final boolean colon) {
-            mColon = colon;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return 5;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-
-            int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
-
-            if (offset < 0) {
-                buffer.append('-');
-                offset = -offset;
-            } else {
-                buffer.append('+');
-            }
-
-            final int hours = offset / (60 * 60 * 1000);
-            appendDigits(buffer, hours);
-
-            if (mColon) {
-                buffer.append(':');
-            }
-
-            final int minutes = offset / (60 * 1000) - 60 * hours;
-            appendDigits(buffer, minutes);
-        }
-    }
-
-    /**
-     * <p>Inner class to output a time zone as a number {@code +/-HHMM}
-     * or {@code +/-HH:MM}.</p>
-     */
-    private static class Iso8601_Rule implements Rule {
-
-        // Sign TwoDigitHours or Z
-        static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3);
-        // Sign TwoDigitHours Minutes or Z
-        static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5);
-        // Sign TwoDigitHours : Minutes or Z
-        static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6);
-
-        /**
-         * Factory method for Iso8601_Rules.
-         *
-         * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
-         * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such
-         *          rule exists, an IllegalArgumentException will be thrown.
-         */
-        static Iso8601_Rule getRule(final int tokenLen) {
-            switch (tokenLen) {
-                case 1:
-                    return Iso8601_Rule.ISO8601_HOURS;
-                case 2:
-                    return Iso8601_Rule.ISO8601_HOURS_MINUTES;
-                case 3:
-                    return Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;
-                default:
-                    throw new IllegalArgumentException("invalid number of X");
-            }
-        }
-
-        final int length;
-
-        /**
-         * Constructs an instance of {@code Iso8601_Rule} with the specified properties.
-         *
-         * @param length The number of characters in output (unless Z is output)
-         */
-        Iso8601_Rule(final int length) {
-            this.length = length;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int estimateLength() {
-            return length;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
-            int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
-            if (offset == 0) {
-                buffer.append("Z");
-                return;
-            }
-
-            if (offset < 0) {
-                buffer.append('-');
-                offset = -offset;
-            } else {
-                buffer.append('+');
-            }
-
-            final int hours = offset / (60 * 60 * 1000);
-            appendDigits(buffer, hours);
-
-            if (length < 5) {
-                return;
-            }
-
-            if (length == 6) {
-                buffer.append(':');
-            }
-
-            final int minutes = offset / (60 * 1000) - 60 * hours;
-            appendDigits(buffer, minutes);
-        }
-    }
-
-    // ----------------------------------------------------------------------
-    /**
-     * <p>Inner class that acts as a compound key for time zone names.</p>
-     */
-    private static class TimeZoneDisplayKey {
-        private final TimeZone mTimeZone;
-        private final int mStyle;
-        private final Locale mLocale;
-
-        /**
-         * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties.
-         *
-         * @param timeZone the time zone
-         * @param daylight adjust the style for daylight saving time if {@code true}
-         * @param style the timezone style
-         * @param locale the timezone locale
-         */
-        TimeZoneDisplayKey(final TimeZone timeZone, final boolean daylight, final int style, final Locale locale) {
-            mTimeZone = timeZone;
-            if (daylight) {
-                mStyle = style | 0x80000000;
-            } else {
-                mStyle = style;
-            }
-            mLocale = locale;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int hashCode() {
-            return (mStyle * 31 + mLocale.hashCode()) * 31 + mTimeZone.hashCode();
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public boolean equals(final Object obj) {
-            if (this == obj) {
-                return true;
-            }
-            if (obj instanceof TimeZoneDisplayKey) {
-                final TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj;
-                return mTimeZone.equals(other.mTimeZone) && mStyle == other.mStyle && mLocale.equals(other.mLocale);
-            }
-            return false;
-        }
-    }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java
deleted file mode 100644
index b2986151b7..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java
+++ /dev/null
@@ -1,760 +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.time.internal.format;
-
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Objects;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import org.apache.logging.log4j.core.time.Instant;
-import org.osgi.annotation.versioning.ProviderType;
-
-/**
- * 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}.
- * <p>
- * Related benchmarks: /log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and
- * /log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
- * </p>
- */
-@ProviderType
-public class FixedDateFormat {
-
-    /**
-     * Enumeration over the supported date/time format patterns.
-     * <p>
-     * Package protected for unit tests.
-     * </p>
-     */
-    public enum FixedFormat {
-        /**
-         * ABSOLUTE time format: {@code "HH:mm:ss,SSS"}.
-         */
-        ABSOLUTE("HH:mm:ss,SSS", null, 0, ':', 1, ',', 1, 3, null),
-        /**
-         * ABSOLUTE time format with microsecond precision: {@code "HH:mm:ss,nnnnnn"}.
-         */
-        ABSOLUTE_MICROS("HH:mm:ss,nnnnnn", null, 0, ':', 1, ',', 1, 6, null),
-        /**
-         * ABSOLUTE time format with nanosecond precision: {@code "HH:mm:ss,nnnnnnnnn"}.
-         */
-        ABSOLUTE_NANOS("HH:mm:ss,nnnnnnnnn", null, 0, ':', 1, ',', 1, 9, null),
-
-        /**
-         * ABSOLUTE time format variation with period separator: {@code "HH:mm:ss.SSS"}.
-         */
-        ABSOLUTE_PERIOD("HH:mm:ss.SSS", null, 0, ':', 1, '.', 1, 3, null),
-
-        /**
-         * COMPACT time format: {@code "yyyyMMddHHmmssSSS"}.
-         */
-        COMPACT("yyyyMMddHHmmssSSS", "yyyyMMdd", 0, ' ', 0, ' ', 0, 3, null),
-
-        /**
-         * DATE_AND_TIME time format: {@code "dd MMM yyyy HH:mm:ss,SSS"}.
-         */
-        DATE("dd MMM yyyy HH:mm:ss,SSS", "dd MMM yyyy ", 0, ':', 1, ',', 1, 3, null),
-
-        /**
-         * DATE_AND_TIME time format variation with period separator: {@code "dd MMM yyyy HH:mm:ss.SSS"}.
-         */
-        DATE_PERIOD("dd MMM yyyy HH:mm:ss.SSS", "dd MMM yyyy ", 0, ':', 1, '.', 1, 3, null),
-
-        /**
-         * DEFAULT time format: {@code "yyyy-MM-dd HH:mm:ss,SSS"}.
-         */
-        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_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,nnnnnnnnn"}.
-         */
-        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"}.
-         */
-        DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS", "yyyy-MM-dd ", 0, ':', 1, '.', 1, 3, null),
-
-        /**
-         * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss,SSS"}.
-         */
-        ISO8601_BASIC("yyyyMMdd'T'HHmmss,SSS", "yyyyMMdd'T'", 2, ' ', 0, ',', 1, 3, null),
-
-        /**
-         * ISO8601_BASIC time format: {@code "yyyyMMdd'T'HHmmss.SSS"}.
-         */
-        ISO8601_BASIC_PERIOD("yyyyMMdd'T'HHmmss.SSS", "yyyyMMdd'T'", 2, ' ', 0, '.', 1, 3, null),
-
-        /**
-         * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSS"}.
-         */
-        ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, null),
-
-        // TODO Do we even want a format without seconds?
-        //      /**
-        //       * ISO8601_OFFSET_DATE_TIME time format: {@code "yyyy-MM-dd'T'HH:mmXXX"}.
-        //       */
-        //      // Would need work in org.apache.logging.log4j.core.util.datetime.FixedDateFormat.writeTime(int, char[],
-        // int)
-        //      ISO8601_OFFSET_DATE_TIME("yyyy-MM-dd'T'HH:mmXXX", "yyyy-MM-dd'T'", 2, ':', 1, ' ', 0, 0,
-        // FixedTimeZoneFormat.XXX),
-
-        /**
-         * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSX"} with a time zone like {@code -07}.
-         */
-        ISO8601_OFFSET_DATE_TIME_HH(
-                "yyyy-MM-dd'T'HH:mm:ss,SSSX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.HH),
-
-        /**
-         * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSXX"} with a time zone like {@code -0700}.
-         */
-        ISO8601_OFFSET_DATE_TIME_HHMM(
-                "yyyy-MM-dd'T'HH:mm:ss,SSSXX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.HHMM),
-
-        /**
-         * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSXXX"} with a time zone like {@code -07:00}.
-         */
-        ISO8601_OFFSET_DATE_TIME_HHCMM(
-                "yyyy-MM-dd'T'HH:mm:ss,SSSXXX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.HHCMM),
-
-        /**
-         * ISO8601 time format: {@code "yyyy-MM-dd'T'HH:mm:ss.SSS"}.
-         */
-        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_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"}.
-         */
-        US_MONTH_DAY_YEAR2_TIME("dd/MM/yy HH:mm:ss.SSS", "dd/MM/yy ", 0, ':', 1, '.', 1, 3, null),
-
-        /**
-         * American date/time format with 4-digit year: {@code "dd/MM/yyyy HH:mm:ss.SSS"}.
-         */
-        US_MONTH_DAY_YEAR4_TIME("dd/MM/yyyy HH:mm:ss.SSS", "dd/MM/yyyy ", 0, ':', 1, '.', 1, 3, null);
-
-        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 final String pattern;
-        private final String datePattern;
-        private final int escapeCount;
-        private final char timeSeparatorChar;
-        private final int timeSeparatorLength;
-        private final char millisSeparatorChar;
-        private final int millisSeparatorLength;
-        private final int secondFractionDigits;
-        private final FixedTimeZoneFormat fixedTimeZoneFormat;
-
-        FixedFormat(
-                final String pattern,
-                final String datePattern,
-                final int escapeCount,
-                final char timeSeparator,
-                final int timeSepLength,
-                final char millisSeparator,
-                final int millisSepLength,
-                final int secondFractionDigits,
-                final FixedTimeZoneFormat fixedTimeZoneFormat) {
-            this.timeSeparatorChar = timeSeparator;
-            this.timeSeparatorLength = timeSepLength;
-            this.millisSeparatorChar = millisSeparator;
-            this.millisSeparatorLength = millisSepLength;
-            this.pattern = Objects.requireNonNull(pattern);
-            this.datePattern = datePattern; // may be null
-            this.escapeCount = escapeCount;
-            this.secondFractionDigits = secondFractionDigits;
-            this.fixedTimeZoneFormat = fixedTimeZoneFormat;
-        }
-
-        /**
-         * Returns the full pattern.
-         *
-         * @return the full pattern
-         */
-        public String getPattern() {
-            return pattern;
-        }
-
-        /**
-         * Returns the date part of the pattern.
-         *
-         * @return the date part of the pattern
-         */
-        public String getDatePattern() {
-            return datePattern;
-        }
-
-        /**
-         * Returns the FixedFormat with the name or pattern matching the specified string or {@code null} if not found.
-         *
-         * @param nameOrPattern the name or pattern to find a FixedFormat for
-         * @return the FixedFormat with the name or pattern matching the specified string
-         */
-        public static FixedFormat lookup(final String nameOrPattern) {
-            for (final FixedFormat type : FixedFormat.values()) {
-                if (type.name().equals(nameOrPattern) || type.getPattern().equals(nameOrPattern)) {
-                    return type;
-                }
-            }
-            return null;
-        }
-
-        static FixedFormat lookupIgnoringNanos(final String pattern) {
-            final int[] nanoRange = nanoRange(pattern);
-            final int nanoStart = nanoRange[0];
-            final int nanoEnd = nanoRange[1];
-            if (nanoStart > 0) {
-                final String subPattern = pattern.substring(0, nanoStart)
-                        + DEFAULT_SECOND_FRACTION_PATTERN
-                        + pattern.substring(nanoEnd, pattern.length());
-                for (final FixedFormat type : FixedFormat.values()) {
-                    if (type.getPattern().equals(subPattern)) {
-                        return type;
-                    }
-                }
-            }
-            return null;
-        }
-
-        private static final int[] EMPTY_RANGE = {-1, -1};
-
-        /**
-         * @return int[0] start index inclusive; int[1] end index exclusive
-         */
-        private static int[] nanoRange(final String pattern) {
-            final int indexStart = pattern.indexOf(SECOND_FRACTION_PATTERN);
-            int indexEnd = -1;
-            if (indexStart >= 0) {
-                indexEnd = pattern.indexOf('Z', indexStart);
-                indexEnd = indexEnd < 0 ? pattern.indexOf('X', indexStart) : indexEnd;
-                indexEnd = indexEnd < 0 ? pattern.length() : indexEnd;
-                for (int i = indexStart + 1; i < indexEnd; i++) {
-                    if (pattern.charAt(i) != SECOND_FRACTION_PATTERN) {
-                        return EMPTY_RANGE;
-                    }
-                }
-            }
-            return new int[] {indexStart, indexEnd};
-        }
-
-        /**
-         * Returns the optional time zone format.
-         * @return the optional time zone format, may be null.
-         */
-        public FixedTimeZoneFormat getTimeZoneFormat() {
-            return fixedTimeZoneFormat;
-        }
-
-        /**
-         * Returns the length of the resulting formatted date and time strings.
-         *
-         * @return the length of the resulting formatted date and time strings
-         */
-        public int getLength() {
-            return pattern.length() - escapeCount;
-        }
-
-        /**
-         * Returns the length of the date part of the resulting formatted string.
-         *
-         * @return the length of the date part of the resulting formatted string
-         */
-        public int getDatePatternLength() {
-            return getDatePattern() == null ? 0 : getDatePattern().length() - escapeCount;
-        }
-
-        /**
-         * Returns the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null} if the
-         * pattern does not have a date part.
-         *
-         * @return the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null}
-         */
-        public FastDateFormat getFastDateFormat() {
-            return getFastDateFormat(null);
-        }
-
-        /**
-         * Returns the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null} if the
-         * pattern does not have a date part.
-         *
-         * @param tz the time zone to use
-         * @return the {@code FastDateFormat} object for formatting the date part of the pattern or {@code null}
-         */
-        public FastDateFormat getFastDateFormat(final TimeZone tz) {
-            return getDatePattern() == null ? null : FastDateFormat.getInstance(getDatePattern(), tz);
-        }
-
-        /**
-         * Returns the number of digits specifying the fraction of the second to show
-         * @return 3 for millisecond precision, 6 for microsecond precision or 9 for nanosecond precision
-         */
-        public int getSecondFractionDigits() {
-            return secondFractionDigits;
-        }
-    }
-
-    private static final char NONE = (char) 0;
-
-    /**
-     * Fixed time zone formats. The enum names are symbols from Java's <a href=
-     * "https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html">DateTimeFormatter</a>.
-     *
-     * @see <a href=
-     * "https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html">DateTimeFormatter</a>
-     */
-    public enum FixedTimeZoneFormat {
-
-        /**
-         * Offset like {@code -07}
-         */
-        HH(NONE, false, 3),
-
-        /**
-         * Offset like {@code -0700}.
-         * Same as Z.
-         */
-        HHMM(NONE, true, 5),
-
-        /**
-         * Offset like {@code -07:00}
-         */
-        HHCMM(':', true, 6);
-
-        FixedTimeZoneFormat() {
-            this(NONE, true, 4);
-        }
-
-        FixedTimeZoneFormat(final char timeSeparatorChar, final boolean minutes, final int length) {
-            this.timeSeparatorChar = timeSeparatorChar;
-            this.timeSeparatorCharLen = timeSeparatorChar != NONE ? 1 : 0;
-            this.useMinutes = minutes;
-            this.length = length;
-        }
-
-        private final char timeSeparatorChar;
-        private final int timeSeparatorCharLen;
-        private final boolean useMinutes;
-        // The length includes 1 for the leading sign
-        private final int length;
-
-        public int getLength() {
-            return length;
-        }
-
-        // Profiling showed this method is important to log4j performance. Modify with care!
-        // 262 bytes (will be inlined when hot enough: <= -XX:FreqInlineSize=325 bytes on Linux)
-        private int write(final int offset, final char[] buffer, final int pos) {
-            // This method duplicates part of writeTime()
-            int p = pos;
-            buffer[p++] = offset < 0 ? '-' : '+';
-            final int absOffset = Math.abs(offset);
-            final int hours = absOffset / 3600000;
-            int ms = absOffset - (3600000 * hours);
-
-            // Hour
-            int temp = hours / 10;
-            buffer[p++] = ((char) (temp + '0'));
-
-            // Do subtract to get remainder instead of doing % 10
-            buffer[p++] = ((char) (hours - 10 * temp + '0'));
-
-            // Minute
-            if (useMinutes) {
-                buffer[p] = timeSeparatorChar;
-                p += timeSeparatorCharLen;
-                final int minutes = ms / 60000;
-                ms -= 60000 * minutes;
-
-                temp = minutes / 10;
-                buffer[p++] = ((char) (temp + '0'));
-
-                // Do subtract to get remainder instead of doing % 10
-                buffer[p++] = ((char) (minutes - 10 * temp + '0'));
-            }
-            return p;
-        }
-    }
-
-    private final FixedFormat fixedFormat;
-    private final TimeZone timeZone;
-    private final int length;
-    private final int secondFractionDigits;
-    private final FastDateFormat fastDateFormat; // may be null
-    private final char timeSeparatorChar;
-    private final char millisSeparatorChar;
-    private final int timeSeparatorLength;
-    private final int millisSeparatorLength;
-    private final FixedTimeZoneFormat fixedTimeZoneFormat;
-
-    private volatile long midnightToday;
-    private volatile long midnightTomorrow;
-    private final int[] dstOffsets = new int[25];
-
-    // cachedDate does not need to be volatile because
-    // there is a write to a volatile field *after* cachedDate is modified,
-    // and there is a read from a volatile field *before* cachedDate is read.
-    // The Java memory model guarantees that because of the above,
-    // changes to cachedDate in one thread are visible to other threads.
-    // See http://g.oswego.edu/dl/jmm/cookbook.html
-    private char[] cachedDate; // may be null
-    private int dateLength;
-    private final Lock cacheLock = new ReentrantLock();
-
-    /**
-     * Constructs a FixedDateFormat for the specified fixed format.
-     * <p>
-     * Package protected for unit tests.
-     * </p>
-     *
-     * @param fixedFormat the fixed format
-     * @param tz time zone
-     */
-    FixedDateFormat(final FixedFormat fixedFormat, final TimeZone tz) {
-        this(fixedFormat, tz, fixedFormat.getSecondFractionDigits());
-    }
-
-    /**
-     * Constructs a FixedDateFormat for the specified fixed format.
-     * <p>
-     * Package protected for unit tests.
-     * </p>
-     *
-     * @param fixedFormat the fixed format
-     * @param tz time zone
-     * @param secondFractionDigits the number of digits specifying the fraction of the second to show
-     */
-    FixedDateFormat(final FixedFormat fixedFormat, final TimeZone tz, final int secondFractionDigits) {
-        this.fixedFormat = Objects.requireNonNull(fixedFormat);
-        this.timeZone = Objects.requireNonNull(tz);
-        this.timeSeparatorChar = fixedFormat.timeSeparatorChar;
-        this.timeSeparatorLength = fixedFormat.timeSeparatorLength;
-        this.millisSeparatorChar = fixedFormat.millisSeparatorChar;
-        this.millisSeparatorLength = fixedFormat.millisSeparatorLength;
-        this.fixedTimeZoneFormat = fixedFormat.fixedTimeZoneFormat;
-        this.length = fixedFormat.getLength();
-        this.secondFractionDigits = Math.max(1, Math.min(9, secondFractionDigits));
-        this.fastDateFormat = fixedFormat.getFastDateFormat(tz);
-    }
-
-    public static FixedDateFormat createIfSupported(final String... options) {
-        if (options == null || options.length == 0 || options[0] == null) {
-            return new FixedDateFormat(FixedFormat.DEFAULT, TimeZone.getDefault());
-        }
-        final TimeZone tz;
-        if (options.length > 1) {
-            if (options[1] != null) {
-                String zoneId = options[1];
-                if (zoneId.startsWith("-") || zoneId.startsWith("+")) {
-                    zoneId = "GMT" + zoneId;
-                }
-                tz = TimeZone.getTimeZone(zoneId);
-            } else {
-                tz = TimeZone.getDefault();
-            }
-        } else {
-            tz = TimeZone.getDefault();
-        }
-
-        final String option0 = options[0];
-        final FixedFormat withoutNanos = FixedFormat.lookupIgnoringNanos(option0);
-        if (withoutNanos != null) {
-            final int[] nanoRange = FixedFormat.nanoRange(option0);
-            final int nanoStart = nanoRange[0];
-            final int nanoEnd = nanoRange[1];
-            final int secondFractionDigits = nanoEnd - nanoStart;
-            return new FixedDateFormat(withoutNanos, tz, secondFractionDigits);
-        }
-        final FixedFormat type = FixedFormat.lookup(option0);
-        return type == null ? null : new FixedDateFormat(type, tz);
-    }
-
-    /**
-     * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and a {@code TimeZone.getDefault()} TimeZone.
-     *
-     * @param format the format to use
-     * @return a new {@code FixedDateFormat} object
-     */
-    public static FixedDateFormat create(final FixedFormat format) {
-        return new FixedDateFormat(format, TimeZone.getDefault());
-    }
-
-    /**
-     * Returns a new {@code FixedDateFormat} object for the specified {@code FixedFormat} and TimeZone.
-     *
-     * @param format the format to use
-     * @param tz the time zone to use
-     * @return a new {@code FixedDateFormat} object
-     */
-    public static FixedDateFormat create(final FixedFormat format, final TimeZone tz) {
-        return new FixedDateFormat(format, tz != null ? tz : TimeZone.getDefault());
-    }
-
-    /**
-     * Returns the full pattern of the selected fixed format.
-     *
-     * @return the full date-time pattern
-     */
-    public String getFormat() {
-        return fixedFormat.getPattern();
-    }
-
-    /**
-     * Returns the length of the resulting formatted date and time strings.
-     *
-     * @return the length of the resulting formatted date and time strings
-     */
-    public final int getLength() {
-        return length;
-    }
-
-    /**
-     * Returns the time zone.
-     *
-     * @return the time zone
-     */
-    public TimeZone getTimeZone() {
-        return timeZone;
-    }
-
-    /**
-     * <p>Returns the number of milliseconds since midnight in the time zone that this {@code FixedDateFormat}
-     * was constructed with for the specified currentTime.</p>
-     * <p>As a side effect, this method updates the cached formatted date and the cached date demarcation timestamps
-     * when the specified current time is outside the previously set demarcation timestamps for the start or end
-     * of the current day.</p>
-     * @param currentTime the current time in millis since the epoch
-     * @return the number of milliseconds since midnight for the specified time
-     */
-    // Profiling showed this method is important to log4j performance. Modify with care!
-    // 30 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
-    public long millisSinceMidnight(final long currentTime) {
-        if (currentTime >= midnightTomorrow || currentTime < midnightToday) {
-            updateMidnightMillis(currentTime);
-        }
-        return currentTime - midnightToday;
-    }
-
-    private void updateMidnightMillis(final long now) {
-        if (now >= midnightTomorrow || now < midnightToday) {
-            cacheLock.lock();
-            try {
-                updateCachedDate(now);
-                midnightToday = calcMidnightMillis(now, 0);
-                midnightTomorrow = calcMidnightMillis(now, 1);
-
-                updateDaylightSavingTime();
-            } finally {
-                cacheLock.unlock();
-            }
-        }
-    }
-
-    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();
-    }
-
-    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)) {
-            for (int i = 0; i < dstOffsets.length; i++) {
-                final long time = midnightToday + i * ONE_HOUR;
-                dstOffsets[i] = timeZone.getOffset(time) - timeZone.getRawOffset();
-            }
-            if (dstOffsets[0] > dstOffsets[23]) { // clock is moved backwards.
-                // we obtain midnightTonight with Calendar.getInstance(TimeZone), so it already includes raw offset
-                for (int i = dstOffsets.length - 1; i >= 0; i--) {
-                    dstOffsets[i] -= dstOffsets[0]; //
-                }
-            }
-        }
-    }
-
-    private void updateCachedDate(final long now) {
-        if (fastDateFormat != null) {
-            final StringBuilder result = fastDateFormat.format(now, new StringBuilder());
-            cachedDate = result.toString().toCharArray();
-            dateLength = result.length();
-        }
-    }
-
-    public String formatInstant(final Instant instant) {
-        final char[] result = new char[length << 1]; // double size for locales with lengthy DateFormatSymbols
-        final int written = formatInstant(instant, result, 0);
-        return new String(result, 0, written);
-    }
-
-    public int formatInstant(final Instant instant, final char[] buffer, final int startPos) {
-        final long epochMillisecond = instant.getEpochMillisecond();
-        int result = format(epochMillisecond, buffer, startPos);
-        result -= digitsLessThanThree();
-        final int pos = formatNanoOfMillisecond(instant.getNanoOfMillisecond(), buffer, startPos + result);
-        return writeTimeZone(epochMillisecond, buffer, pos);
-    }
-
-    private int digitsLessThanThree() { // in case user specified only 1 or 2 'n' format characters
-        return Math.max(0, FixedFormat.MILLI_FRACTION_DIGITS - secondFractionDigits);
-    }
-
-    // Profiling showed this method is important to log4j performance. Modify with care!
-    // 28 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
-    public String format(final long epochMillis) {
-        final char[] result = new char[length << 1]; // double size for locales with lengthy DateFormatSymbols
-        final int written = format(epochMillis, result, 0);
-        return new String(result, 0, written);
-    }
-
-    // Profiling showed this method is important to log4j performance. Modify with care!
-    // 31 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
-    public int format(final long epochMillis, final char[] buffer, final int startPos) {
-        // Calculate values by getting the ms values first and do then
-        // calculate the hour minute and second values divisions.
-
-        // Get daytime in ms: this does fit into an int
-        // int ms = (int) (time % 86400000);
-        final int ms = (int) (millisSinceMidnight(epochMillis));
-        writeDate(buffer, startPos);
-        final int pos = writeTime(ms, buffer, startPos + dateLength);
-        return pos - startPos;
-    }
-
-    // Profiling showed this method is important to log4j performance. Modify with care!
-    // 22 bytes (allows immediate JVM inlining: <= -XX:MaxInlineSize=35 bytes)
-    private void writeDate(final char[] buffer, final int startPos) {
-        if (cachedDate != null) {
-            System.arraycopy(cachedDate, 0, buffer, startPos, dateLength);
-        }
-    }
-
-    // Profiling showed this method is important to log4j performance. Modify with care!
-    // 262 bytes (will be inlined when hot enough: <= -XX:FreqInlineSize=325 bytes on Linux)
-    private int writeTime(int ms, final char[] buffer, int pos) {
-        final int hourOfDay = ms / 3600000;
-        final int hours = hourOfDay + daylightSavingTime(hourOfDay) / 3600000;
-        ms -= 3600000 * hourOfDay;
-
-        final int minutes = ms / 60000;
-        ms -= 60000 * minutes;
-
-        final int seconds = ms / 1000;
-        ms -= 1000 * seconds;
-
-        // Hour
-        int temp = hours / 10;
-        buffer[pos++] = ((char) (temp + '0'));
-
-        // Do subtract to get remainder instead of doing % 10
-        buffer[pos++] = ((char) (hours - 10 * temp + '0'));
-        buffer[pos] = timeSeparatorChar;
-        pos += timeSeparatorLength;
-
-        // Minute
-        temp = minutes / 10;
-        buffer[pos++] = ((char) (temp + '0'));
-
-        // Do subtract to get remainder instead of doing % 10
-        buffer[pos++] = ((char) (minutes - 10 * temp + '0'));
-        buffer[pos] = timeSeparatorChar;
-        pos += timeSeparatorLength;
-
-        // Second
-        temp = seconds / 10;
-        buffer[pos++] = ((char) (temp + '0'));
-        buffer[pos++] = ((char) (seconds - 10 * temp + '0'));
-        buffer[pos] = millisSeparatorChar;
-        pos += millisSeparatorLength;
-
-        // Millisecond
-        temp = ms / 100;
-        buffer[pos++] = ((char) (temp + '0'));
-
-        ms -= 100 * temp;
-        temp = ms / 10;
-        buffer[pos++] = ((char) (temp + '0'));
-
-        ms -= 10 * temp;
-        buffer[pos++] = ((char) (ms + '0'));
-        return pos;
-    }
-
-    private int writeTimeZone(final long epochMillis, final char[] buffer, int pos) {
-        if (fixedTimeZoneFormat != null) {
-            pos = fixedTimeZoneFormat.write(timeZone.getOffset(epochMillis), buffer, pos);
-        }
-        return pos;
-    }
-
-    static final int[] TABLE = {
-        100000, // 0
-        10000, // 1
-        1000, // 2
-        100, // 3
-        10, // 4
-        1, // 5
-    };
-
-    private int formatNanoOfMillisecond(final int nanoOfMillisecond, final char[] buffer, int pos) {
-        int temp;
-        int remain = nanoOfMillisecond;
-        for (int i = 0; i < secondFractionDigits - FixedFormat.MILLI_FRACTION_DIGITS; i++) {
-            final int divisor = TABLE[i];
-            temp = remain / divisor;
-            buffer[pos++] = ((char) (temp + '0'));
-            remain -= divisor * temp; // equivalent of remain % 10
-        }
-        return pos;
-    }
-
-    private int daylightSavingTime(final int hourOfDay) {
-        return hourOfDay > 23 ? dstOffsets[23] : dstOffsets[hourOfDay];
-    }
-
-    /**
-     * Returns {@code true} if the old and new date values will result in the same formatted output, {@code false}
-     * if results <i>may</i> differ.
-     */
-    public boolean isEquivalent(
-            final long oldEpochSecond, final int oldNanoOfSecond, final long epochSecond, final int nanoOfSecond) {
-        if (oldEpochSecond == epochSecond) {
-            if (secondFractionDigits <= 3) {
-                // Convert nanos to milliseconds for comparison if the format only requires milliseconds.
-                return (oldNanoOfSecond / 1000_000L) == (nanoOfSecond / 1000_000L);
-            }
-            return oldNanoOfSecond == nanoOfSecond;
-        }
-        return false;
-    }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/Format.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/Format.java
deleted file mode 100644
index dfecffbabe..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/Format.java
+++ /dev/null
@@ -1,31 +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.time.internal.format;
-
-import java.text.FieldPosition;
-
-/**
- * The basic methods for performing date formatting.
- */
-public abstract class Format {
-
-    public final String format(final Object obj) {
-        return format(obj, new StringBuilder(), new FieldPosition(0)).toString();
-    }
-
-    public abstract StringBuilder format(Object obj, StringBuilder toAppendTo, FieldPosition pos);
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FormatCache.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FormatCache.java
deleted file mode 100644
index 303255c663..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FormatCache.java
+++ /dev/null
@@ -1,265 +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.time.internal.format;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-/**
- * <p>FormatCache is a cache and factory for {@link Format}s.</p>
- *
- * <p>
- * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
- * </p>
- *
- * @since Apache Commons Lang 3.0
- */
-// TODO: Before making public move from getDateTimeInstance(Integer,...) to int; or some other approach.
-abstract class FormatCache<F extends Format> {
-
-    /**
-     * No date or no time.  Used in same parameters as DateFormat.SHORT or DateFormat.LONG
-     */
-    static final int NONE = -1;
-
-    private final ConcurrentMap<MultipartKey, F> cInstanceCache = new ConcurrentHashMap<>(7);
-
-    private static final ConcurrentMap<MultipartKey, String> cDateTimeInstanceCache = new ConcurrentHashMap<>(7);
-
-    /**
-     * <p>Gets a formatter instance using the default pattern in the
-     * default timezone and locale.</p>
-     *
-     * @return a date/time formatter
-     */
-    public F getInstance() {
-        return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault());
-    }
-
-    /**
-     * <p>Gets a formatter instance using the specified pattern, time zone
-     * and locale.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible
-     *  pattern, non-null
-     * @param timeZone  the time zone, null means use the default TimeZone
-     * @param locale  the locale, null means use the default Locale
-     * @return a pattern based date/time formatter
-     * @throws IllegalArgumentException if pattern is invalid
-     *  or <code>null</code>
-     */
-    public F getInstance(final String pattern, TimeZone timeZone, Locale locale) {
-        if (pattern == null) {
-            throw new NullPointerException("pattern must not be null");
-        }
-        if (timeZone == null) {
-            timeZone = TimeZone.getDefault();
-        }
-        if (locale == null) {
-            locale = Locale.getDefault();
-        }
-        final MultipartKey key = new MultipartKey(pattern, timeZone, locale);
-        F format = cInstanceCache.get(key);
-        if (format == null) {
-            format = createInstance(pattern, timeZone, locale);
-            final F previousValue = cInstanceCache.putIfAbsent(key, format);
-            if (previousValue != null) {
-                // another thread snuck in and did the same work
-                // we should return the instance that is in ConcurrentMap
-                format = previousValue;
-            }
-        }
-        return format;
-    }
-
-    /**
-     * <p>Create a format instance using the specified pattern, time zone
-     * and locale.</p>
-     *
-     * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern, this will not be null.
-     * @param timeZone  time zone, this will not be null.
-     * @param locale  locale, this will not be null.
-     * @return a pattern based date/time formatter
-     * @throws IllegalArgumentException if pattern is invalid
-     *  or <code>null</code>
-     */
-    protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale);
-
-    /**
-     * <p>Gets a date/time formatter instance using the specified style,
-     * time zone and locale.</p>
-     *
-     * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format
-     * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date, null means use default Locale
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard date/time formatter
-     * @throws IllegalArgumentException if the Locale has no date/time
-     *  pattern defined
-     */
-    // This must remain private, see LANG-884
-    private F getDateTimeInstance(
-            final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) {
-        if (locale == null) {
-            locale = Locale.getDefault();
-        }
-        final String pattern = getPatternForStyle(dateStyle, timeStyle, locale);
-        return getInstance(pattern, timeZone, locale);
-    }
-
-    /**
-     * <p>Gets a date/time formatter instance using the specified style,
-     * time zone and locale.</p>
-     *
-     * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date, null means use default Locale
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard date/time formatter
-     * @throws IllegalArgumentException if the Locale has no date/time
-     *  pattern defined
-     */
-    // package protected, for access from FastDateFormat; do not make public or protected
-    F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
-        return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale);
-    }
-
-    /**
-     * <p>Gets a date formatter instance using the specified style,
-     * time zone and locale.</p>
-     *
-     * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date, null means use default Locale
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard date/time formatter
-     * @throws IllegalArgumentException if the Locale has no date/time
-     *  pattern defined
-     */
-    // package protected, for access from FastDateFormat; do not make public or protected
-    F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) {
-        return getDateTimeInstance(Integer.valueOf(dateStyle), null, timeZone, locale);
-    }
-
-    /**
-     * <p>Gets a time formatter instance using the specified style,
-     * time zone and locale.</p>
-     *
-     * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT
-     * @param timeZone  optional time zone, overrides time zone of
-     *  formatted date, null means use default Locale
-     * @param locale  optional locale, overrides system locale
-     * @return a localized standard date/time formatter
-     * @throws IllegalArgumentException if the Locale has no date/time
-     *  pattern defined
-     */
-    // package protected, for access from FastDateFormat; do not make public or protected
-    F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) {
-        return getDateTimeInstance(null, Integer.valueOf(timeStyle), timeZone, locale);
-    }
-
-    /**
-     * <p>Gets a date/time format for the specified styles and locale.</p>
-     *
-     * @param dateStyle  date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format
-     * @param timeStyle  time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format
-     * @param locale  The non-null locale of the desired format
-     * @return a localized standard date/time format
-     * @throws IllegalArgumentException if the Locale has no date/time pattern defined
-     */
-    // package protected, for access from test code; do not make public or protected
-    static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) {
-        final MultipartKey key = new MultipartKey(dateStyle, timeStyle, locale);
-
-        String pattern = cDateTimeInstanceCache.get(key);
-        if (pattern == null) {
-            try {
-                final DateFormat formatter;
-                if (dateStyle == null) {
-                    formatter = DateFormat.getTimeInstance(timeStyle.intValue(), locale);
-                } else if (timeStyle == null) {
-                    formatter = DateFormat.getDateInstance(dateStyle.intValue(), locale);
-                } else {
-                    formatter = DateFormat.getDateTimeInstance(dateStyle.intValue(), timeStyle.intValue(), locale);
-                }
-                pattern = ((SimpleDateFormat) formatter).toPattern();
-                final String previous = cDateTimeInstanceCache.putIfAbsent(key, pattern);
-                if (previous != null) {
-                    // even though it doesn't matter if another thread put the pattern
-                    // it's still good practice to return the String instance that is
-                    // actually in the ConcurrentMap
-                    pattern = previous;
-                }
-            } catch (final ClassCastException ex) {
-                throw new IllegalArgumentException("No date time pattern for locale: " + locale);
-            }
-        }
-        return pattern;
-    }
-
-    // ----------------------------------------------------------------------
-    /**
-     * <p>Helper class to hold multi-part Map keys</p>
-     */
-    private static class MultipartKey {
-        private final Object[] keys;
-        private int hashCode;
-
-        /**
-         * Constructs an instance of <code>MultipartKey</code> to hold the specified objects.
-         * @param keys the set of objects that make up the key.  Each key may be null.
-         */
-        public MultipartKey(final Object... keys) {
-            this.keys = keys;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public boolean equals(final Object obj) {
-            // Eliminate the usual boilerplate because
-            // this inner static class is only used in a generic ConcurrentHashMap
-            // which will not compare against other Object types
-            return Arrays.equals(keys, ((MultipartKey) obj).keys);
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public int hashCode() {
-            if (hashCode == 0) {
-                int rc = 0;
-                for (final Object key : keys) {
-                    if (key != null) {
-                        rc = rc * 7 + key.hashCode();
-                    }
-                }
-                hashCode = rc;
-            }
-            return hashCode;
-        }
-    }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/package-info.java
deleted file mode 100644
index aec2664fa4..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/package-info.java
+++ /dev/null
@@ -1,20 +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.
- */
-/**
- * Log4j 2 date formatting classes.
- */
-package org.apache.logging.log4j.core.time.internal.format;
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
deleted file mode 100644
index 7214e443a8..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DatePrinter.java
+++ /dev/null
@@ -1,133 +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.datetime;
-
-import java.text.FieldPosition;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * Use {@link org.apache.logging.log4j.core.time.internal.format.DatePrinter}.
- */
-@Deprecated
-public interface DatePrinter {
-
-    /**
-     * <p>Formats a millisecond {@code long} value.</p>
-     *
-     * @param millis  the millisecond value to format
-     * @return the formatted string
-     * @since 2.1
-     */
-    String format(long millis);
-
-    /**
-     * <p>Formats a {@code Date} object using a {@code GregorianCalendar}.</p>
-     *
-     * @param date  the date to format
-     * @return the formatted string
-     */
-    String format(Date date);
-
-    /**
-     * <p>Formats a {@code Calendar} object.</p>
-     * The TimeZone set on the Calendar is only used to adjust the time offset.
-     * The TimeZone specified during the construction of the Parser will determine the TimeZone
-     * used in the formatted string.
-     *
-     * @param calendar  the calendar to format.
-     * @return the formatted string
-     */
-    String format(Calendar calendar);
-
-    /**
-     * <p>Formats a millisecond {@code long} value into the
-     * supplied {@code Appendable}.</p>
-     *
-     * @param millis  the millisecond value to format
-     * @param buf  the buffer to format into
-     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    <B extends Appendable> B format(long millis, B buf);
-
-    /**
-     * <p>Formats a {@code Date} object into the
-     * supplied {@code Appendable} using a {@code GregorianCalendar}.</p>
-     *
-     * @param date  the date to format
-     * @param buf  the buffer to format into
-     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    <B extends Appendable> B format(Date date, B buf);
-
-    /**
-     * <p>Formats a {@code Calendar} object into the supplied {@code Appendable}.</p>
-     * The TimeZone set on the Calendar is only used to adjust the time offset.
-     * The TimeZone specified during the construction of the Parser will determine the TimeZone
-     * used in the formatted string.
-     *
-     * @param calendar  the calendar to format
-     * @param buf  the buffer to format into
-     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
-     * @return the specified string buffer
-     * @since 3.5
-     */
-    <B extends Appendable> B format(Calendar calendar, B buf);
-
-    // Accessors
-    // -----------------------------------------------------------------------
-    /**
-     * <p>Gets the pattern used by this printer.</p>
-     *
-     * @return the pattern, {@link java.text.SimpleDateFormat} compatible
-     */
-    String getPattern();
-
-    /**
-     * <p>Gets the time zone used by this printer.</p>
-     *
-     * <p>This zone is always used for {@code Date} printing. </p>
-     *
-     * @return the time zone
-     */
-    TimeZone getTimeZone();
-
-    /**
-     * <p>Gets the locale used by this printer.</p>
-     *
-     * @return the locale
-     */
-    Locale getLocale();
-
-    /**
-     * <p>Formats a {@code Date}, {@code Calendar} or
-     * {@code Long} (milliseconds) object.</p>
-     *
-     * @param obj  the object to format
-     * @param toAppendTo  the buffer to append to
-     * @param pos  the position - ignored
-     * @return the buffer passed in
-     * @see java.text.DateFormat#format(Object, StringBuffer, FieldPosition)
-     */
-    StringBuilder format(Object obj, StringBuilder toAppendTo, FieldPosition pos);
-}
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
deleted file mode 100644
index b15fd704dd..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
+++ /dev/null
@@ -1,207 +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.datetime;
-
-import java.text.FieldPosition;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- *
- * @deprecated Use {@link org.apache.logging.log4j.core.time.internal.format.FastDateFormat}
- */
-public class FastDateFormat extends Format implements DatePrinter {
-
-    private org.apache.logging.log4j.core.time.internal.format.FastDateFormat formatter = null;
-
-    public static FastDateFormat getInstance() {
-        return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance());
-    }
-
-    public static FastDateFormat getInstance(final String pattern) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(pattern));
-    }
-
-    public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(pattern, timeZone));
-    }
-
-    public static FastDateFormat getInstance(final String pattern, final Locale locale) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(pattern, locale));
-    }
-
-    public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
-        return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(
-                pattern, timeZone, locale));
-    }
-
-    public static FastDateFormat getDateInstance(final int style) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateInstance(style));
-    }
-
-    public static FastDateFormat getDateInstance(final int style, final Locale locale) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateInstance(style, locale));
-    }
-
-    public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateInstance(style, timeZone));
-    }
-
-    public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone, final Locale locale) {
-        return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateInstance(
-                style, timeZone, locale));
-    }
-
-    public static FastDateFormat getTimeInstance(final int style) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getTimeInstance(style));
-    }
-
-    public static FastDateFormat getTimeInstance(final int style, final Locale locale) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getTimeInstance(style, locale));
-    }
-
-    public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone) {
-        return new FastDateFormat(
-                org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getTimeInstance(style, timeZone));
-    }
-
-    public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone, final Locale locale) {
-        return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getTimeInstance(
-                style, timeZone, locale));
-    }
-
-    public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle) {
-        return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
-                dateStyle, timeStyle));
-    }
-
-    public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final Locale locale) {
-        return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
-                dateStyle, timeStyle, locale));
-    }
-
-    public static FastDateFormat getDateTimeInstance(
-            final int dateStyle, final int timeStyle, final TimeZone timeZone) {
-        return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
-                dateStyle, timeStyle, timeZone));
-    }
-
-    public static FastDateFormat getDateTimeInstance(
-            final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
-        return new FastDateFormat(org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
-                dateStyle, timeStyle, timeZone, locale));
-    }
-
-    private FastDateFormat(final org.apache.logging.log4j.core.time.internal.format.FastDateFormat formatter) {
-        this.formatter = formatter;
-    }
-
-    protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) {
-        formatter = org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getInstance(
-                pattern, timeZone, locale);
-    }
-
-    protected FastDateFormat(
-            final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
-        formatter = org.apache.logging.log4j.core.time.internal.format.FastDateFormat.getDateTimeInstance(
-                pattern, timeZone, locale, centuryStart);
-    }
-
-    @Override
-    public StringBuilder format(final Object obj, final StringBuilder toAppendTo, final FieldPosition pos) {
-        return formatter.format(obj, toAppendTo, pos);
-    }
-
-    @Override
-    public String format(final long millis) {
-        return formatter.format(millis);
-    }
-
-    @Override
-    public String format(final Date date) {
-        return formatter.format(date);
-    }
-
-    @Override
-    public String format(final Calendar calendar) {
-        return formatter.format(calendar);
-    }
-
-    @Override
-    public <B extends Appendable> B format(final long millis, final B buf) {
-        return formatter.format(millis, buf);
-    }
-
-    @Override
-    public <B extends Appendable> B format(final Date date, final B buf) {
-        return formatter.format(date, buf);
-    }
-
-    @Override
-    public <B extends Appendable> B format(final Calendar calendar, final B buf) {
-        return formatter.format(calendar, buf);
-    }
-
-    @Override
-    public String getPattern() {
-        return formatter.getPattern();
-    }
-
-    @Override
-    public TimeZone getTimeZone() {
-        return formatter.getTimeZone();
-    }
-
-    @Override
-    public Locale getLocale() {
-        return formatter.getLocale();
-    }
-
-    public int getMaxLengthEstimate() {
-        return formatter.getMaxLengthEstimate();
-    }
-
-    @Override
-    public boolean equals(final Object obj) {
-        if (obj instanceof FastDateFormat == false) {
-            return false;
-        }
-        final FastDateFormat other = (FastDateFormat) obj;
-        // no need to check parser, as it has same invariants as printer
-        return formatter.equals(other.formatter);
-    }
-
-    @Override
-    public int hashCode() {
-        return formatter.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return formatter.toString();
-    }
-}
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/internal/instant/InstantFormatter.java
similarity index 54%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/Format.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantFormatter.java
index 38ea25e1f4..e53777813d 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/internal/instant/InstantFormatter.java
@@ -14,19 +14,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.core.util.datetime;
+package org.apache.logging.log4j.core.util.internal.instant;
 
-import java.text.FieldPosition;
+import static java.util.Objects.requireNonNull;
+
+import java.time.temporal.ChronoUnit;
+import org.apache.logging.log4j.core.time.Instant;
 
 /**
- * Use {@link org.apache.logging.log4j.core.time.internal.format.Format}.
+ * Contract for formatting {@link Instant}s.
+ *
+ * @since 2.25.0
  */
-@Deprecated
-public abstract class Format {
+public interface InstantFormatter {
+
+    /**
+     * @return the time precision of the formatted output
+     */
+    ChronoUnit getPrecision();
 
-    public final String format(final Object obj) {
-        return format(obj, new StringBuilder(), new FieldPosition(0)).toString();
+    default String format(final Instant instant) {
+        requireNonNull(instant, "instant");
+        final StringBuilder buffer = new StringBuilder();
+        formatTo(buffer, instant);
+        return buffer.toString();
     }
 
-    public abstract StringBuilder format(Object obj, StringBuilder toAppendTo, FieldPosition pos);
+    void formatTo(StringBuilder buffer, Instant instant);
 }
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
new file mode 100644
index 0000000000..0ca4a982b3
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantNumberFormatter.java
@@ -0,0 +1,128 @@
+/*
+ * 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.function.BiConsumer;
+import org.apache.logging.log4j.core.time.Instant;
+
+/**
+ * Formats an {@link Instant} numerically; e.g., formats its epoch<sup>1</sup> seconds.
+ * <p>
+ * <sup>1</sup> Epoch is a fixed instant on {@code 1970-01-01Z}.
+ * </p>
+ * <h2>Internal usage only!</h2>
+ * <p>
+ * This class is intended only for internal Log4j usage.
+ * <b>Log4j users should not use this class!</b>
+ * This class is not subject to any backward compatibility concerns.
+ * </p>
+ *
+ * @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<Instant, StringBuilder> formatter;
+
+    InstantNumberFormatter(final ChronoUnit precision, final BiConsumer<Instant, StringBuilder> 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/instant/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
new file mode 100644
index 0000000000..9c93dd3406
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
@@ -0,0 +1,831 @@
+/*
+ * 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.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * An {@link InstantPatternFormatter} that uses {@link DateTimeFormatter} under the hood.
+ * 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.
+ * <p>
+ * For instance, given the pattern {@code yyyy-MM-dd'T'HH:mm:ss.SSSX}, the generated formatter will
+ * </p>
+ * <ol>
+ * <li>Sequence the pattern and assign a time precision to each part (e.g., {@code MM} is of month precision)</li>
+ * <li>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</li>
+ * <li>Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss.SSS})</li>
+ * </ol>
+ * <h2>Implementation note</h2>
+ * <p>
+ * Formatting can actually even be made faster and garbage-free by manually formatting sub-minute precision directives as follows:
+ * </p>
+ * <pre>{@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();
+ * }</pre>
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @since 2.25.0
+ */
+final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
+
+    static final ChronoUnit PRECISION_THRESHOLD = ChronoUnit.MINUTES;
+
+    private final AtomicReference<TimestampedFormatter> 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<PatternSequence> sequences = sequencePattern(pattern, precisionThreshold);
+        final List<InstantPatternFormatter> 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<PatternSequence> sequencePattern(final String pattern, final ChronoUnit precisionThreshold) {
+        List<PatternSequence> sequences = sequencePattern(pattern);
+        final List<PatternSequence> mergedSequences = mergeDynamicSequences(sequences, precisionThreshold);
+        return mergeConsequentEffectivelyConstantSequences(mergedSequences, precisionThreshold);
+    }
+
+    private static List<PatternSequence> sequencePattern(final String pattern) {
+        if (pattern.isEmpty()) {
+            return Collections.emptyList();
+        }
+        final List<PatternSequence> 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.
+     *
+     * <p>
+     * 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.
+     * </p>
+     *
+     * <h2>Example</h2>
+     *
+     * <p>
+     * The {@code [MM-dd] HH:mm} pattern will result in following sequences:
+     * </p>
+     *
+     * <pre>{@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)
+     * ]
+     * }</pre>
+     *
+     * <p>
+     * The above sequencing implies creation of 9 {@link AbstractFormatter}s.
+     * This method transforms it to the following:
+     * </p>
+     *
+     * <pre>{@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)
+     * ]
+     * }</pre>
+     *
+     * <p>
+     * The above sequencing implies creation of 8 {@link AbstractFormatter}s.
+     * </p>
+     *
+     * @param sequences sequences to be transformed
+     * @return transformed sequencing where consequent static sequences are merged
+     */
+    private static List<PatternSequence> mergeConsequentStaticPatternSequences(final List<PatternSequence> sequences) {
+
+        // Short-circuit if there is nothing to merge
+        if (sequences.size() < 2) {
+            return sequences;
+        }
+
+        final List<PatternSequence> mergedSequences = new ArrayList<>();
+        final List<StaticPatternSequence> 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<PatternSequence> mergedSequences, final List<StaticPatternSequence> 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.
+     *
+     * <p>
+     * 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}.
+     * </p>
+     *
+     * <h2>Example</h2>
+     *
+     * <p>
+     * Assume {@link #mergeConsequentStaticPatternSequences(List)} produced the following:
+     * </p>
+     *
+     * <pre>{@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),
+     * ]
+     * }</pre>
+     *
+     * <p>
+     * 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:
+     * </p>
+     *
+     * <pre>{@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),
+     * ]
+     * }</pre>
+     *
+     * <p>
+     * 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}.
+     * </p>
+     *
+     * @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<PatternSequence> mergeDynamicSequences(
+            final List<PatternSequence> 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<PatternSequence> 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.
+     *
+     * <p>
+     * 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.
+     * </p>
+     *
+     * <h2>Example</h2>
+     *
+     * <p>
+     * Assume {@link #mergeDynamicSequences(List, ChronoUnit)} produced the following:
+     * </p>
+     *
+     * <pre>{@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),
+     * ]
+     * }</pre>
+     *
+     * <p>
+     * The above sequencing implies creation of 12 {@link AbstractFormatter}s.
+     * This method transforms it to the following:
+     * </p>
+     *
+     * <pre>{@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),
+     * ]
+     * }</pre>
+     *
+     * <p>
+     * The resultant sequencing effectively translates to 3 {@link AbstractFormatter}s.
+     * </p>
+     *
+     * @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<PatternSequence> mergeConsequentEffectivelyConstantSequences(
+            final List<PatternSequence> sequences, final ChronoUnit precisionThreshold) {
+
+        // Short-circuit if there is nothing to merge
+        if (sequences.size() < 2) {
+            return sequences;
+        }
+
+        final List<PatternSequence> mergedSequences = new ArrayList<>();
+        boolean accumulatorConstant = true;
+        final List<PatternSequence> 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<PatternSequence> mergedSequences, final List<PatternSequence> accumulatedSequences) {
+        mergeAccumulatedSequences(
+                mergedSequences, accumulatedSequences, () -> new CompositePatternSequence(accumulatedSequences));
+    }
+
+    private static <S extends PatternSequence> void mergeAccumulatedSequences(
+            final List<PatternSequence> mergedSequences,
+            final List<S> accumulatedSequences,
+            final Supplier<PatternSequence> 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<PatternSequence> 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<PatternSequence> sequences) {
+            return sequences.stream()
+                    .map(sequence -> sequence.precision)
+                    .min(Comparator.comparing(ChronoUnit::getDuration))
+                    .get();
+        }
+
+        private static String concatSequencePatterns(List<PatternSequence> 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/instant/InstantPatternFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
new file mode 100644
index 0000000000..037e79a009
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternFormatter.java
@@ -0,0 +1,166 @@
+/*
+ * 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 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.util.Constants;
+
+/**
+ * Contract for formatting {@link Instant}s using a date and time formatting pattern.
+ * <h2>Internal usage only!</h2>
+ * <p>
+ * This class is intended only for internal Log4j usage.
+ * <b>Log4j users should not use this class!</b>
+ * This class is not subject to any backward compatibility concerns.
+ * </p>
+ *
+ * @since 2.25.0
+ */
+public interface InstantPatternFormatter extends InstantFormatter {
+
+    String getPattern();
+
+    Locale getLocale();
+
+    TimeZone getTimeZone();
+
+    static Builder newBuilder() {
+        return new Builder();
+    }
+
+    final class Builder {
+
+        private String pattern;
+
+        private Locale locale = Locale.getDefault();
+
+        private TimeZone timeZone = TimeZone.getDefault();
+
+        private boolean cachingEnabled = Constants.ENABLE_THREADLOCALS;
+
+        private Builder() {}
+
+        public String getPattern() {
+            return pattern;
+        }
+
+        public Builder setPattern(final String pattern) {
+            this.pattern = pattern;
+            return this;
+        }
+
+        public Locale getLocale() {
+            return locale;
+        }
+
+        public Builder setLocale(final Locale locale) {
+            this.locale = locale;
+            return this;
+        }
+
+        public TimeZone getTimeZone() {
+            return timeZone;
+        }
+
+        public Builder setTimeZone(final TimeZone timeZone) {
+            this.timeZone = timeZone;
+            return this;
+        }
+
+        public boolean isCachingEnabled() {
+            return cachingEnabled;
+        }
+
+        public Builder setCachingEnabled(boolean cachingEnabled) {
+            this.cachingEnabled = cachingEnabled;
+            return this;
+        }
+
+        public InstantPatternFormatter build() {
+
+            // 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);
+            }
+
+            // 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
+                case NANOS:
+                case MICROS:
+                    return formatter;
+
+                    // Millisecond precision cache
+                case MILLIS:
+                    return InstantPatternThreadLocalCachedFormatter.ofMilliPrecision(formatter);
+
+                    // Cache everything else with second precision
+                default:
+                    return InstantPatternThreadLocalCachedFormatter.ofSecondPrecision(formatter);
+            }
+        }
+
+        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/InstantPatternThreadLocalCachedFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatter.java
new file mode 100644
index 0000000000..96bf4504aa
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatter.java
@@ -0,0 +1,134 @@
+/*
+ * 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.Locale;
+import java.util.TimeZone;
+import java.util.function.Function;
+import org.apache.logging.log4j.core.time.Instant;
+
+/**
+ * An {@link InstantFormatter} wrapper caching the last formatted output in a {@link ThreadLocal} and trying to reuse it.
+ *
+ * @since 2.25.0
+ */
+final class InstantPatternThreadLocalCachedFormatter implements InstantPatternFormatter {
+
+    private final InstantPatternFormatter formatter;
+
+    private final Function<Instant, Long> epochInstantExtractor;
+
+    private final ThreadLocal<Object[]> 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<Instant, Long> 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-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/internal/instant/package-info.java
similarity index 61%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/package-info.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/package-info.java
index f801bb4b4e..3c87d12589 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/internal/instant/package-info.java
@@ -15,11 +15,23 @@
  * limitations under the license.
  */
 /**
- * Log4j 2 date formatting classes.
+ * Utilities for formatting log event {@link org.apache.logging.log4j.core.time.Instant}s.
+ * <h2>Internal usage only!</h2>
+ * <p>
+ * This package is intended only for internal Log4j usage.
+ * <b>Log4j users should not use this package!</b>
+ * This package is not subject to any backward compatibility concerns.
+ * </p>
+ *
+ * @since 2.25.0
  */
 @Export
-@Version("3.0.0")
-package org.apache.logging.log4j.core.util.datetime;
+@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;
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 21daa06819..bc9a36094d 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.MutableThreadContextStack;
 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<LogEvent> createLiteLogEvents(final int logEventCount) {
+    public static List<LogEvent> createLiteLogEvents(final int logEventCount) {
+        return createLiteLogEvents(logEventCount, TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT);
+    }
+
+    public static List<LogEvent> createLiteLogEvents(
+            final int logEventCount, final int timeOverlappingConsecutiveEventCount) {
         final List<LogEvent> 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 @@ final class LogEventFixture {
                 .build();
     }
 
-    static List<LogEvent> createFullLogEvents(final int logEventCount) {
+    public static List<LogEvent> createFullLogEvents(final int logEventCount) {
+        return createFullLogEvents(logEventCount, TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT);
+    }
+
+    public static List<LogEvent> createFullLogEvents(
+            final int logEventCount, final int timeOverlappingConsecutiveEventCount) {
         final List<LogEvent> 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/main/java/org/apache/logging/log4j/layout/template/json/package-info.java b/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java
deleted file mode 100644
index d4d9924283..0000000000
--- a/log4j-layout-template-json-test/src/main/java/org/apache/logging/log4j/layout/template/json/package-info.java
+++ /dev/null
@@ -1,22 +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.
- */
-@Export
-@Version("2.20.1")
-package org.apache.logging.log4j.layout.template.json;
-
-import org.osgi.annotation.bundle.Export;
-import org.osgi.annotation.versioning.Version;
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 a9d0870e2f..f6bc105057 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,8 +20,9 @@ import java.util.Locale;
 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.time.InstantFormatter;
-import org.apache.logging.log4j.core.time.MutableInstant;
+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.JsonTemplateLayoutProperties;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 
@@ -53,15 +54,14 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
  * rounded       = "rounded" -> boolean
  * </pre>
  *
- * If no configuration options are provided, <tt>pattern-config</tt> is
- * employed. There {@link
- * JsonTemplateLayoutProperties#timestampFormatPattern()}, {@link
- * JsonTemplateLayoutProperties#timeZone()}, {@link
- * JsonTemplateLayoutProperties#locale()} are used as defaults for
- * <tt>pattern</tt>, <tt>timeZone</tt>, and <tt>locale</tt>, respectively.
+ * <p>
+ * If no configuration options are provided, <tt>pattern-config</tt> is employed.
+ * There {@link JsonTemplateLayoutProperties#timestampFormatPattern()}, {@link JsonTemplateLayoutProperties#timeZone()}, {@link JsonTemplateLayoutProperties#locale()} are used as defaults for <tt>pattern</tt>, <tt>timeZone</tt>, and <tt>locale</tt>, respectively.
+ * </p>
  *
- * In <tt>epoch-config</tt>, <tt>millis.nanos</tt>, <tt>secs.nanos</tt> stand
- * for the fractional component in nanoseconds.
+ * <p>
+ * In <tt>epoch-config</tt>, <tt>millis.nanos</tt>, <tt>secs.nanos</tt> stand for the fractional component in nanoseconds.
+ * </p>
  *
  * <h3>Examples</h3>
  *
@@ -207,100 +207,55 @@ public final class TimestampResolver implements EventResolver {
         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 : config.getDefaults().timestampFormatPattern();
+    }
 
-        private static String readPattern(final TemplateResolverConfig config) {
-            final String format = config.getString(new String[] {"pattern", "format"});
-            return format != null ? format : config.getDefaults().timestampFormatPattern();
+    private static TimeZone readTimeZone(final TemplateResolverConfig config) {
+        final String timeZoneId = config.getString(new String[] {"pattern", "timeZone"});
+        if (timeZoneId == null) {
+            return config.getDefaults().timeZone();
         }
-
-        private static TimeZone readTimeZone(final TemplateResolverConfig config) {
-            final String timeZoneId = config.getString(new String[] {"pattern", "timeZone"});
-            if (timeZoneId == null) {
-                return config.getDefaults().timeZone();
-            }
-            boolean found = false;
-            for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
-                if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
-                    found = true;
-                    break;
-                }
+        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);
-            }
-            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 PatternResolverContext patternResolverContext;
+        private final InstantFormatter formatter;
 
-        private PatternResolver(final PatternResolverContext patternResolverContext) {
-            this.patternResolverContext = patternResolverContext;
+        private PatternResolver(final InstantFormatter formatter) {
+            this.formatter = formatter;
         }
 
         @Override
-        public synchronized void resolve(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);
-            }
+        public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
+            jsonWriter.writeString(formatter::formatTo, logEvent.getInstant());
         }
     }
 
-    private static EventResolver createPatternResolver(final TemplateResolverConfig config) {
-        final PatternResolverContext patternResolverContext = PatternResolverContext.fromConfig(config);
-        return new PatternResolver(patternResolverContext);
-    }
-
     private static EventResolver createEpochResolver(final TemplateResolverConfig config) {
         final String unit = config.getString(new String[] {"epoch", "unit"});
         final Boolean rounded = config.getBoolean(new String[] {"epoch", "rounded"});
@@ -318,108 +273,48 @@ public final class TimestampResolver implements EventResolver {
         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 EpochResolutionRecord resolutionRecord = new EpochResolutionRecord();
-
-        @Override
-        public synchronized void resolve(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-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
deleted file mode 100644
index 8f7522bb77..0000000000
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/DateTimeFormatBenchmark.java
+++ /dev/null
@@ -1,117 +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.time.Instant;
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.TimeZone;
-import java.util.stream.IntStream;
-import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.time.internal.format.FastDatePrinter;
-import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat;
-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}.
- * <p>
- * 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.
- */
-@State(Scope.Thread)
-public class DateTimeFormatBenchmark {
-
-    /**
-     * The pattern to be tested.
-     * <p>
-     * Note that neither {@link FastDatePrinter}, nor {@link FixedDateFormat}
-     * supports nanosecond precision.
-     */
-    private static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";
-
-    private static final Locale LOCALE = Locale.US;
-
-    private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC");
-
-    private static final Instant INIT_INSTANT = Instant.parse("2020-05-14T10:44:23.901Z");
-
-    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 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);
-
-    private static final FastDatePrinter FAST_DATE_PRINTER = new FastDatePrinter(PATTERN, TIME_ZONE, LOCALE) {};
-
-    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());
-
-    private static final DateTimeFormatter DATE_TIME_FORMATTER =
-            DateTimeFormatter.ofPattern(PATTERN).withZone(TIME_ZONE.toZoneId()).withLocale(LOCALE);
-
-    private final StringBuilder stringBuilder = new StringBuilder(PATTERN.length() * 2);
-
-    private final char[] charBuffer = new char[stringBuilder.capacity()];
-
-    @Benchmark
-    public void fastDatePrinter(final Blackhole blackhole) {
-        for (final Calendar calendar : CALENDARS) {
-            stringBuilder.setLength(0);
-            FAST_DATE_PRINTER.format(calendar, stringBuilder);
-            blackhole.consume(stringBuilder.length());
-        }
-    }
-
-    @Benchmark
-    public void fixedDateFormat(final Blackhole blackhole) {
-        for (final MutableInstant instant : INSTANTS) {
-            final int length = FIXED_DATE_FORMAT.formatInstant(instant, charBuffer, 0);
-            blackhole.consume(length);
-        }
-    }
-
-    @Benchmark
-    public void dateTimeFormatter(final Blackhole blackhole) {
-        for (final MutableInstant instant : INSTANTS) {
-            stringBuilder.setLength(0);
-            DATE_TIME_FORMATTER.formatTo(instant, stringBuilder);
-            blackhole.consume(stringBuilder.length());
-        }
-    }
-}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
deleted file mode 100644
index e22ffa31c7..0000000000
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
+++ /dev/null
@@ -1,241 +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.text.SimpleDateFormat;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.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 DateFormatters in a thread-safe manner.
- */
-// ============================== HOW TO RUN THIS TEST: ====================================
-//
-// single thread:
-// java -jar log4j-perf/target/benchmarks.jar ".*ThreadsafeDateFormat.*" -f 1 -wi 5 -i 10
-//
-// multiple threads (for example, 4 threads):
-// java -jar log4j-perf/target/benchmarks.jar ".*ThreadsafeDateFormat.*" -f 1 -wi 5 -i 10 -t 4 -si true
-//
-// Usage help:
-// java -jar log4j-perf/target/benchmarks.jar -help
-//
-@State(Scope.Benchmark)
-public class ThreadsafeDateFormatBenchmark {
-
-    private final Date date = new Date();
-    private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
-    private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
-    private final ThreadLocal<SimpleDateFormat> threadLocalSDFormat = new ThreadLocal<SimpleDateFormat>() {
-        @Override
-        protected SimpleDateFormat initialValue() {
-            return new SimpleDateFormat("HH:mm:ss.SSS");
-        }
-    };
-
-    private final ThreadLocal<FormatterSimple> threadLocalCachedSDFormat = new ThreadLocal<FormatterSimple>() {
-        @Override
-        protected FormatterSimple initialValue() {
-            return new FormatterSimple();
-        }
-    };
-
-    private final FastDateFormat fastDateFormat = FastDateFormat.getInstance("HH:mm:ss.SSS");
-    private final FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported("HH:mm:ss.SSS");
-    private final FormatterFixedReuseBuffer formatFixedReuseBuffer = new FormatterFixedReuseBuffer();
-
-    private class CachedTimeFastFormat {
-        private final long timestamp;
-        private final String formatted;
-
-        public CachedTimeFastFormat(final long timestamp) {
-            this.timestamp = timestamp;
-            this.formatted = fastDateFormat.format(timestamp);
-        }
-    }
-
-    private class CachedTimeFixedFmt {
-        private final long timestamp;
-        private final String formatted;
-
-        public CachedTimeFixedFmt(final long timestamp) {
-            this.timestamp = timestamp;
-            this.formatted = fixedDateFormat.format(timestamp);
-        }
-    }
-
-    private class FormatterSimple {
-        private final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.SSS");
-        private long timestamp;
-        private String formatted;
-
-        public FormatterSimple() {
-            this.timestamp = 0;
-        }
-
-        public String format(final long timestamp) {
-            if (timestamp != this.timestamp) {
-                this.timestamp = timestamp;
-                formatted = format.format(timestamp);
-            }
-            return formatted;
-        }
-    }
-
-    private class FormatterFixedReuseBuffer {
-        private final FixedDateFormat customFormat = FixedDateFormat.createIfSupported("HH:mm:ss.SSS");
-        private long timestamp;
-        private String formatted;
-        private final ThreadLocal<char[]> reusableBuffer = new ThreadLocal<char[]>() {
-            @Override
-            protected char[] initialValue() {
-                return new char[255];
-            }
-        };
-
-        public FormatterFixedReuseBuffer() {
-            this.timestamp = 0;
-        }
-
-        public String format(final long timestamp) {
-            if (timestamp != this.timestamp) {
-                this.timestamp = timestamp;
-                final char[] buffer = reusableBuffer.get();
-                final int len = customFormat.format(timestamp, buffer, 0);
-                formatted = new String(buffer, 0, len);
-            }
-            return formatted;
-        }
-    }
-
-    private final long currentTimestamp = 0;
-    private String cachedTime = null;
-
-    private final AtomicReference<CachedTimeFastFormat> cachedTimeFastFmt =
-            new AtomicReference<>(new CachedTimeFastFormat(System.currentTimeMillis()));
-    private final AtomicReference<CachedTimeFixedFmt> cachedTimeFixedFmt =
-            new AtomicReference<>(new CachedTimeFixedFmt(System.currentTimeMillis()));
-
-    public static void main(final String[] args) {}
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public void baseline() {}
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String synchronizedSimpleDateFmt() {
-        final long timestamp = System.currentTimeMillis();
-        synchronized (simpleDateFormat) {
-            if (timestamp != currentTimestamp) {
-                cachedTime = simpleDateFormat.format(date);
-            }
-            return cachedTime;
-        }
-    }
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String dateTimeFormatter() {
-        final LocalDateTime now = LocalDateTime.now();
-        return dateTimeFormatter.format(now);
-    }
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String threadLocalSimpleDateFmt() {
-        final long timestamp = System.currentTimeMillis();
-        return threadLocalSDFormat.get().format(timestamp);
-    }
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String cachedThrdLocalSimpleDateFmt() {
-        final long timestamp = System.currentTimeMillis();
-        return threadLocalCachedSDFormat.get().format(timestamp);
-    }
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String cachedThrdLocalCustomFormat() {
-        final long timestamp = System.currentTimeMillis();
-        return formatFixedReuseBuffer.format(timestamp);
-    }
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String fastDateFormat() {
-        return fastDateFormat.format(System.currentTimeMillis());
-    }
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String fixedDateFormat() {
-        return fixedDateFormat.format(System.currentTimeMillis());
-    }
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String atomicFastFormat() {
-        final long timestamp = System.currentTimeMillis();
-        final CachedTimeFastFormat current = cachedTimeFastFmt.get();
-        if (timestamp != current.timestamp) {
-            final CachedTimeFastFormat newTime = new CachedTimeFastFormat(timestamp);
-            if (cachedTimeFastFmt.compareAndSet(current, newTime)) {
-                return newTime.formatted;
-            }
-            return cachedTimeFastFmt.get().formatted;
-        }
-        return current.formatted;
-    }
-
-    @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
-    @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    public String atomicFixedFormat() {
-        final long timestamp = System.currentTimeMillis();
-        final CachedTimeFixedFmt current = cachedTimeFixedFmt.get();
-        if (timestamp != current.timestamp) {
-            final CachedTimeFixedFmt newTime = new CachedTimeFixedFmt(timestamp);
-            if (cachedTimeFixedFmt.compareAndSet(current, newTime)) {
-                return newTime.formatted;
-            }
-            return cachedTimeFixedFmt.get().formatted;
-        }
-        return current.formatted;
-    }
-}
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 90d970f4bc..0000000000
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
+++ /dev/null
@@ -1,310 +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.time.internal.format.FastDateFormat;
-import org.apache.logging.log4j.core.time.internal.format.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.
- */
-// ============================== HOW TO RUN THIS TEST: ====================================
-//
-// single thread:
-// java -jar log4j-perf/target/benchmarks.jar ".*TimeFormat.*" -f 1 -wi 5 -i 10
-//
-// multiple threads (for example, 4 threads):
-// java -jar log4j-perf/target/benchmarks.jar ".*TimeFormat.*" -f 1 -wi 5 -i 5 -t 4 -si true
-//
-// Usage help:
-// java -jar log4j-perf/target/benchmarks.jar -help
-//
-@State(Scope.Benchmark)
-public class TimeFormatBenchmark {
-
-    ThreadLocal<SimpleDateFormat> threadLocalSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
-        @Override
-        protected SimpleDateFormat initialValue() {
-            return new SimpleDateFormat("HH:mm:ss.SSS");
-        }
-    };
-    FastDateFormat fastDateFormat = FastDateFormat.getInstance("HH:mm:ss.SSS");
-    FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(new String[] {"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;
-    }
-}
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 0000000000..d46c431ea0
--- /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 {@code 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/instant/InstantPatternFormatterBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
new file mode 100644
index 0000000000..0506318acd
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterBenchmark.java
@@ -0,0 +1,151 @@
+/*
+ * 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.util.Arrays;
+import java.util.Locale;
+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.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 InstantPatternFormatter} and {@link DateTimeFormatter}.
+ * <p>
+ * The major formatting efficiency is mostly provided by caching, i.e., reusing the earlier formatter output if timestamps match.
+ * 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.
+ * </p>
+ *
+ * @see InstantPatternFormatterImpactBenchmark for the performance impact of different date & time formatters on a typical layout
+ */
+@State(Scope.Thread)
+public class InstantPatternFormatterBenchmark {
+
+    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 Instant instant = initInstant.plusMillis(index).plusNanos(1);
+                    final MutableInstant mutableInstant = new MutableInstant();
+                    mutableInstant.initFromEpochSecond(instant.getEpochSecond(), instant.getNano());
+                    return mutableInstant;
+                })
+                .toArray(MutableInstant[]::new);
+        validateInstants(instants);
+        return instants;
+    }
+
+    @SuppressWarnings("OptionalGetWithoutIsPresent")
+    static <I extends org.apache.logging.log4j.core.time.Instant> void validateInstants(final I[] instants) {
+
+        // Find the instant offset
+        final Supplier<LongStream> 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 `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",
+                    InstantPatternFormatter.class.getSimpleName());
+            throw new IllegalStateException(message);
+        }
+    }
+
+    private static final Formatters DATE_TIME_FORMATTERS = new Formatters("yyyy-MM-dd'T'HH:mm:ss.SSS");
+
+    private static final Formatters TIME_FORMATTERS = new Formatters("HH:mm:ss.SSS");
+
+    static final class Formatters {
+
+        private final String pattern;
+
+        final InstantPatternFormatter instantFormatter;
+
+        final DateTimeFormatter javaFormatter;
+
+        Formatters(final String pattern) {
+            this.pattern = pattern;
+            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);
+        }
+    }
+
+    private final StringBuilder stringBuilder =
+            new StringBuilder(Math.max(DATE_TIME_FORMATTERS.pattern.length(), TIME_FORMATTERS.pattern.length()) * 2);
+
+    @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 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);
+            formatter.formatTo(instant, stringBuilder);
+            blackhole.consume(stringBuilder.length());
+        }
+    }
+}
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java
new file mode 100644
index 0000000000..53645ac7e6
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/instant/InstantPatternFormatterImpactBenchmark.java
@@ -0,0 +1,125 @@
+/*
+ * 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 static org.apache.logging.log4j.perf.jmh.instant.InstantPatternFormatterBenchmark.validateInstants;
+
+import java.time.format.DateTimeFormatter;
+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.Instant;
+import org.apache.logging.log4j.core.time.MutableInstant;
+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;
+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 InstantPatternFormatterBenchmark for isolated benchmarks of date & time formatters
+ */
+@State(Scope.Thread)
+@SuppressWarnings("deprecation")
+public class InstantPatternFormatterImpactBenchmark {
+
+    private static final List<LogEvent> LITE_LOG_EVENTS = createLogEvents(LogEventFixture::createLiteLogEvents);
+
+    private static final List<LogEvent> FULL_LOG_EVENTS = createLogEvents(LogEventFixture::createFullLogEvents);
+
+    private static List<LogEvent> createLogEvents(final BiFunction<Integer, Integer, List<LogEvent>> supplier) {
+        final int logEventCount = 1_000;
+        final List<LogEvent> logEvents = supplier.apply(
+                logEventCount,
+                // Avoid overlapping instants to ensure the impact of date & time formatting at event encoding:
+                1);
+        final Instant[] instants = logEvents.stream().map(LogEvent::getInstant).toArray(Instant[]::new);
+        validateInstants(instants);
+        return logEvents;
+    }
+
+    private static final PatternLayout LAYOUT = PatternLayout.newBuilder()
+            .setConfiguration(new NullConfiguration())
+            // Use a typical pattern *without* a date & time converter!
+            .setPattern("[%t] %p %-40.40c{1.} %notEmpty{%x }- %m%n")
+            .setAlwaysWriteExceptions(true)
+            .build();
+
+    private static final InstantPatternFormatterBenchmark.Formatters FORMATTERS =
+            new InstantPatternFormatterBenchmark.Formatters("yyyy-MM-dd'T'HH:mm:ss.SSS");
+
+    private final StringBuilder stringBuilder = new StringBuilder(1_1024 * 16);
+
+    @Benchmark
+    public void instantFormatter_lite(final Blackhole blackhole) {
+        instantFormatter(blackhole, LITE_LOG_EVENTS, FORMATTERS.instantFormatter);
+    }
+
+    @Benchmark
+    public void instantFormatter_full(final Blackhole blackhole) {
+        instantFormatter(blackhole, FULL_LOG_EVENTS, FORMATTERS.instantFormatter);
+    }
+
+    private void instantFormatter(
+            final Blackhole blackhole, final List<LogEvent> 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);
+    }
+
+    @Benchmark
+    public void javaFormatter_full(final Blackhole blackhole) {
+        javaFormatter(blackhole, FULL_LOG_EVENTS, FORMATTERS.javaFormatter);
+    }
+
+    private void javaFormatter(
+            final Blackhole blackhole, final List<LogEvent> 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());
+        }
+    }
+}
diff --git a/src/changelog/.3.x.x/3149_remove_named_date_patterns.xml b/src/changelog/.3.x.x/3149_remove_named_date_patterns.xml
new file mode 100644
index 0000000000..38c2bd8ae3
--- /dev/null
+++ b/src/changelog/.3.x.x/3149_remove_named_date_patterns.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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="removed">
+  <issue id="3149" link="https://github.com/apache/logging-log4j2/pull/3149"/>
+  <description format="asciidoc">Remove _named_ date &amp; time formatting patterns in Pattern Layout, except for `UNIX` and `UNIX_MILLIS`</description>
+</entry>
diff --git a/src/changelog/.index.adoc.ftl b/src/changelog/.index.adoc.ftl
index b71f9905f3..24c1ef3895 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/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index c754462477..ef0a1c1c26 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -387,7 +387,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]
@@ -396,8 +396,7 @@ d{pattern}
 date{pattern}
 ----
 
-The date conversion specifier may be followed by a set of braces containing a date and time pattern string per
-https://docs.oracle.com/en/java/javase/{java-target-version}/docs/api/java.base/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/en/java/javase/{java-target-version}/docs/api/java.base/java/time/format/DateTimeFormatter.html[`DateTimeFormatter`].
 The predefined _named_ formats are:
 
 [%header,cols="2m,3m"]
@@ -405,45 +404,6 @@ The predefined _named_ formats are:
 |Pattern
 |Example output
 
-|%d\{DEFAULT}
-|2012-11-02 14:34:02,123
-
-|%d\{DEFAULT_MICROS}
-|2012-11-02 14:34:02,123456
-
-|%d\{DEFAULT_NANOS}
-|2012-11-02 14:34:02,123456789
-
-|%d\{ISO8601}
-|2012-11-02T14:34:02,781
-
-|%d\{ISO8601_BASIC}
-|20121102T143402,781
-
-|%d\{ISO8601_OFFSET_DATE_TIME_HH}
-|2012-11-02'T'14:34:02,781-07
-
-|%d\{ISO8601_OFFSET_DATE_TIME_HHMM}
-|2012-11-02'T'14:34:02,781-0700
-
-|%d\{ISO8601_OFFSET_DATE_TIME_HHCMM}
-|2012-11-02'T'14:34:02,781-07:00
-
-|%d\{ABSOLUTE}
-|14:34:02,781
-
-|%d\{ABSOLUTE_MICROS}
-|14:34:02,123456
-
-|%d\{ABSOLUTE_NANOS}
-|14:34:02,123456789
-
-|%d\{DATE}
-|02 Nov 2012 14:34:02,781
-
-|%d\{COMPACT}
-|20121102143402781
-
 |%d\{UNIX}
 |1351866842
 
@@ -453,7 +413,7 @@ The predefined _named_ formats are:
 
 You can also use a set of braces containing a time zone id per
 https://docs.oracle.com/en/java/javase/{java-target-version}/docs/api/java.base/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.
+If no date format specifier is given, then the `yyyy-MM-dd HH:mm:ss.SSS` pattern is used.
 
 You can also define custom date formats, see following examples:
 
@@ -465,36 +425,19 @@ 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.
-`%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.
-
-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/en/java/javase/{java-target-version}/docs/api/java.base/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`.
+`%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.
 
-Users may revert to a millisecond-precision clock when running on Java 9 by setting xref:manual/systemproperties.adoc#log4j.configuration.clock[the `log4j.configuration.clock` system property] to `SystemMillisClock`.
+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]
 ====
-Only named date formats (`DEFAULT`, `ISO8601`, `UNIX`, `UNIX_MILLIS`, etc.) are garbage-free.
+Except `UNIX` and `UNIX_MILLIS` named patterns, the rest of the date & time formatters are not garbage-free.
 ====
 
 [#converter-encode]