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            if (entry.getValue() < createTime) {
077                LOGGER.debug("Removing appender " + entry.getKey());
078                if (appendersUsage.remove(entry.getKey(), entry.getValue())) {
079                    routingAppender.deleteAppender(entry.getKey());
080                }
081            }
082        }
083    }
084
085    @Override
086    public void update(final String key, final LogEvent event) {
087        final long now = System.currentTimeMillis();
088        appendersUsage.put(key, now);
089        if (future == null) {
090            synchronized (this) {
091                if (future == null) {
092                    scheduleNext();
093                }
094            }
095        }
096
097    }
098
099    @Override
100    public void run() {
101        purge();
102        scheduleNext();
103    }
104
105    private void scheduleNext() {
106        long updateTime = Long.MAX_VALUE;
107        for (final Entry<String, Long> entry : appendersUsage.entrySet()) {
108            if (entry.getValue() < updateTime) {
109                updateTime = entry.getValue();
110            }
111        }
112
113        if (updateTime < Long.MAX_VALUE) {
114            final long interval = timeToLive - (System.currentTimeMillis() - updateTime);
115            future = scheduler.schedule(this, interval, TimeUnit.MILLISECONDS);
116        } else {
117            // reset to initial state - in case of all appenders already purged
118            future = scheduler.schedule(this, checkInterval, TimeUnit.MILLISECONDS);
119        }
120    }
121
122    /**
123     * Create the PurgePolicy
124     *
125     * @param timeToLive    the number of increments of timeUnit before the Appender should be purged.
126     * @param checkInterval when all appenders purged, the number of increments of timeUnit to check if any appenders appeared  
127     * @param timeUnit      the unit of time the timeToLive and the checkInterval is expressed in.
128     * @return The Routes container.
129     */
130    @PluginFactory
131    public static PurgePolicy createPurgePolicy(
132        @PluginAttribute("timeToLive") final String timeToLive,
133        @PluginAttribute("checkInterval") final String checkInterval,
134        @PluginAttribute("timeUnit") final String timeUnit,
135        @PluginConfiguration final Configuration configuration) {
136
137        if (timeToLive == null) {
138            LOGGER.error("A timeToLive value is required");
139            return null;
140        }
141        TimeUnit units;
142        if (timeUnit == null) {
143            units = TimeUnit.MINUTES;
144        } else {
145            try {
146                units = TimeUnit.valueOf(timeUnit.toUpperCase());
147            } catch (final Exception ex) {
148                LOGGER.error("Invalid timeUnit value {}. timeUnit set to MINUTES", timeUnit, ex);
149                units = TimeUnit.MINUTES;
150            }
151        }
152
153        long ttl = units.toMillis(Long.parseLong(timeToLive));
154        if (ttl < 0) {
155            LOGGER.error("timeToLive must be positive. timeToLive set to 0");
156            ttl = 0;
157        }
158        
159        long ci;
160        if (checkInterval == null) {
161            ci = ttl;
162        } else {
163            ci = units.toMillis(Long.parseLong(checkInterval));
164            if (ci < 0) {
165                LOGGER.error("checkInterval must be positive. checkInterval set equal to timeToLive = {}", ttl);
166                ci = ttl;
167            }
168        }
169
170        return new IdlePurgePolicy(ttl, ci, configuration.getScheduler());
171    }
172
173    @Override
174    public String toString() {
175        return "timeToLive=" + timeToLive;
176    }
177
178}