You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by gg...@apache.org on 2018/12/05 00:22:22 UTC

[logging-log4j2] branch release-2.x updated: [LOG4J2-1246] PatternLayout %date conversion pattern should render time zone designator for ISO-ISO8601.

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

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


The following commit(s) were added to refs/heads/release-2.x by this push:
     new 8e4f428  [LOG4J2-1246] PatternLayout %date conversion pattern should render time zone designator for ISO-ISO8601.
8e4f428 is described below

commit 8e4f4286940fe2199221a90e72d006563fce4083
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Tue Dec 4 17:22:19 2018 -0700

    [LOG4J2-1246] PatternLayout %date conversion pattern should render time
    zone designator for ISO-ISO8601.
    
    - Add new format in pattern layout %d{ISO8601_OFFSET_DATE_TIME_X}
    - Add new format in pattern layout %d{ISO8601_OFFSET_DATE_TIME_XX}
    - Add new format in pattern layout %d{ISO8601_OFFSET_DATE_TIME_XXX}
    - Add new format in pattern layout %d{ISO8601_OFFSET_DATE_TIME_Z}
---
 .../log4j/core/pattern/DatePatternConverter.java   |   2 +-
 .../log4j/core/util/datetime/FixedDateFormat.java  | 224 +++++++++++++++++----
 .../rolling/OnStartupTriggeringPolicyTest.java     |   1 +
 .../core/pattern/DatePatternConverterTest.java     | 173 ++++++++++++----
 .../core/util/datetime/FixedDateFormatTest.java    |  54 +++--
 src/site/xdoc/manual/layouts.xml.vm                |  16 ++
 6 files changed, 373 insertions(+), 97 deletions(-)

diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java
index 703786b..fa5a8c3 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
@@ -88,7 +88,7 @@ public final class DatePatternConverter extends LogEventPatternConverter impleme
         private final FixedDateFormat fixedDateFormat;
 
         // below fields are only used in ThreadLocal caching mode
-        private final char[] cachedBuffer = new char[64]; // max length of formatted date-time in any format < 64
+        private final char[] cachedBuffer = new char[70]; // max length of formatted date-time in any format < 64
         private int length = 0;
 
         FixedFormatter(final FixedDateFormat fixedDateFormat) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
index f58a29b..c067314 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormat.java
@@ -31,6 +31,7 @@ import java.util.concurrent.TimeUnit;
  * <p>
  * Related benchmarks: /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java and
  * /log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadsafeDateFormatBenchmark.java
+ * </p>
  */
 public class FixedDateFormat {
 
@@ -38,78 +39,107 @@ 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),
+        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),
+        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),
+        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),
+        ABSOLUTE_PERIOD("HH:mm:ss.SSS", null, 0, ':', 1, '.', 1, 3, null),
 
         /**
          * COMPACT time format: {@code "yyyyMMddHHmmssSSS"}.
          */
-        COMPACT("yyyyMMddHHmmssSSS", "yyyyMMdd", 0, ' ', 0, ' ', 0, 3),
+        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),
+        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),
+        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),
+        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),
+        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),
+        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),
+        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),
+        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),
+        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),
+        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_OFFSET_DATE_TIME_X time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSX"}.
+         */
+        ISO8601_OFFSET_DATE_TIME_X("yyyy-MM-dd'T'HH:mm:ss,SSSX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.X),
+
+        /**
+         * ISO8601_OFFSET_DATE_TIME_XX time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSXX"}.
+         */
+        ISO8601_OFFSET_DATE_TIME_XX("yyyy-MM-dd'T'HH:mm:ss,SSSXX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.XX),
+
+        /**
+         * ISO8601_OFFSET_DATE_TIME_XXX time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSXXX"}.
+         */
+        ISO8601_OFFSET_DATE_TIME_XXX("yyyy-MM-dd'T'HH:mm:ss,SSSXXX", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.XXX),
+
+        /**
+         * ISO8601_OFFSET_DATE_TIME_Z time format: {@code "yyyy-MM-dd'T'HH:mm:ss,SSSZ"}.
+         */
+        ISO8601_OFFSET_DATE_TIME_Z("yyyy-MM-dd'T'HH:mm:ss,SSSZ", "yyyy-MM-dd'T'", 2, ':', 1, ',', 1, 3, FixedTimeZoneFormat.Z),
 
         /**
          * 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);
+        ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'", 2, ':', 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();
@@ -123,10 +153,11 @@ public class FixedDateFormat {
         private final char millisSeparatorChar;
         private final int millisSeparatorLength;
         private final int secondFractionDigits;
+        private final FixedTimeZoneFormat timeZoneFormat;
 
         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 int secondFractionDigits, final FixedTimeZoneFormat timeZoneFormat) {
             this.timeSeparatorChar = timeSeparator;
             this.timeSeparatorLength = timeSepLength;
             this.millisSeparatorChar = millisSeparator;
@@ -135,6 +166,7 @@ public class FixedDateFormat {
             this.datePattern = datePattern; // may be null
             this.escapeCount = escapeCount;
             this.secondFractionDigits = secondFractionDigits;
+            this.timeZoneFormat = timeZoneFormat;
         }
 
         /**
@@ -171,9 +203,12 @@ public class FixedDateFormat {
         }
 
         static FixedFormat lookupIgnoringNanos(final String pattern) {
-            final int nanoStart = nanoStart(pattern);
+            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;
+                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;
@@ -183,16 +218,25 @@ public class FixedDateFormat {
             return null;
         }
 
-        private static int nanoStart(final String pattern) {
-            final int index = pattern.indexOf(SECOND_FRACTION_PATTERN);
-            if (index >= 0) {
-                for (int i = index + 1; i < pattern.length(); i++) {
+        private final static 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 -1;
+                        return EMPTY_RANGE;
                     }
                 }
             }
-            return index;
+            return new int [] {indexStart, indexEnd};
         }
 
         /**
@@ -241,6 +285,100 @@ public class FixedDateFormat {
         public int getSecondFractionDigits() {
             return secondFractionDigits;
         }
+
+        /**
+         * Returns the optional time zone format.
+         * @return the optional time zone format, may be null.
+         */
+        public FixedTimeZoneFormat getTimeZoneFormat() {
+            return timeZoneFormat;
+        }
+    }
+
+    private static final char NONE = (char) 0;
+
+    /**
+     * Fixed time zone formats. The enum names are symbols from
+     * https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html.
+     */
+    public enum FixedTimeZoneFormat {
+
+        /**
+         * Offset like {@code -07}
+         */
+        X(NONE, false, 3),
+
+        /**
+         * Offset like {@code -0700}.
+         * Same as Z.
+         */
+        XX(NONE, true, 5), 
+        
+        /** 
+         * Offset like {@code -07:00}
+         */
+        XXX(':', true, 6), 
+        
+        /**
+         * Offset like {@code -0700}.
+         * Same as XX.
+         */
+        Z(NONE, true, 5); 
+
+        private FixedTimeZoneFormat() {
+            this(NONE, true, 4);
+        }
+
+        private FixedTimeZoneFormat(char timeSeparatorChar, boolean minutes, 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(int offset, final char[] buffer, int pos) {
+            // This method duplicates part of writeTime()
+
+            buffer[pos++] = offset < 0 ? '-' : '+';
+            final int absOffset = Math.abs(offset);
+            final int hours = absOffset / 3600000;
+            int ms = absOffset - (3600000 * hours);
+
+            // 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'));
+
+            // Minute
+            if (useMinutes) {
+                buffer[pos] = timeSeparatorChar;
+                pos += timeSeparatorCharLen;
+                final int minutes = ms / 60000;
+                ms -= 60000 * minutes;
+
+                temp = minutes / 10;
+                buffer[pos++] = ((char) (temp + '0'));
+
+                // Do subtract to get remainder instead of doing % 10
+                buffer[pos++] = ((char) (minutes - 10 * temp + '0'));
+            }
+            return pos;
+        }
+
     }
 
     private final FixedFormat fixedFormat;
@@ -252,6 +390,7 @@ public class FixedDateFormat {
     private final char millisSeparatorChar;
     private final int timeSeparatorLength;
     private final int millisSeparatorLength;
+    private final FixedTimeZoneFormat timeZoneFormat;
 
     private volatile long midnightToday = 0;
     private volatile long midnightTomorrow = 0;
@@ -282,6 +421,7 @@ public class FixedDateFormat {
      * Constructs a FixedDateFormat for the specified fixed format.
      * <p>
      * Package protected for unit tests.
+     * </p>
      *
      * @param fixedFormat the fixed format
      * @param tz time zone
@@ -294,6 +434,7 @@ public class FixedDateFormat {
         this.timeSeparatorLength = fixedFormat.timeSeparatorLength;
         this.millisSeparatorChar = fixedFormat.millisSeparatorChar;
         this.millisSeparatorLength = fixedFormat.millisSeparatorLength;
+        this.timeZoneFormat = fixedFormat.timeZoneFormat; // may be null
         this.length = fixedFormat.getLength();
         this.secondFractionDigits = Math.max(1, Math.min(9, secondFractionDigits));
         this.fastDateFormat = fixedFormat.getFastDateFormat(tz);
@@ -314,12 +455,16 @@ public class FixedDateFormat {
             tz = TimeZone.getDefault();
         }
 
-        final FixedFormat withNanos = FixedFormat.lookupIgnoringNanos(options[0]);
+        final String option0 = options[0];
+        final FixedFormat withNanos = FixedFormat.lookupIgnoringNanos(option0);
         if (withNanos != null) {
-            final int secondFractionDigits = options[0].length() - FixedFormat.nanoStart(options[0]);
+            final int[] nanoRange = FixedFormat.nanoRange(option0);
+            final int nanoStart = nanoRange[0];
+            final int nanoEnd = nanoRange[1];
+            final int secondFractionDigits = nanoEnd - nanoStart;
             return new FixedDateFormat(withNanos, tz, secondFractionDigits);
         }
-        final FixedFormat type = FixedFormat.lookup(options[0]);
+        final FixedFormat type = FixedFormat.lookup(option0);
         return type == null ? null : new FixedDateFormat(type, tz);
     }
 
@@ -358,7 +503,6 @@ public class FixedDateFormat {
      *
      * @return the time zone
      */
-
     public TimeZone getTimeZone() {
         return timeZone;
     }
@@ -436,20 +580,17 @@ public class FixedDateFormat {
     }
 
     public int formatInstant(final Instant instant, final char[] buffer, final int startPos) {
-        int result = format(instant.getEpochMillisecond(), buffer, startPos);
+        final long epochMillisecond = instant.getEpochMillisecond();
+        int result = format(epochMillisecond, buffer, startPos);
         result -= digitsLessThanThree();
-        formatNanoOfMillisecond(instant.getNanoOfMillisecond(), buffer, startPos + result);
-        return result + digitsMorePreciseThanMillis();
+        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);
     }
 
-    private int digitsMorePreciseThanMillis() {
-        return Math.max(0, secondFractionDigits - FixedFormat.MILLI_FRACTION_DIGITS);
-    }
-
     // 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) {
@@ -468,7 +609,8 @@ public class FixedDateFormat {
         // int ms = (int) (time % 86400000);
         final int ms = (int) (millisSinceMidnight(epochMillis));
         writeDate(buffer, startPos);
-        return writeTime(ms, buffer, startPos + dateLength) - startPos;
+        int pos = writeTime(ms, buffer, startPos + dateLength);
+        return pos - startPos;
     }
 
     // Profiling showed this method is important to log4j performance. Modify with care!
@@ -530,6 +672,13 @@ public class FixedDateFormat {
         return pos;
     }
 
+    private int writeTimeZone(final long epochMillis, final char[] buffer, int pos) {
+        if (timeZoneFormat != null) {
+            pos = timeZoneFormat.write(timeZone.getOffset(epochMillis), buffer, pos);
+        }
+        return pos;
+    }
+
     static int[] TABLE = {
             100000, // 0
             10000, // 1
@@ -539,7 +688,7 @@ public class FixedDateFormat {
             1, // 5
     };
 
-    private void formatNanoOfMillisecond(final int nanoOfMillisecond, final char[] buffer, int pos) {
+    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++) {
@@ -548,6 +697,7 @@ public class FixedDateFormat {
             buffer[pos++] = ((char) (temp + '0'));
             remain -= divisor * temp; // equivalent of remain % 10
         }
+        return pos;
     }
 
     private int daylightSavingTime(final int hourOfDay) {
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
index 5f26338..1ea3aa2 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/rolling/OnStartupTriggeringPolicyTest.java
@@ -79,6 +79,7 @@ public class OnStartupTriggeringPolicyTest {
         final FileTime fileTime = FileTime.fromMillis(timeStamp);
         final BasicFileAttributeView attrs = Files.getFileAttributeView(target, BasicFileAttributeView.class);
         attrs.setTimes(fileTime, fileTime, fileTime);
+
         final PatternLayout layout = PatternLayout.newBuilder().withPattern("%msg").withConfiguration(configuration)
                 .build();
         final RolloverStrategy strategy = DefaultRolloverStrategy.createStrategy(null, null, null, "0", null, true,
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
index affb5b2..e391917 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/pattern/DatePatternConverterTest.java
@@ -31,6 +31,8 @@ import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.core.time.Instant;
 import org.apache.logging.log4j.core.time.MutableInstant;
 import org.apache.logging.log4j.core.util.datetime.FixedDateFormat;
+import org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedTimeZoneFormat;
+import org.apache.logging.log4j.util.Strings;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -50,9 +52,19 @@ public class DatePatternConverterTest {
      */
     private static final String ISO8601_FORMAT = FixedDateFormat.FixedFormat.ISO8601.name();
 
+    /**
+     * ISO8601_OFFSET string literal.
+     */
+    private static final String ISO8601_OFFSET_DATE_TIME_XXX = FixedDateFormat.FixedFormat.ISO8601_OFFSET_DATE_TIME_XXX.name();
+
+    /**
+     * ISO8601_OFFSET_Z string literal.
+     */
+    private static final String ISO8601_OFFSET_Z_FORMAT = FixedDateFormat.FixedFormat.ISO8601_OFFSET_DATE_TIME_Z.name();
+
     private static final String[] ISO8601_FORMAT_OPTIONS = {ISO8601_FORMAT};
 
-    @Parameterized.Parameters
+    @Parameterized.Parameters(name = "threadLocalEnabled={0}")
     public static Collection<Object[]> data() {
         return Arrays.asList(new Object[][]{{Boolean.TRUE}, {Boolean.FALSE}});
     }
@@ -133,6 +145,49 @@ public class DatePatternConverterTest {
     }
 
     @Test
+    public void testFormatLogEventStringBuilderIso8601TimezoneZ() {
+        final LogEvent event = new MyLogEvent();
+        final String[] optionsWithTimezone = {ISO8601_FORMAT, "Z"};
+        final DatePatternConverter converter = DatePatternConverter.newInstance(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 testFormatLogEventStringBuilderIso8601TimezoneOffset() {
+        final LogEvent event = new MyLogEvent();
+        final String[] optionsWithTimezone = {ISO8601_OFFSET_DATE_TIME_XXX};
+        final DatePatternConverter converter = DatePatternConverter.newInstance(optionsWithTimezone);
+        final StringBuilder sb = new StringBuilder();
+        converter.format(event, sb);
+
+        final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
+        final String expected = sdf.format(new Date(event.getTimeMillis()));
+        assertEquals(expected, sb.toString());
+    }
+
+    @Test
+    public void testFormatLogEventStringBuilderIso8601TimezoneOffsetZ() {
+        final LogEvent event = new MyLogEvent();
+        final String[] optionsWithTimezone = {ISO8601_OFFSET_Z_FORMAT};
+        final DatePatternConverter converter = DatePatternConverter.newInstance(optionsWithTimezone);
+        final StringBuilder sb = new StringBuilder();
+        converter.format(event, sb);
+
+        final SimpleDateFormat sdf = new SimpleDateFormat(converter.getPattern());
+        final String expected = sdf.format(new Date(event.getTimeMillis()));
+        assertEquals(expected, sb.toString());
+    }
+
+    @Test
     public void testPredefinedFormatWithTimezone() {
         for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
             final String[] options = {format.name(), "PDT"}; // Pacific Daylight Time=UTC-8:00
@@ -151,37 +206,53 @@ public class DatePatternConverterTest {
     }
 
     private String precisePattern(final String pattern, final int precision) {
-        final String seconds = pattern.substring(0, pattern.indexOf("SSS"));
-        return seconds + "nnnnnnnnn".substring(0, precision);
+        String search = "SSS";
+        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 with all formats from one 'n' (100s of millis) to 'nnnnnnnnn' (nanosecond precision)
     @Test
     public void testPredefinedFormatWithAnyValidNanoPrecision() {
-        final StringBuilder precise = new StringBuilder();
-        final StringBuilder milli = new StringBuilder();
+        final StringBuilder preciseBuilder = new StringBuilder();
+        final StringBuilder milliBuilder = new StringBuilder();
         final LogEvent event = new MyLogEvent();
 
         for (final String timeZone : new String[]{"PDT", null}) { // Pacific Daylight Time=UTC-8:00
             for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
                 for (int i = 1; i <= 9; i++) {
-                    if (format.getPattern().endsWith("n")) {
-                        continue; // ignore patterns that already have precise time formats
+                    final String pattern = format.getPattern();
+                    if (pattern.endsWith("n") || pattern.matches(".+n+X*") || pattern.matches(".+n+Z*")
+                            || pattern.indexOf("SSS") < 0) {
+                        // ignore patterns that already have precise time formats
+                        // ignore patterns that do not use seconds.
+                        continue;
                     }
-                    precise.setLength(0);
-                    milli.setLength(0);
+                    preciseBuilder.setLength(0);
+                    milliBuilder.setLength(0);
 
-                    final String[] preciseOptions = {precisePattern(format.getPattern(), i), timeZone};
+                    final String precisePattern = precisePattern(pattern, i);
+                    final String[] preciseOptions = { precisePattern, timeZone };
                     final DatePatternConverter preciseConverter = DatePatternConverter.newInstance(preciseOptions);
-                    preciseConverter.format(event, precise);
-
-                    final String[] milliOptions = {format.getPattern(), timeZone};
-                    DatePatternConverter.newInstance(milliOptions).format(event, milli);
-                    milli.setLength(milli.length() - 3); // truncate millis
-                    final String expected = milli.append("987123456".substring(0, i)).toString();
-
-                    assertEquals(expected, precise.toString());
-                    //System.out.println(preciseOptions[0] + ": " + precise);
+                    preciseConverter.format(event, preciseBuilder);
+
+                    final String[] milliOptions = { pattern, timeZone };
+                    DatePatternConverter.newInstance(milliOptions).format(event, milliBuilder);
+                    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".substring(0, i)).append(tz).toString();
+
+                    assertEquals(
+                            "format = " + format + ", pattern = " + pattern + ", precisePattern = " + precisePattern,
+                            expected, preciseBuilder.toString());
+                    // System.out.println(preciseOptions[0] + ": " + precise);
                 }
             }
         }
@@ -189,30 +260,52 @@ public class DatePatternConverterTest {
 
     @Test
     public void testInvalidLongPatternIgnoresExcessiveDigits() {
-        final StringBuilder precise = new StringBuilder();
-        final StringBuilder milli = new StringBuilder();
+        final StringBuilder preciseBuilder = new StringBuilder();
+        final StringBuilder milliBuilder = new StringBuilder();
         final LogEvent event = new MyLogEvent();
 
-            for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
-                if (format.getPattern().endsWith("n")) {
-                    continue; // ignore patterns that already have precise time formats
-                }
-                precise.setLength(0);
-                milli.setLength(0);
-
-                final String pattern = format.getPattern().substring(0, format.getPattern().indexOf("SSS"));
-                final String[] preciseOptions = {pattern + "nnnnnnnnn" + "n"}; // too long
-                final DatePatternConverter preciseConverter = DatePatternConverter.newInstance(preciseOptions);
-                preciseConverter.format(event, precise);
-
-                final String[] milliOptions = {format.getPattern()};
-                DatePatternConverter.newInstance(milliOptions).format(event, milli);
-                milli.setLength(milli.length() - 3); // truncate millis
-                final String expected = milli.append("987123456").toString();
-
-                assertEquals(expected, precise.toString());
-                //System.out.println(preciseOptions[0] + ": " + precise);
+        for (final FixedDateFormat.FixedFormat format : FixedDateFormat.FixedFormat.values()) {
+            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 = DatePatternConverter.newInstance(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 = DatePatternConverter.newInstance(new String[] { precisePattern });
             }
+            preciseConverter.format(event, preciseBuilder);
+
+            final String[] milliOptions = { pattern };
+            DatePatternConverter.newInstance(milliOptions).format(event, milliBuilder);
+            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("format = " + format + ", pattern = " + pattern + ", precisePattern = " + precisePattern,
+                    expected, preciseBuilder.toString());
+            // System.out.println(preciseOptions[0] + ": " + precise);
+        }
     }
 
     private class MyLogEvent extends AbstractLogEvent {
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormatTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormatTest.java
index e41c4e1..b36c977 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormatTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/datetime/FixedDateFormatTest.java
@@ -17,6 +17,12 @@
 
 package org.apache.logging.log4j.core.util.datetime;
 
+import static org.apache.logging.log4j.core.util.datetime.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;
@@ -27,9 +33,6 @@ import java.util.concurrent.TimeUnit;
 import org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedFormat;
 import org.junit.Test;
 
-import static org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedFormat.*;
-import static org.junit.Assert.*;
-
 /**
  * Tests {@link FixedDateFormat}.
  */
@@ -156,15 +159,16 @@ public class FixedDateFormatTest {
         final long start = now - TimeUnit.HOURS.toMillis(25);
         final long end = now + TimeUnit.HOURS.toMillis(25);
         for (final FixedFormat format : FixedFormat.values()) {
-            if (format.getPattern().endsWith("n")) {
+            String pattern = format.getPattern();
+            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
                 continue; // cannot compile precise timestamp formats with SimpleDateFormat
             }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(format.getPattern(), Locale.getDefault());
+            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 + "(" + format.getPattern() + ")" + "/" + time, expected, actual);
+                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
             }
         }
     }
@@ -175,15 +179,16 @@ public class FixedDateFormatTest {
         final long start = now - TimeUnit.HOURS.toMillis(25);
         final long end = now + TimeUnit.HOURS.toMillis(25);
         for (final FixedFormat format : FixedFormat.values()) {
-            if (format.getPattern().endsWith("n")) {
+            String pattern = format.getPattern();
+            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
                 continue; // cannot compile precise timestamp formats with SimpleDateFormat
             }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(format.getPattern(), Locale.getDefault());
+            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 + "(" + format.getPattern() + ")" + "/" + time, expected, actual);
+                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
             }
         }
     }
@@ -195,16 +200,19 @@ public class FixedDateFormatTest {
         final long end = now + TimeUnit.HOURS.toMillis(25);
         final char[] buffer = new char[128];
         for (final FixedFormat format : FixedFormat.values()) {
-            if (format.getPattern().endsWith("n")) {
-                continue; // cannot compile precise timestamp formats with SimpleDateFormat
+            String pattern = format.getPattern();
+            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
+                // cannot compile precise timestamp formats with SimpleDateFormat
+                // This format() API not include the TZ
+                continue;
             }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(format.getPattern(), Locale.getDefault());
+            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 + "(" + format.getPattern() + ")" + "/" + time, expected, actual);
+                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
             }
         }
     }
@@ -216,16 +224,17 @@ public class FixedDateFormatTest {
         final long end = now + TimeUnit.HOURS.toMillis(25);
         final char[] buffer = new char[128];
         for (final FixedFormat format : FixedFormat.values()) {
-            if (format.getPattern().endsWith("n")) {
+            String pattern = format.getPattern();
+            if (pattern.endsWith("n") || pattern.matches(".+n+X*") || pattern.matches(".+n+Z*") || format.getTimeZoneFormat() != null) {
                 continue; // cannot compile precise timestamp formats with SimpleDateFormat
             }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(format.getPattern(), Locale.getDefault());
+            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 + "(" + format.getPattern() + ")" + "/" + time, expected, actual);
+                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
             }
         }
     }
@@ -360,6 +369,12 @@ public class FixedDateFormatTest {
             calendar.add(Calendar.HOUR_OF_DAY, 1);
         }
     }
+    
+    private boolean containsNanos(FixedFormat fixedFormat) {
+        final String pattern = fixedFormat.getPattern();
+        return pattern.endsWith("n") || pattern.matches(".+n+X*") || pattern.matches(".+n+Z*");
+    }
+    
     /**
      * This test case validates date pattern before and after DST
      * Base Date : 12 Mar 2017
@@ -374,15 +389,16 @@ public class FixedDateFormatTest {
         final long end = now + TimeUnit.HOURS.toMillis(1);
 
         for (final FixedFormat format : FixedFormat.values()) {
-            if (format.getPattern().endsWith("n")) {
+            String pattern = format.getPattern();
+            if (containsNanos(format) || format.getTimeZoneFormat() != null) {
                 continue; // cannot compile precise timestamp formats with SimpleDateFormat
             }
-            final SimpleDateFormat simpleDF = new SimpleDateFormat(format.getPattern(), Locale.getDefault());
+            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 + "(" + format.getPattern() + ")" + "/" + time, expected, actual);
+                assertEquals(format + "(" + pattern + ")" + "/" + time, expected, actual);
             }
         }
     }
diff --git a/src/site/xdoc/manual/layouts.xml.vm b/src/site/xdoc/manual/layouts.xml.vm
index fe72b0d..76de62f 100644
--- a/src/site/xdoc/manual/layouts.xml.vm
+++ b/src/site/xdoc/manual/layouts.xml.vm
@@ -747,6 +747,22 @@ WARN  [main]: Message 2</pre>
                     <td>20121102T143402,781</td>
                   </tr>
                   <tr>
+                    <td>%d{ISO8601_OFFSET_DATE_TIME_X}</td>
+                    <td>2012-11-02'T'14:34:02,781-07</td>
+                  </tr>
+                  <tr>
+                    <td>%d{ISO8601_OFFSET_DATE_TIME_XX}</td>
+                    <td>2012-11-02'T'14:34:02,781-0700</td>
+                  </tr>
+                  <tr>
+                    <td>%d{ISO8601_OFFSET_DATE_TIME_XXX}</td>
+                    <td>2012-11-02'T'14:34:02,781-07:00</td>
+                  </tr>
+                  <tr>
+                    <td>%d{ISO8601_OFFSET_DATE_TIME_Z}</td>
+                    <td>2012-11-02'T'14:34:02,781-0700</td>
+                  </tr>
+                  <tr>
                     <td>%d{ABSOLUTE}</td>
                     <td>14:34:02,781</td>
                   </tr>