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.appender.routing;
018
019import java.util.Map.Entry;
020import java.util.concurrent.ConcurrentHashMap;
021import java.util.concurrent.ConcurrentMap;
022import java.util.concurrent.ScheduledFuture;
023import java.util.concurrent.TimeUnit;
024
025import org.apache.logging.log4j.core.AbstractLifeCycle;
026import org.apache.logging.log4j.core.Core;
027import org.apache.logging.log4j.core.LogEvent;
028import org.apache.logging.log4j.core.config.Configuration;
029import org.apache.logging.log4j.core.config.ConfigurationScheduler;
030import org.apache.logging.log4j.core.config.Scheduled;
031import org.apache.logging.log4j.core.config.plugins.Plugin;
032import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
033import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
034import org.apache.logging.log4j.core.config.plugins.PluginFactory;
035
036/**
037 * Policy is purging appenders that were not in use specified time in minutes
038 */
039@Plugin(name = "IdlePurgePolicy", category = Core.CATEGORY_NAME, printObject = true)
040@Scheduled
041public class IdlePurgePolicy extends AbstractLifeCycle implements PurgePolicy, Runnable {
042
043    private final long timeToLive;
044    private final long checkInterval;
045    private final ConcurrentMap<String, Long> appendersUsage = new ConcurrentHashMap<>();
046    private RoutingAppender routingAppender;
047    private final ConfigurationScheduler scheduler;
048    private volatile ScheduledFuture<?> future;
049
050    public IdlePurgePolicy(final long timeToLive, final long checkInterval, final ConfigurationScheduler scheduler) {
051        this.timeToLive = timeToLive;
052        this.checkInterval = checkInterval;
053        this.scheduler = scheduler;
054    }
055
056    @Override
057    public void initialize(@SuppressWarnings("hiding") final RoutingAppender routingAppender) {
058        this.routingAppender = routingAppender;
059    }
060
061    @Override
062    public boolean stop(final long timeout, final TimeUnit timeUnit) {
063        setStopping();
064        final boolean stopped = stop(future);
065        setStopped();
066        return stopped;
067    }
068
069    /**
070     * Purging appenders that were not in use specified time
071     */
072    @Override
073    public void purge() {
074        final long createTime = System.currentTimeMillis() - timeToLive;
075        for (final Entry<String, Long> entry : appendersUsage.entrySet()) {
076            long entryValue = entry.getValue();
077            if (entryValue < createTime) {
078                if (appendersUsage.remove(entry.getKey(), entryValue)) {
079                    LOGGER.debug("Removing appender {}", entry.getKey());
080                    routingAppender.deleteAppender(entry.getKey());
081                }
082            }
083        }
084    }
085
086    @Override
087    public void update(final String key, final LogEvent event) {
088        final long now = System.currentTimeMillis();
089        appendersUsage.put(key, now);
090        if (future == null) {
091            synchronized (this) {
092                if (future == null) {
093                    scheduleNext();
094                }
095            }
096        }
097
098    }
099
100    @Override
101    public void run() {
102        purge();
103        scheduleNext();
104    }
105
106    private void scheduleNext() {
107        long updateTime = Long.MAX_VALUE;
108        for (final Entry<String, Long> entry : appendersUsage.entrySet()) {
109            if (entry.getValue() < updateTime) {
110                updateTime = entry.getValue();
111            }
112        }
113
114        if (updateTime < Long.MAX_VALUE) {
115            final long interval = timeToLive - (System.currentTimeMillis() - updateTime);
116            future = scheduler.schedule(this, interval, TimeUnit.MILLISECONDS);
117        } else {
118            // reset to initial state - in case of all appenders already purged
119            future = scheduler.schedule(this, checkInterval, TimeUnit.MILLISECONDS);
120        }
121    }
122
123    /**
124     * Create the PurgePolicy
125     *
126     * @param timeToLive    the number of increments of timeUnit before the Appender should be purged.
127     * @param checkInterval when all appenders purged, the number of increments of timeUnit to check if any appenders appeared
128     * @param timeUnit      the unit of time the timeToLive and the checkInterval is expressed in.
129     * @return The Routes container.
130     */
131    @PluginFactory
132    public static PurgePolicy createPurgePolicy(
133        @PluginAttribute("timeToLive") final String timeToLive,
134        @PluginAttribute("checkInterval") final String checkInterval,
135        @PluginAttribute("timeUnit") final String timeUnit,
136        @PluginConfiguration final Configuration configuration) {
137
138        if (timeToLive == null) {
139            LOGGER.error("A timeToLive value is required");
140            return null;
141        }
142        TimeUnit units;
143        if (timeUnit == null) {
144            units = TimeUnit.MINUTES;
145        } else {
146            try {
147                units = TimeUnit.valueOf(timeUnit.toUpperCase());
148            } catch (final Exception ex) {
149                LOGGER.error("Invalid timeUnit value {}. timeUnit set to MINUTES", timeUnit, ex);
150                units = TimeUnit.MINUTES;
151            }
152        }
153
154        long ttl = units.toMillis(Long.parseLong(timeToLive));
155        if (ttl < 0) {
156            LOGGER.error("timeToLive must be positive. timeToLive set to 0");
157            ttl = 0;
158        }
159
160        long ci;
161        if (checkInterval == null) {
162            ci = ttl;
163        } else {
164            ci = units.toMillis(Long.parseLong(checkInterval));
165            if (ci < 0) {
166                LOGGER.error("checkInterval must be positive. checkInterval set equal to timeToLive = {}", ttl);
167                ci = ttl;
168            }
169        }
170
171        return new IdlePurgePolicy(ttl, ci, configuration.getScheduler());
172    }
173
174    @Override
175    public String toString() {
176        return "timeToLive=" + timeToLive;
177    }
178
179}