001/*
002 *  Copyright 2018 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.plugins.core.ui.log.parser;
017
018import java.io.File;
019import java.io.IOException;
020import java.nio.charset.StandardCharsets;
021import java.time.LocalDateTime;
022import java.time.ZoneId;
023import java.time.ZonedDateTime;
024import java.time.format.DateTimeFormatter;
025import java.time.format.DateTimeParseException;
026import java.util.ArrayList;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033import org.apache.commons.io.input.ReversedLinesFileReader;
034import org.apache.commons.lang3.StringUtils;
035import org.slf4j.Logger;
036
037/**
038 * A log file parser.
039 * The log file is parsed by the end with a limit of [limit] events.
040 */
041public final class LogFileParser
042{
043    private static final DateTimeFormatter __DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss,SSS");
044    private static final Pattern __REGEXP = Pattern.compile("^([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}) ([A-Z ]{5}) \\[([^\\]]+)\\] \\(([^ ]+)\\) (.*)$");
045
046    /** Default constructor */
047    private LogFileParser()
048    {
049        // Nothing to do
050    }
051    
052    /**
053     * Parse the log file.
054     * @param logFile The log file
055     * @param filters The filters on logs
056     * @param limit The limit of events
057     * @param logger The logger
058     * @return A {@link List} of parsed log lines
059     * @throws IOException if an error occurs
060     */
061    public static List<Map<String, Object>> parseFile(File logFile, Map<String, Object> filters, int limit, Logger logger) throws IOException
062    {
063        List<Map<String, Object>> logLines = new ArrayList<>();
064        
065        try (ReversedLinesFileReader reader = new ReversedLinesFileReader(logFile, StandardCharsets.UTF_8))
066        {
067            StringBuilder stackTrace = new StringBuilder();
068            String line = reader.readLine();
069            while (line != null && (limit <= 0 || logLines.size() < limit))
070            {
071                Matcher matcher = __REGEXP.matcher(line);
072                if (matcher.matches())
073                {
074                    Map<String, Object> logLine = _getLogLine(matcher, stackTrace.toString(), logger);
075                    
076                    // Add if filters are OK
077                    if (_filter(logLine, filters))
078                    {
079                        logLines.add(logLine);
080                    }
081                    
082                    // Reset the detail
083                    stackTrace.setLength(0);
084                }
085                else
086                {
087                    // Stack trace
088                    stackTrace.insert(0, line + "<br/>");
089                }
090                line = reader.readLine();
091            }
092        }
093        
094        return logLines;
095    }
096    
097    private static Map<String, Object> _getLogLine(Matcher matcher, String stackTrace, Logger logger)
098    {
099        Map<String, Object> logLine = new HashMap<>();
100        logLine.put("timestamp", _convertToMs(matcher.group(1), logger));
101        logLine.put("level", matcher.group(2).trim());
102        logLine.put("category", matcher.group(3));
103        logLine.put("thread", matcher.group(4));
104        logLine.put("message", matcher.group(5));
105        logLine.put("callstack", stackTrace);
106        return logLine;
107    }
108    
109    private static long _convertToMs(String dateTime, Logger logger)
110    {
111        long milliseconds = 0;
112        try
113        {
114            LocalDateTime ldt = LocalDateTime.parse(dateTime, __DATE_FORMAT);
115            ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault());
116            milliseconds = zdt.toInstant().toEpochMilli();
117        }
118        catch (DateTimeParseException e)
119        {
120            logger.error("Impossible to parse the date {} to milliseconds with the format {}", dateTime, __DATE_FORMAT, e);
121        }
122        return milliseconds;
123    }
124    
125    @SuppressWarnings("unchecked")
126    private static boolean _filter(Map<String, Object> logLine, Map<String, Object> filters)
127    {
128        if (filters != null && !filters.isEmpty())
129        {
130            List<String> filterLevels = (List<String>) filters.get("level");
131            if (filterLevels != null && !filterLevels.isEmpty() && !filterLevels.contains(logLine.get("level")))
132            {
133                return false;
134            }
135
136            String filterMessage = (String) filters.get("message");
137            if (StringUtils.isNotBlank(filterMessage) && !StringUtils.containsIgnoreCase((String) logLine.get("message"), filterMessage))
138            {
139                return false;
140            }
141            
142            String filterCategory = (String) filters.get("category");
143            if (StringUtils.isNotBlank(filterCategory) && !StringUtils.containsIgnoreCase((String) logLine.get("category"), filterCategory))
144            {
145                return false;
146            }
147        }
148        
149        return true;
150    }
151}