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    public List<MemoryLogRecord> getEvents(long fromTimestamp)
100    {
101        return getEvents(fromTimestamp, Collections.EMPTY_LIST);
102    }
103    
104    /**
105     * Get the logs from a given timestamp and filtered by specific categories
106     * @param fromTimestamp If greater than 0, get the events more recent than this timestamp parameter. If equals or less than 0, returns all events
107     * @param filterCategories The filter categories
108     * @return The log events
109     */
110    public List<MemoryLogRecord> getEvents(long fromTimestamp, List<String> filterCategories)
111    {
112        // New events can occur at current time, retrieve only the fixed logs list: 1ms before now.
113        long toTimestamp = System.currentTimeMillis() - 1;
114        
115        synchronized (_logsQueue)
116        {
117            if (fromTimestamp <= 0 && filterCategories.isEmpty())
118            {
119                // returns all records
120                return new ArrayList<>(_logsQueue);
121            }
122            else if (toTimestamp < fromTimestamp)
123            {
124                return new ArrayList<>();
125            }
126            else
127            {
128                List<MemoryLogRecord> recentRecords = new ArrayList<>();
129                
130                Iterator<MemoryLogRecord> it = _logsQueue.descendingIterator();
131                while (it.hasNext())
132                {
133                    MemoryLogRecord record = it.next();
134                    if ((fromTimestamp <= 0 || record.getMillis() >= fromTimestamp) && record.getMillis() <= toTimestamp)
135                    {
136                        if (_matchCategoryFilter(filterCategories, record))
137                        {
138                            recentRecords.add(record);
139                        }
140                    }
141                    else
142                    {
143                        // Stop iteration
144                        break;
145                    }
146                }
147                
148                return recentRecords;
149            }
150        }
151    }
152    
153    private boolean _matchCategoryFilter(List<String> filterCategories, MemoryLogRecord record)
154    {
155        String recordCategory = record.getCategory();
156        if (filterCategories.isEmpty() || filterCategories.contains(recordCategory))
157        {
158            return true;
159        }
160        
161        for (String category : filterCategories)
162        {
163            if (recordCategory.startsWith(category.concat(".")))
164            {
165                return true;
166            }
167        }
168        
169        return false;
170    }
171    
172    /**
173     * Remove the expired log records every 10 seconds
174     * @param from The expiration date in milliseconds
175     */
176    public void removeExpiredEvents(long from)
177    {
178        synchronized (_logsQueue)
179        {
180            if (_lastCleanTime != 0 && _lastCleanTime + __TIME_DURATION_BEFORE_NEXT_CLEAN > System.currentTimeMillis())
181            {
182                // The time between 2 cleansing has not yet expired, do nothing
183                return;
184            }
185            
186            // Remove logs before 'from'
187            MemoryLogRecord record = _logsQueue.peek();
188            while (record != null && record.getMillis() < from)
189            {
190                _logsQueue.remove();
191                record = _logsQueue.peek();
192            }
193            
194            _lastCleanTime = System.currentTimeMillis();
195        }
196    }
197    
198    @Override
199    public boolean requiresLayout()
200    {
201        return false;
202    }
203
204    @Override
205    public void close() 
206    {
207        _logsQueue.clear();
208    }
209
210}