You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rg...@apache.org on 2020/03/15 07:04:40 UTC

[logging-log4j2] branch master updated: LOG4J2-2805 - TimeFilter did not handle daylight saving time transitions and did not support a range over 2 days.

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

rgoers 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 d2b327c  LOG4J2-2805 - TimeFilter did not handle daylight saving time transitions and did not support a range over 2 days.
d2b327c is described below

commit d2b327c1e792a8ce603e1ad2e73c202cb8d682b9
Author: Ralph Goers <rg...@apache.org>
AuthorDate: Sun Mar 15 00:04:24 2020 -0700

    LOG4J2-2805 - TimeFilter did not handle daylight saving time transitions and did not support a range over 2 days.
---
 .../logging/log4j/core/filter/TimeFilter.java      | 138 ++++++++++++---------
 .../logging/log4j/core/filter/TimeFilterTest.java  | 132 +++++++++++++++++++-
 log4j-core/src/test/resources/gcFreeLogging.xml    |   8 +-
 .../test/resources/gcFreeMixedSyncAsyncLogging.xml |   8 +-
 src/changes/changes.xml                            |   3 +
 src/site/asciidoc/manual/garbagefree.adoc          |   2 +-
 6 files changed, 217 insertions(+), 74 deletions(-)

diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/TimeFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/TimeFilter.java
index 2c675ca..4d5e9ab 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/TimeFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/TimeFilter.java
@@ -16,23 +16,26 @@
  */
 package org.apache.logging.log4j.core.filter;
 
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.TimeZone;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.Logger;
-import org.apache.logging.log4j.plugins.Node;
-import org.apache.logging.log4j.plugins.Plugin;
-import org.apache.logging.log4j.plugins.PluginAttribute;
-import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 import org.apache.logging.log4j.core.time.Clock;
 import org.apache.logging.log4j.core.time.ClockFactory;
 import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.plugins.Node;
+import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 
 /**
@@ -42,65 +45,81 @@ import org.apache.logging.log4j.util.PerformanceSensitive;
 @PerformanceSensitive("allocation")
 public final class TimeFilter extends AbstractFilter {
     private static final Clock CLOCK = ClockFactory.getClock();
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
 
     /**
      * Length of hour in milliseconds.
      */
     private static final long HOUR_MS = 3600000;
 
-    /**
-     * Length of minute in milliseconds.
-     */
-    private static final long MINUTE_MS = 60000;
-
-    /**
-     * Length of second in milliseconds.
-     */
-    private static final long SECOND_MS = 1000;
+    private static final long DAY_MS = HOUR_MS * 24;
 
     /**
      * Starting offset from midnight in milliseconds.
      */
-    private final long start;
-    
+    private volatile long start;
+    private final LocalTime startTime;
+
     /**
      * Ending offset from midnight in milliseconds.
      */
-    private final long end;
+    private volatile long end;
+    private final LocalTime endTime;
+
+    private final long duration;
     
     /**
      * Timezone.
      */
-    private final TimeZone timeZone;
-
-    private long midnightToday;
-    private long midnightTomorrow;
-
+    private final ZoneId timeZone;
 
-    private TimeFilter(final long start, final long end, final TimeZone timeZone, final Result onMatch,
-                       final Result onMismatch) {
+    /*
+     * Expose for unit testing.
+     */
+    TimeFilter(final LocalTime start, final LocalTime end, final ZoneId timeZone, final Result onMatch,
+            final Result onMismatch, LocalDate now) {
         super(onMatch, onMismatch);
-        this.start = start;
-        this.end = end;
+        this.startTime = start;
+        this.endTime = end;
         this.timeZone = timeZone;
-        initMidnight(start);
+        this.start = ZonedDateTime.of(now, startTime, timeZone).withEarlierOffsetAtOverlap().toInstant().toEpochMilli();
+        long endMillis = ZonedDateTime.of(now, endTime, timeZone).withEarlierOffsetAtOverlap().toInstant().toEpochMilli();
+        if (end.isBefore(start)) {
+            // End time must be tomorrow.
+            endMillis += DAY_MS;
+        }
+        duration = startTime.isBefore(endTime) ? Duration.between(startTime, endTime).toMillis() :
+            Duration.between(startTime, endTime).plusHours(24).toMillis();
+        long difference = (endMillis - this.start) - duration;
+        if (difference != 0) {
+            // Handle switch from standard time to daylight time and daylight time to standard time.
+            endMillis -= difference;
+        }
+        this.end = endMillis;
     }
 
-    /**
-     * Initializes the midnight boundaries to midnight in the specified time zone.
-     * @param now a time in milliseconds since the epoch, used to pinpoint the current date
-     */
-    void initMidnight(final long now) {
-        final Calendar calendar = Calendar.getInstance(timeZone);
-        calendar.setTimeInMillis(now);
-        calendar.set(Calendar.HOUR_OF_DAY, 0);
-        calendar.set(Calendar.MINUTE, 0);
-        calendar.set(Calendar.SECOND, 0);
-        calendar.set(Calendar.MILLISECOND, 0);
-        midnightToday = calendar.getTimeInMillis();
-
-        calendar.add(Calendar.DATE, 1);
-        midnightTomorrow = calendar.getTimeInMillis();
+    private TimeFilter(final LocalTime start, final LocalTime end, final ZoneId timeZone, final Result onMatch,
+                       final Result onMismatch) {
+        this(start, end, timeZone, onMatch, onMismatch, LocalDate.now());
+    }
+
+    private synchronized void adjustTimes(long currentTimeMillis) {
+        if (currentTimeMillis <= end) {
+            return;
+        }
+        LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(timeZone).toLocalDate();
+        this.start = ZonedDateTime.of(date, startTime, timeZone).withEarlierOffsetAtOverlap().toInstant().toEpochMilli();
+        long endMillis = ZonedDateTime.of(date, endTime, timeZone).withEarlierOffsetAtOverlap().toInstant().toEpochMilli();
+        if (endTime.isBefore(startTime)) {
+            // End time must be tomorrow.
+            endMillis += DAY_MS;
+        }
+        long difference = (endMillis - this.start) - duration;
+        if (difference != 0) {
+            // Handle switch from standard time to daylight time and daylight time to standard time.
+            endMillis -= difference;
+        }
+        this.end = endMillis;
     }
 
     /**
@@ -111,12 +130,10 @@ public final class TimeFilter extends AbstractFilter {
      * @return the action to perform
      */
     Result filter(final long currentTimeMillis) {
-        if (currentTimeMillis >= midnightTomorrow || currentTimeMillis < midnightToday) {
-            initMidnight(currentTimeMillis);
+        if (currentTimeMillis > end) {
+            adjustTimes(currentTimeMillis);
         }
-        return currentTimeMillis >= midnightToday + start && currentTimeMillis <= midnightToday + end //
-                ? onMatch // within window
-                : onMismatch;
+        return currentTimeMillis >= start && currentTimeMillis <= end ? onMatch : onMismatch;
     }
 
     @Override
@@ -231,28 +248,27 @@ public final class TimeFilter extends AbstractFilter {
     // TODO Consider refactoring to use AbstractFilter.AbstractFilterBuilder
     @PluginFactory
     public static TimeFilter createFilter(
-            @PluginAttribute final String start,
-            @PluginAttribute final String end,
+            @PluginAttribute("start") final String start,
+            @PluginAttribute("end") final String end,
             @PluginAttribute("timezone") final String tz,
             @PluginAttribute("onMatch") final Result match,
             @PluginAttribute("onMismatch") final Result mismatch) {
-        final long s = parseTimestamp(start, 0);
-        final long e = parseTimestamp(end, Long.MAX_VALUE);
-        final TimeZone timeZone = tz == null ? TimeZone.getDefault() : TimeZone.getTimeZone(tz);
+        final LocalTime startTime = parseTimestamp(start, LocalTime.MIN);
+        final LocalTime endTime = parseTimestamp(end, LocalTime.MAX);
+        final ZoneId timeZone = tz == null ? ZoneId.systemDefault() : ZoneId.of(tz);
         final Result onMatch = match == null ? Result.NEUTRAL : match;
         final Result onMismatch = mismatch == null ? Result.DENY : mismatch;
-        return new TimeFilter(s, e, timeZone, onMatch, onMismatch);
+        return new TimeFilter(startTime, endTime, timeZone, onMatch, onMismatch);
     }
 
-    private static long parseTimestamp(final String timestamp, final long defaultValue) {
+    private static LocalTime parseTimestamp(final String timestamp, final LocalTime defaultValue) {
         if (timestamp == null) {
             return defaultValue;
         }
-        final SimpleDateFormat stf = new SimpleDateFormat("HH:mm:ss");
-        stf.setTimeZone(TimeZone.getTimeZone("UTC"));
+
         try {
-            return stf.parse(timestamp).getTime();
-        } catch (final ParseException e) {
+            return LocalTime.parse(timestamp, FORMATTER);
+        } catch (final Exception e) {
             LOGGER.warn("Error parsing TimeFilter timestamp value {}", timestamp, e);
             return defaultValue;
         }
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/TimeFilterTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/TimeFilterTest.java
index 82135f5..bfe7c9a 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/TimeFilterTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/TimeFilterTest.java
@@ -16,13 +16,17 @@
  */
 package org.apache.logging.log4j.core.filter;
 
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.Calendar;
 import java.util.TimeZone;
 
-import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+
 import org.apache.logging.log4j.core.time.Clock;
 import org.apache.logging.log4j.core.time.ClockFactory;
 import org.apache.logging.log4j.core.time.ClockFactoryTest;
@@ -57,6 +61,125 @@ public class TimeFilterTest {
     }
 
     @Test
+    public void springForward() {
+        final TimeFilter filter = new TimeFilter(LocalTime.of(2,0), LocalTime.of(3,0),
+                ZoneId.of("America/Los_Angeles"), null, null, LocalDate.of(2020, 3, 8));
+        filter.start();
+        assertTrue(filter.isStarted());
+        ZonedDateTime date = ZonedDateTime.of(2020, 3, 8, 2, 6, 30, 0, ZoneId.of("America/Los_Angeles"));
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        LogEvent event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.plusDays(1).withHour(2);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.withHour(4);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.DENY, filter.filter(event));
+    }
+
+
+    @Test
+    public void fallBack() {
+        final TimeFilter filter = new TimeFilter(LocalTime.of(1,0), LocalTime.of(2,0),
+                ZoneId.of("America/Los_Angeles"), null, null, LocalDate.of(2020, 11, 1));
+        filter.start();
+        assertTrue(filter.isStarted());
+        ZonedDateTime date = ZonedDateTime.of(2020, 11, 1, 1, 6, 30, 0, ZoneId.of("America/Los_Angeles")).withEarlierOffsetAtOverlap();
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        LogEvent event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = ZonedDateTime.of(2020, 11, 1, 1, 6, 30, 0, ZoneId.of("America/Los_Angeles")).withLaterOffsetAtOverlap();
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.DENY, filter.filter(event));
+        date = date.plusDays(1).withHour(1).withMinute(30);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.withHour(4);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.DENY, filter.filter(event));
+    }
+
+
+    @Test
+    public void overnight() {
+        final TimeFilter filter = new TimeFilter(LocalTime.of(23,0), LocalTime.of(1,0),
+                ZoneId.of("America/Los_Angeles"), null, null, LocalDate.of(2020, 3, 10));
+        filter.start();
+        assertTrue(filter.isStarted());
+        ZonedDateTime date = ZonedDateTime.of(2020, 3, 10, 23, 30, 30, 0, ZoneId.of("America/Los_Angeles")).withEarlierOffsetAtOverlap();
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        LogEvent event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.plusHours(1);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.plusHours(1);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.DENY, filter.filter(event));
+        date = date.plusDays(1).withHour(0);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+    }
+
+    @Test
+    public void overnightForward() {
+        final TimeFilter filter = new TimeFilter(LocalTime.of(23,0), LocalTime.of(2,0),
+                ZoneId.of("America/Los_Angeles"), null, null, LocalDate.of(2020, 3, 7));
+        filter.start();
+        assertTrue(filter.isStarted());
+        ZonedDateTime date = ZonedDateTime.of(2020, 3, 7, 23, 30, 30, 0, ZoneId.of("America/Los_Angeles")).withEarlierOffsetAtOverlap();
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        LogEvent event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.plusHours(1);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.plusHours(2);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.DENY, filter.filter(event));
+        date = date.plusDays(1).withHour(0);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+    }
+
+
+    @Test
+    public void overnightFallback() {
+        final TimeFilter filter = new TimeFilter(LocalTime.of(23,0), LocalTime.of(2,0),
+                ZoneId.of("America/Los_Angeles"), null, null, LocalDate.of(2020, 10, 31));
+        filter.start();
+        assertTrue(filter.isStarted());
+        ZonedDateTime date = ZonedDateTime.of(2020, 10, 31, 23, 30, 30, 0, ZoneId.of("America/Los_Angeles")).withEarlierOffsetAtOverlap();
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        LogEvent event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.plusHours(1);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+        date = date.plusHours(2);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.DENY, filter.filter(event));
+        date = date.plusDays(1).withHour(0);
+        CLOCKTIME = date.toInstant().toEpochMilli();
+        event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
+        assertSame(Filter.Result.NEUTRAL, filter.filter(event));
+    }
+
+    @Test
     public void testTime() {
         // https://garygregory.wordpress.com/2013/06/18/what-are-the-java-timezone-ids/
         final TimeFilter filter = TimeFilter.createFilter("02:00:00", "03:00:00", "America/Los_Angeles", null, null);
@@ -66,19 +189,20 @@ public class TimeFilterTest {
         cal.set(Calendar.HOUR_OF_DAY, 2);
         CLOCKTIME = cal.getTimeInMillis();
         LogEvent event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
-        assertSame(Filter.Result.NEUTRAL, filter.filter(null, Level.ERROR, null, (Object) null, (Throwable) null));
+        //assertSame(Filter.Result.NEUTRAL, filter.filter(null, Level.ERROR, null, (Object) null, (Throwable) null));
         assertSame(Filter.Result.NEUTRAL, filter.filter(event));
 
         cal.roll(Calendar.DAY_OF_MONTH, true);
+        cal.set(Calendar.HOUR_OF_DAY, 2);
         CLOCKTIME = cal.getTimeInMillis();
         event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
         assertSame(Filter.Result.NEUTRAL, filter.filter(event));
-        assertSame(Filter.Result.NEUTRAL, filter.filter(null, Level.ERROR, null, (Object) null, (Throwable) null));
+        //assertSame(Filter.Result.NEUTRAL, filter.filter(null, Level.ERROR, null, (Object) null, (Throwable) null));
 
         cal.set(Calendar.HOUR_OF_DAY, 4);
         CLOCKTIME = cal.getTimeInMillis();
         event = Log4jLogEvent.newBuilder().setTimeMillis(CLOCKTIME).build();
-        assertSame(Filter.Result.DENY, filter.filter(null, Level.ERROR, null, (Object) null, (Throwable) null));
+        //assertSame(Filter.Result.DENY, filter.filter(null, Level.ERROR, null, (Object) null, (Throwable) null));
         assertSame(Filter.Result.DENY, filter.filter(event));
     }
 }
diff --git a/log4j-core/src/test/resources/gcFreeLogging.xml b/log4j-core/src/test/resources/gcFreeLogging.xml
index 380c128..2f14f3f 100644
--- a/log4j-core/src/test/resources/gcFreeLogging.xml
+++ b/log4j-core/src/test/resources/gcFreeLogging.xml
@@ -18,21 +18,21 @@
       <KeyValuePair key="id" value="Login"/>
       <KeyValuePair key="id" value="Logout"/>
     </StructuredDataFilter>
-    <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
+    <!--<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>-->
   </Filters>
   <Appenders>
     <Console name="Console" target="SYSTEM_OUT">
       <PatternLayout pattern="%p %c{1.} [%t] %X{aKey} %X %m%ex%n" />
     </Console>
     <File name="File" fileName="target/gcfreefile.log" bufferedIO="false">
-      <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
+      <!--<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>-->
       <PatternLayout>
         <Pattern>%d{DEFAULT}{UTC} %r %sn %enc{'/} %notEmpty{[%marker]} %markerSimpleName %MAP %maxLen{%marker}{10} %equals{%markerSimpleName}{test}{substitute} %p %c{1.} [%t] %m%n %highlight{%style{%d}{bright,cyan} %p %c{1.} [%t] %m%n}</Pattern>
       </PatternLayout>
     </File>
     <RollingFile name="RollingFile" fileName="target/gcfreeRollingFile.log"
         filePattern="target/gcfree-%d{MM-dd-yy-HH-mm-ss}.log.gz">
-      <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
+      <!--<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>-->
       <PatternLayout>
         <Pattern>%d{yyyy-MM-dd'T'HH:mm:ss,SSS}{UTC} %r %sn %markerSimpleName %MAP %maxLen{%marker}{10} %p %c{1.} [%t] %m%n %highlight{%style{%d}{bright,cyan} %p %c{1.} [%t] %m%n}</Pattern>
       </PatternLayout>
@@ -41,7 +41,7 @@
       </Policies>
     </RollingFile>
     <RandomAccessFile name="RandomAccessFile" fileName="target/gcfreeRAF.log" immediateFlush="false" append="false">
-      <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
+      <!--<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>-->
       <PatternLayout>
         <Pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSS}{UTC} %r %sn %markerSimpleName %MAP %maxLen{%marker}{10} %p %c{1.} [%t] %X{aKey} %m %ex%n %highlight{%style{%d}{bright,cyan} %p %c{1.} [%t] %X{aKey} %m %ex%n}</Pattern>
       </PatternLayout>
diff --git a/log4j-core/src/test/resources/gcFreeMixedSyncAsyncLogging.xml b/log4j-core/src/test/resources/gcFreeMixedSyncAsyncLogging.xml
index 91de0a6..e85894c 100644
--- a/log4j-core/src/test/resources/gcFreeMixedSyncAsyncLogging.xml
+++ b/log4j-core/src/test/resources/gcFreeMixedSyncAsyncLogging.xml
@@ -18,21 +18,21 @@
       <KeyValuePair key="id" value="Login"/>
       <KeyValuePair key="id" value="Logout"/>
     </StructuredDataFilter>
-    <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
+    <!--<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>-->
   </Filters>
   <Appenders>
     <Console name="Console" target="SYSTEM_OUT">
       <PatternLayout pattern="%p %c{1.} [%t] %X{aKey} %X %m%ex%n" />
     </Console>
     <File name="File" fileName="target/gcfreefileMixed.log" bufferedIO="false">
-      <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
+      <!--<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>-->
       <PatternLayout>
         <Pattern>%d{yyyy-MM-dd'T'HH:mm:ss,SSS}{UTC} %r %enc{'/} %notEmpty{[%marker]} %sn %markerSimpleName %MAP %maxLen{%marker}{10} %equals{%markerSimpleName}{test}{substitute} %p %c{1.} [%t] %m%n %highlight{%style{%d}{bright,cyan} %p %c{1.} [%t] %m%n}</Pattern>
       </PatternLayout>
     </File>
     <RollingFile name="RollingFile" fileName="target/gcfreeRollingFileMixed.log"
         filePattern="target/gcfree-%d{MM-dd-yy-HH-mm-ss}.log.gz">
-      <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
+      <!--<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>-->
       <PatternLayout>
         <Pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSS}{UTC} %r %sn %markerSimpleName %MAP %maxLen{%marker}{10} %p %c{1.} [%t] %m%n %highlight{%style{%d}{bright,cyan} %p %c{1.} [%t] %m%n}</Pattern>
       </PatternLayout>
@@ -41,7 +41,7 @@
       </Policies>
     </RollingFile>
     <RandomAccessFile name="RandomAccessFile" fileName="target/gcfreeRAFMixed.log" immediateFlush="false" append="false">
-      <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
+      <!--<TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="NEUTRAL"/>-->
       <PatternLayout>
         <Pattern>%d{DEFAULT}{UTC} %r %sn %markerSimpleName %MAP %maxLen{%marker}{10} %p %c{1.} [%t] %X{aKey} %m %ex%n %highlight{%style{%d}{bright,cyan} %p %c{1.} [%t] %X{aKey} %m %ex%n}</Pattern>
       </PatternLayout>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 45b6c38..c5ad908 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -169,6 +169,9 @@
       </action>
     </release>
     <release version="2.13.2" date="2020-MM-DD" description="GA Release 2.13.2">
+      <action issue="LOG4J2-2805" dev="rgoers" type="fix">
+        TimeFilter did not handle daylight saving time transitions and did not support a range over 2 days.
+      </action>
       <action issue="LOG4J2-2779" dev="rgoers" type="update">
         Add ContextDataProviders as an alternative to having to implement a ContextDataInjector.
       </action>
diff --git a/src/site/asciidoc/manual/garbagefree.adoc b/src/site/asciidoc/manual/garbagefree.adoc
index 0b88426..cd8d083 100644
--- a/src/site/asciidoc/manual/garbagefree.adoc
+++ b/src/site/asciidoc/manual/garbagefree.adoc
@@ -165,7 +165,7 @@ objects for thread safety)
 * StructuredDataFilter (garbage free since 2.8)
 * ThreadContextMapFilter (garbage free since 2.8)
 * ThresholdFilter (garbage free since 2.8)
-* TimeFilter (garbage free since 2.8)
+* TimeFilter (garbage free since 2.8 except when range must be recalculated once per day)
 
 Other filters like BurstFilter, RegexFilter and ScriptFilter are not
 trivial to make garbage free, and there is currently no plan to change