001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.filter;
018
019import java.time.Duration;
020import java.time.Instant;
021import java.time.LocalDate;
022import java.time.LocalTime;
023import java.time.ZoneId;
024import java.time.ZonedDateTime;
025import java.time.format.DateTimeFormatter;
026
027import org.apache.logging.log4j.Level;
028import org.apache.logging.log4j.Marker;
029import org.apache.logging.log4j.core.Filter;
030import org.apache.logging.log4j.core.LogEvent;
031import org.apache.logging.log4j.core.Logger;
032import org.apache.logging.log4j.core.config.Node;
033import org.apache.logging.log4j.core.config.plugins.Plugin;
034import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
035import org.apache.logging.log4j.core.config.plugins.PluginFactory;
036import org.apache.logging.log4j.core.util.Clock;
037import org.apache.logging.log4j.core.util.ClockFactory;
038import org.apache.logging.log4j.message.Message;
039import org.apache.logging.log4j.util.PerformanceSensitive;
040
041/**
042 * Filters events that fall within a specified time period in each day.
043 */
044@Plugin(name = "TimeFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true)
045@PerformanceSensitive("allocation")
046public final class TimeFilter extends AbstractFilter {
047    private static final Clock CLOCK = ClockFactory.getClock();
048    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
049
050    /**
051     * Length of hour in milliseconds.
052     */
053    private static final long HOUR_MS = 3600000;
054
055    private static final long DAY_MS = HOUR_MS * 24;
056
057    /**
058     * Starting offset from midnight in milliseconds.
059     */
060    private volatile long start;
061    private final LocalTime startTime;
062
063    /**
064     * Ending offset from midnight in milliseconds.
065     */
066    private volatile long end;
067    private final LocalTime endTime;
068
069    private final long duration;
070    
071    /**
072     * Timezone.
073     */
074    private final ZoneId timeZone;
075
076    /*
077     * Expose for unit testing.
078     */
079    TimeFilter(final LocalTime start, final LocalTime end, final ZoneId timeZone, final Result onMatch,
080            final Result onMismatch, LocalDate now) {
081        super(onMatch, onMismatch);
082        this.startTime = start;
083        this.endTime = end;
084        this.timeZone = timeZone;
085        this.start = ZonedDateTime.of(now, startTime, timeZone).withEarlierOffsetAtOverlap().toInstant().toEpochMilli();
086        long endMillis = ZonedDateTime.of(now, endTime, timeZone).withEarlierOffsetAtOverlap().toInstant().toEpochMilli();
087        if (end.isBefore(start)) {
088            // End time must be tomorrow.
089            endMillis += DAY_MS;
090        }
091        duration = startTime.isBefore(endTime) ? Duration.between(startTime, endTime).toMillis() :
092            Duration.between(startTime, endTime).plusHours(24).toMillis();
093        long difference = (endMillis - this.start) - duration;
094        if (difference != 0) {
095            // Handle switch from standard time to daylight time and daylight time to standard time.
096            endMillis -= difference;
097        }
098        this.end = endMillis;
099    }
100
101    private TimeFilter(final LocalTime start, final LocalTime end, final ZoneId timeZone, final Result onMatch,
102                       final Result onMismatch) {
103        this(start, end, timeZone, onMatch, onMismatch, LocalDate.now(timeZone));
104    }
105
106    private synchronized void adjustTimes(long currentTimeMillis) {
107        if (currentTimeMillis <= end) {
108            return;
109        }
110        LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(timeZone).toLocalDate();
111        this.start = ZonedDateTime.of(date, startTime, timeZone).withEarlierOffsetAtOverlap().toInstant().toEpochMilli();
112        long endMillis = ZonedDateTime.of(date, endTime, timeZone).withEarlierOffsetAtOverlap().toInstant().toEpochMilli();
113        if (endTime.isBefore(startTime)) {
114            // End time must be tomorrow.
115            endMillis += DAY_MS;
116        }
117        long difference = (endMillis - this.start) - duration;
118        if (difference != 0) {
119            // Handle switch from standard time to daylight time and daylight time to standard time.
120            endMillis -= difference;
121        }
122        this.end = endMillis;
123    }
124
125    /**
126     * Package-protected for tests.
127     *
128     * @param currentTimeMillis the time to compare with the boundaries. May re-initialize the cached midnight
129     *          boundary values.
130     * @return the action to perform
131     */
132    Result filter(final long currentTimeMillis) {
133        if (currentTimeMillis > end) {
134            adjustTimes(currentTimeMillis);
135        }
136        return currentTimeMillis >= start && currentTimeMillis <= end ? onMatch : onMismatch;
137    }
138
139    @Override
140    public Result filter(final LogEvent event) {
141        return filter(event.getTimeMillis());
142    }
143
144    private Result filter() {
145        return filter(CLOCK.currentTimeMillis());
146    }
147
148    @Override
149    public Result filter(final Logger logger, final Level level, final Marker marker, final Message msg,
150            final Throwable t) {
151        return filter();
152    }
153
154    @Override
155    public Result filter(final Logger logger, final Level level, final Marker marker, final Object msg,
156            final Throwable t) {
157        return filter();
158    }
159
160    @Override
161    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
162            final Object... params) {
163        return filter();
164    }
165
166    @Override
167    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
168            final Object p0) {
169        return filter();
170    }
171
172    @Override
173    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
174            final Object p0, final Object p1) {
175        return filter();
176    }
177
178    @Override
179    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
180            final Object p0, final Object p1, final Object p2) {
181        return filter();
182    }
183
184    @Override
185    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
186            final Object p0, final Object p1, final Object p2, final Object p3) {
187        return filter();
188    }
189
190    @Override
191    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
192            final Object p0, final Object p1, final Object p2, final Object p3, final Object p4) {
193        return filter();
194    }
195
196    @Override
197    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
198            final Object p0, final Object p1, final Object p2, final Object p3, final Object p4, final Object p5) {
199        return filter();
200    }
201
202    @Override
203    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
204            final Object p0, final Object p1, final Object p2, final Object p3, final Object p4, final Object p5,
205            final Object p6) {
206        return filter();
207    }
208
209    @Override
210    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
211            final Object p0, final Object p1, final Object p2, final Object p3, final Object p4, final Object p5,
212            final Object p6, final Object p7) {
213        return filter();
214    }
215
216    @Override
217    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
218            final Object p0, final Object p1, final Object p2, final Object p3, final Object p4, final Object p5,
219            final Object p6, final Object p7, final Object p8) {
220        return filter();
221    }
222
223    @Override
224    public Result filter(final Logger logger, final Level level, final Marker marker, final String msg,
225            final Object p0, final Object p1, final Object p2, final Object p3, final Object p4, final Object p5,
226            final Object p6, final Object p7, final Object p8, final Object p9) {
227        return filter();
228    }
229
230    @Override
231    public String toString() {
232        final StringBuilder sb = new StringBuilder();
233        sb.append("start=").append(start);
234        sb.append(", end=").append(end);
235        sb.append(", timezone=").append(timeZone.toString());
236        return sb.toString();
237    }
238
239    /**
240     * Creates a TimeFilter.
241     * @param start The start time.
242     * @param end The end time.
243     * @param tz timezone.
244     * @param match Action to perform if the time matches.
245     * @param mismatch Action to perform if the action does not match.
246     * @return A TimeFilter.
247     */
248    // TODO Consider refactoring to use AbstractFilter.AbstractFilterBuilder
249    @PluginFactory
250    public static TimeFilter createFilter(
251            @PluginAttribute("start") final String start,
252            @PluginAttribute("end") final String end,
253            @PluginAttribute("timezone") final String tz,
254            @PluginAttribute("onMatch") final Result match,
255            @PluginAttribute("onMismatch") final Result mismatch) {
256        final LocalTime startTime = parseTimestamp(start, LocalTime.MIN);
257        final LocalTime endTime = parseTimestamp(end, LocalTime.MAX);
258        final ZoneId timeZone = tz == null ? ZoneId.systemDefault() : ZoneId.of(tz);
259        final Result onMatch = match == null ? Result.NEUTRAL : match;
260        final Result onMismatch = mismatch == null ? Result.DENY : mismatch;
261        return new TimeFilter(startTime, endTime, timeZone, onMatch, onMismatch);
262    }
263
264    private static LocalTime parseTimestamp(final String timestamp, final LocalTime defaultValue) {
265        if (timestamp == null) {
266            return defaultValue;
267        }
268
269        try {
270            return LocalTime.parse(timestamp, FORMATTER);
271        } catch (final Exception e) {
272            LOGGER.warn("Error parsing TimeFilter timestamp value {}", timestamp, e);
273            return defaultValue;
274        }
275    }
276
277}