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}