001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.runtime.log;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.Iterator;
021import java.util.LinkedList;
022import java.util.List;
023
024import org.apache.log4j.spi.LoggingEvent;
025
026
027/**
028 * Log appender that stores a pile of logs in memory.
029 */
030public class MemoryAppender extends org.apache.log4j.AppenderSkeleton
031{
032    /** Time of expiration of a log (10 minutes) */
033    private static final long __LOG_EXPIRATION_TIME = 10 * 60 * 1000; 
034    /** Time duration between 2 cleansing of logs queue (10 seconds) */
035    private static final long __TIME_DURATION_BEFORE_NEXT_CLEAN = 10 * 1000; 
036    
037    private long _lastCleanTime;
038    
039    private LinkedList<MemoryLogRecord> _logsQueue;
040    
041    /**
042     * Default constructor for the memory appender
043     */
044    public MemoryAppender()
045    {
046        _logsQueue = new LinkedList<>();
047    }
048
049    @Override
050    protected void append(LoggingEvent event)
051    {
052        // Add the logging event information to a LogRecord
053        // Retrieve the information from the log4j LoggingEvent.
054        MemoryLogRecord record = new MemoryLogRecord(event);
055
056        // Remove logs older than 10 minutes
057        removeExpiredEvents(System.currentTimeMillis() - __LOG_EXPIRATION_TIME);
058        
059        // Add logs to the queue
060        synchronized (_logsQueue)
061        {
062            _logsQueue.add(record);
063        }
064    }
065    
066    /**
067     * Get the logs from a given timestamp
068     * @param fromTimestamp If greater than 0, get the events more recent than this timestamp parameter. If equals or less than 0, returns all events
069     * @return The log events
070     */
071    @SuppressWarnings("unchecked")
072    public List<MemoryLogRecord> getEvents(long fromTimestamp)
073    {
074        return getEvents(fromTimestamp, Collections.EMPTY_LIST);
075    }
076    
077    /**
078     * Get the logs from a given timestamp and filtered by specific categories
079     * @param fromTimestamp If greater than 0, get the events more recent than this timestamp parameter. If equals or less than 0, returns all events
080     * @param filterCategories The filter categories
081     * @return The log events
082     */
083    public List<MemoryLogRecord> getEvents(long fromTimestamp, List<String> filterCategories)
084    {
085        // New events can occur at current time, retrieve only the fixed logs list: 1ms before now.
086        long toTimestamp = System.currentTimeMillis() - 1;
087        
088        synchronized (_logsQueue)
089        {
090            if (fromTimestamp <= 0 && filterCategories.isEmpty())
091            {
092                // returns all records
093                return new ArrayList<>(_logsQueue);
094            }
095            else if (toTimestamp < fromTimestamp)
096            {
097                return new ArrayList<>();
098            }
099            else
100            {
101                List<MemoryLogRecord> recentRecords = new ArrayList<>();
102                
103                Iterator<MemoryLogRecord> it = _logsQueue.descendingIterator();
104                while (it.hasNext())
105                {
106                    MemoryLogRecord record = it.next();
107                    if ((fromTimestamp <= 0 || record.millis() >= fromTimestamp) && record.millis() <= toTimestamp)
108                    {
109                        if (_matchCategoryFilter(filterCategories, record))
110                        {
111                            recentRecords.add(record);
112                        }
113                    }
114                    else
115                    {
116                        // Stop iteration
117                        break;
118                    }
119                }
120                
121                return recentRecords;
122            }
123        }
124    }
125    
126    private boolean _matchCategoryFilter(List<String> filterCategories, MemoryLogRecord record)
127    {
128        String recordCategory = record.category();
129        if (filterCategories.isEmpty() || filterCategories.contains(recordCategory))
130        {
131            return true;
132        }
133        
134        for (String category : filterCategories)
135        {
136            if (recordCategory.startsWith(category.concat(".")))
137            {
138                return true;
139            }
140        }
141        
142        return false;
143    }
144    
145    /**
146     * Remove the expired log records every 10 seconds
147     * @param from The expiration date in milliseconds
148     */
149    public void removeExpiredEvents(long from)
150    {
151        synchronized (_logsQueue)
152        {
153            if (_lastCleanTime != 0 && _lastCleanTime + __TIME_DURATION_BEFORE_NEXT_CLEAN > System.currentTimeMillis())
154            {
155                // The time between 2 cleansing has not yet expired, do nothing
156                return;
157            }
158            
159            // Remove logs before 'from'
160            MemoryLogRecord record = _logsQueue.peek();
161            while (record != null && record.millis() < from)
162            {
163                _logsQueue.remove();
164                record = _logsQueue.peek();
165            }
166            
167            _lastCleanTime = System.currentTimeMillis();
168        }
169    }
170    
171    @Override
172    public boolean requiresLayout()
173    {
174        return false;
175    }
176
177    @Override
178    public void close() 
179    {
180        _logsQueue.clear();
181    }
182
183}