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 01:56:19 UTC
[logging-log4j2] branch master 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 master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/master by this push:
new cb2bd5c9 [LOG4J2-1246] PatternLayout %date conversion pattern should render time zone designator for ISO-ISO8601.
cb2bd5c9 is described below
commit cb2bd5c951e4c007252d21e98b4606590a648eeb
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Tue Dec 4 18:56:04 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 +-
.../core/time/internal/format/FixedDateFormat.java | 242 +++++++++++++++++----
.../core/pattern/DatePatternConverterTest.java | 185 ++++++++++++----
.../time/internal/format/FixedDateFormatTest.java | 50 +++--
src/changes/changes.xml | 6 +
src/site/asciidoc/manual/layouts.adoc | 12 +
6 files changed, 388 insertions(+), 109 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 2c9652c..c1799d9 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 < 70
private int length = 0;
FixedFormatter(final FixedDateFormat fixedDateFormat) {
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
index a640aea..4d35694 100644
--- 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
@@ -17,20 +17,21 @@
package org.apache.logging.log4j.core.time.internal.format;
-import org.apache.logging.log4j.core.time.Instant;
-
import java.util.Arrays;
import java.util.Calendar;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
+import org.apache.logging.log4j.core.time.Instant;
+
/**
* 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/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,110 @@ 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 +156,11 @@ public class FixedDateFormat {
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 int secondFractionDigits, final FixedTimeZoneFormat fixedTimeZoneFormat) {
this.timeSeparatorChar = timeSeparator;
this.timeSeparatorLength = timeSepLength;
this.millisSeparatorChar = millisSeparator;
@@ -135,6 +169,7 @@ public class FixedDateFormat {
this.datePattern = datePattern; // may be null
this.escapeCount = escapeCount;
this.secondFractionDigits = secondFractionDigits;
+ this.fixedTimeZoneFormat = fixedTimeZoneFormat;
}
/**
@@ -171,9 +206,12 @@ public class FixedDateFormat {
}
static FixedFormat lookupIgnoringNanos(final String pattern) {
- final int nanoStart = nanoStart(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;
+ 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 +221,33 @@ 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++) {
- if (pattern.charAt(i) != SECOND_FRACTION_PATTERN) {
- return -1;
- }
- }
- }
- return index;
+ 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 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;
}
/**
@@ -243,6 +298,92 @@ public class FixedDateFormat {
}
}
+ 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(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, 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;
private final TimeZone timeZone;
private final int length;
@@ -252,6 +393,8 @@ public class FixedDateFormat {
private final char millisSeparatorChar;
private final int timeSeparatorLength;
private final int millisSeparatorLength;
+ private final FixedTimeZoneFormat fixedTimeZoneFormat;
+
private volatile long midnightToday = 0;
private volatile long midnightTomorrow = 0;
@@ -270,6 +413,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
@@ -282,6 +426,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 +439,7 @@ public class FixedDateFormat {
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);
@@ -318,12 +464,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);
}
@@ -440,20 +590,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();
+ 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);
}
- 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) {
@@ -472,7 +619,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;
+ final int pos = writeTime(ms, buffer, startPos + dateLength);
+ return pos - startPos;
}
// Profiling showed this method is important to log4j performance. Modify with care!
@@ -534,6 +682,13 @@ public class FixedDateFormat {
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 int[] TABLE = {
100000, // 0
10000, // 1
@@ -543,15 +698,16 @@ public class FixedDateFormat {
1, // 5
};
- private void formatNanoOfMillisecond(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++) {
- int divisor = TABLE[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) {
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 ae53cc2..2b815c1 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
@@ -16,6 +16,9 @@
*/
package org.apache.logging.log4j.core.pattern;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
@@ -27,16 +30,16 @@ import java.util.TimeZone;
import org.apache.logging.log4j.core.AbstractLogEvent;
import org.apache.logging.log4j.core.LogEvent;
-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.time.internal.format.FixedDateFormat;
+import org.apache.logging.log4j.core.time.internal.format.FixedDateFormat.FixedTimeZoneFormat;
+import org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.util.Strings;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
-import static org.junit.Assert.*;
-
@RunWith(Parameterized.class)
public class DatePatternConverterTest {
@@ -50,9 +53,20 @@ 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}});
}
@@ -151,37 +165,53 @@ public class DatePatternConverterTest {
}
private String precisePattern(final String pattern, int precision) {
- 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 (String timeZone : new String[]{"PDT", null}) { // Pacific Daylight Time=UTC-8:00
+ 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
- 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,32 +219,54 @@ 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
- 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 {
private static final long serialVersionUID = 0;
@@ -362,4 +414,47 @@ public class DatePatternConverterTest {
assertNull(DatePatternConverter.newInstance(options).getPattern());
}
+ @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());
+ }
+
}
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java
index 575ab7c..3fe125d 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormatTest.java
@@ -36,6 +36,11 @@ import static org.junit.Assert.*;
*/
public class FixedDateFormatTest {
+ private boolean containsNanos(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());
@@ -157,15 +162,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")) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
+ 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);
}
}
}
@@ -176,15 +182,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")) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
+ 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);
}
}
}
@@ -196,16 +203,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")) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
+ 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 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);
}
}
}
@@ -217,16 +225,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")) {
- continue; // cannot compile precise timestamp formats with SimpleDateFormat
+ 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 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);
}
}
}
@@ -375,15 +384,16 @@ public class FixedDateFormatTest {
final long end = now + TimeUnit.HOURS.toMillis(1);
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) {
+ 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/changes/changes.xml b/src/changes/changes.xml
index 9ee2845..47c3a9b 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -335,6 +335,9 @@
<action issue="LOG4J2-2509" dev="ggregory" type="add">
Allow a JDBC Appender to truncate strings to match a table's metadata column length limit.
</action>
+ <action issue="LOG4J2-1246" dev="ggregory" type="add">
+ PatternLayout %date conversion pattern should render time zone designator for ISO-ISO8601.
+ </action>
</release>
<release version="2.11.2" date="2018-MM-DD" description="GA Release 2.11.2">
<action issue="LOG4J2-1906" dev="rgoers" type="fix">
@@ -457,6 +460,9 @@
<action issue="LOG4J2-2509" dev="ggregory" type="add">
Allow a JDBC Appender to truncate strings to match a table's metadata column length limit.
</action>
+ <action issue="LOG4J2-1246" dev="ggregory" type="add">
+ PatternLayout %date conversion pattern should render time zone designator for ISO-ISO8601.
+ </action>
</release>
<release version="2.11.1" date="2018-07-22" description="GA Release 2.11.1">
<action issue="LOG4J2-2389" dev="rgoers" type="fix" due-to="Liu Wen">
diff --git a/src/site/asciidoc/manual/layouts.adoc b/src/site/asciidoc/manual/layouts.adoc
index 43e594c..c5d3fb7 100644
--- a/src/site/asciidoc/manual/layouts.adoc
+++ b/src/site/asciidoc/manual/layouts.adoc
@@ -695,6 +695,18 @@ The predefined _named_ formats are:
!%d{ISO8601_BASIC}
!20121102T143402,781
+!%d{ISO8601_OFFSET_DATE_TIME_X}
+!2012-11-02'T'14:34:02,781-07
+
+!%d{ISO8601_OFFSET_DATE_TIME_XX}
+!2012-11-02'T'14:34:02,781-0700
+
+!%d{ISO8601_OFFSET_DATE_TIME_XXX}
+!2012-11-02'T'14:34:02,781-07:00
+
+!%d{ISO8601_OFFSET_DATE_TIME_Z}
+!2012-11-02'T'14:34:02,781-0700
+
!%d{ABSOLUTE}
!14:34:02,781