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
026import org.ametys.core.trace.ForensicLogger;
027
028
029/**
030 * Log appender that stores a pile of logs in memory.
031 */
032public class MemoryAppender extends org.apache.log4j.AppenderSkeleton
033{
034    /** Time of expiration of a log (10 minutes) */
035    private static final long __LOG_EXPIRATION_TIME = 10 * 60 * 1000;
036    /** Time duration between 2 cleansing of logs queue (10 seconds) */
037    private static final long __TIME_DURATION_BEFORE_NEXT_CLEAN = 10 * 1000;
038    
039    private long _lastCleanTime;
040    
041    private LinkedList<MemoryLogRecord> _logsQueue;
042    
043    /**
044     * Default constructor for the memory appender
045     */
046    public MemoryAppender()
047    {
048        _logsQueue = new LinkedList<>();
049    }
050
051    @Override
052    protected void append(LoggingEvent event)
053    {
054        if (event.getLoggerName().startsWith(ForensicLogger.class.getName() + "."))
055        {
056            return; // Do not log forensic events in the memory appender... it floods the UI. Especially in production when logs are in error when those are info.
057        }
058        
059        // Add the logging event information to a LogRecord
060        // Retrieve the information from the log4j LoggingEvent.
061        MemoryLogRecord record = new MemoryLogRecord(event);
062
063        // Remove logs older than 10 minutes
064        removeExpiredEvents(System.currentTimeMillis() - __LOG_EXPIRATION_TIME);
065        
066        // Add logs to the queue
067        synchronized (_logsQueue)
068        {
069            _logsQueue.add(record);
070        }
071    }
072    
073    /**
074     * Get the logs from a given timestamp
075     * @param fromTimestamp If greater than 0, get the events more recent than this timestamp parameter. If equals or less than 0, returns all events
076     * @return The log events
077     */
078    @SuppressWarnings("unchecked")
079    public List<MemoryLogRecord> getEvents(long fromTimestamp)
080    {
081        return getEvents(fromTimestamp, Collections.EMPTY_LIST);
082    }
083    
084    /**
085     * Get the logs from a given timestamp and filtered by specific categories
086     * @param fromTimestamp If greater than 0, get the events more recent than this timestamp parameter. If equals or less than 0, returns all events
087     * @param filterCategories The filter categories
088     * @return The log events
089     */
090    public List<MemoryLogRecord> getEvents(long fromTimestamp, List<String> filterCategories)
091    {
092        // New events can occur at current time, retrieve only the fixed logs list: 1ms before now.
093        long toTimestamp = System.currentTimeMillis() - 1;
094        
095        synchronized (_logsQueue)
096        {
097            if (fromTimestamp <= 0 && filterCategories.isEmpty())
098            {
099                // returns all records
100                return new ArrayList<>(_logsQueue);
101            }
102            else if (toTimestamp < fromTimestamp)
103            {
104                return new ArrayList<>();
105            }
106            else
107            {
108                List<MemoryLogRecord> recentRecords = new ArrayList<>();
109                
110                Iterator<MemoryLogRecord> it = _logsQueue.descendingIterator();
111                while (it.hasNext())
112                {
113                    MemoryLogRecord record = it.next();
114                    if ((fromTimestamp <= 0 || record.millis() >= fromTimestamp) && record.millis() <= toTimestamp)
115                    {
116                        if (_matchCategoryFilter(filterCategories, record))
117                        {
118                            recentRecords.add(record);
119                        }
120                    }
121                    else
122                    {
123                        // Stop iteration
124                        break;
125                    }
126                }
127                
128                return recentRecords;
129            }
130        }
131    }
132    
133    private boolean _matchCategoryFilter(List<String> filterCategories, MemoryLogRecord record)
134    {
135        String recordCategory = record.category();
136        if (filterCategories.isEmpty() || filterCategories.contains(recordCategory))
137        {
138            return true;
139        }
140        
141        for (String category : filterCategories)
142        {
143            if (recordCategory.startsWith(category.concat(".")))
144            {
145                return true;
146            }
147        }
148        
149        return false;
150    }
151    
152    /**
153     * Remove the expired log records every 10 seconds
154     * @param from The expiration date in milliseconds
155     */
156    public void removeExpiredEvents(long from)
157    {
158        synchronized (_logsQueue)
159        {
160            if (_lastCleanTime != 0 && _lastCleanTime + __TIME_DURATION_BEFORE_NEXT_CLEAN > System.currentTimeMillis())
161            {
162                // The time between 2 cleansing has not yet expired, do nothing
163                return;
164            }
165            
166            // Remove logs before 'from'
167            MemoryLogRecord record = _logsQueue.peek();
168            while (record != null && record.millis() < from)
169            {
170                _logsQueue.remove();
171                record = _logsQueue.peek();
172            }
173            
174            _lastCleanTime = System.currentTimeMillis();
175        }
176    }
177    
178    @Override
179    public boolean requiresLayout()
180    {
181        return false;
182    }
183
184    @Override
185    public void close()
186    {
187        _logsQueue.clear();
188    }
189
190}