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