001/* 002 * Copyright (C) Photon Vision. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program. If not, see <https://www.gnu.org/licenses/>. 016 */ 017 018package org.photonvision.common.logging; 019 020import java.io.*; 021import java.nio.file.Path; 022import java.text.ParseException; 023import java.text.SimpleDateFormat; 024import java.util.*; 025import java.util.function.Supplier; 026import org.apache.commons.lang3.tuple.Pair; 027// import org.photonvision.common.configuration.ConfigManager; 028import org.photonvision.common.configuration.PathManager; 029import org.photonvision.common.dataflow.DataChangeService; 030import org.photonvision.common.dataflow.events.OutgoingUIEvent; 031import org.photonvision.common.util.TimedTaskManager; 032 033/** TODO: get rid of static {} blocks and refactor to singleton pattern */ 034public class Logger { 035 private static final HashMap<LogGroup, LogLevel> levelMap = new HashMap<>(); 036 private static final List<LogAppender> currentAppenders = new ArrayList<>(); 037 038 private static final UILogAppender uiLogAppender = new UILogAppender(); 039 040 // // TODO why's the logger care about this? split it out 041 // private static KernelLogLogger klogListener = null; 042 043 static { 044 levelMap.put(LogGroup.Camera, LogLevel.INFO); 045 levelMap.put(LogGroup.General, LogLevel.INFO); 046 levelMap.put(LogGroup.WebServer, LogLevel.INFO); 047 levelMap.put(LogGroup.Data, LogLevel.INFO); 048 levelMap.put(LogGroup.VisionModule, LogLevel.INFO); 049 levelMap.put(LogGroup.Config, LogLevel.INFO); 050 levelMap.put(LogGroup.CSCore, LogLevel.TRACE); 051 levelMap.put(LogGroup.NetworkTables, LogLevel.DEBUG); 052 levelMap.put(LogGroup.System, LogLevel.DEBUG); 053 054 currentAppenders.add(new ConsoleLogAppender()); 055 currentAppenders.add(uiLogAppender); 056 addFileAppender(PathManager.getInstance().getLogPath()); 057 058 cleanLogs(PathManager.getInstance().getLogsDir()); 059 } 060 061 public static final String ANSI_RESET = "\u001B[0m"; 062 public static final String ANSI_BLACK = "\u001B[30m"; 063 public static final String ANSI_RED = "\u001B[31m"; 064 public static final String ANSI_GREEN = "\u001B[32m"; 065 public static final String ANSI_YELLOW = "\u001B[33m"; 066 public static final String ANSI_BLUE = "\u001B[34m"; 067 public static final String ANSI_PURPLE = "\u001B[35m"; 068 public static final String ANSI_CYAN = "\u001B[36m"; 069 public static final String ANSI_WHITE = "\u001B[37m"; 070 071 public static final int MAX_LOGS_TO_KEEP = 100; 072 073 private static final SimpleDateFormat simpleDateFormat = 074 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 075 076 private static final List<Pair<String, LogLevel>> uiBacklog = new ArrayList<>(); 077 private static boolean connected = false; 078 079 private final String className; 080 private final LogGroup group; 081 082 public Logger(Class<?> clazz, LogGroup group) { 083 this.className = clazz.getSimpleName(); 084 this.group = group; 085 } 086 087 public Logger(Class<?> clazz, String suffix, LogGroup group) { 088 this.className = clazz.getSimpleName() + " - " + suffix; 089 this.group = group; 090 } 091 092 public static String getDate() { 093 return simpleDateFormat.format(new Date()); 094 } 095 096 public static String format( 097 String logMessage, LogLevel level, LogGroup group, String clazz, boolean color) { 098 var date = getDate(); 099 var builder = new StringBuilder(); 100 if (color) builder.append(level.colorCode); 101 builder 102 .append("[") 103 .append(date) 104 .append("] [") 105 .append(group) 106 .append(" - ") 107 .append(clazz) 108 .append("] [") 109 .append(level.name()) 110 .append("] ") 111 .append(logMessage); 112 if (color) builder.append(ANSI_RESET); 113 return builder.toString(); 114 } 115 116 @SuppressWarnings("ResultOfMethodCallIgnored") 117 public static void addFileAppender(Path logFilePath) { 118 var file = logFilePath.toFile(); 119 if (!file.exists()) { 120 try { 121 file.getParentFile().mkdirs(); 122 file.createNewFile(); 123 } catch (IOException e) { 124 e.printStackTrace(); 125 } 126 } 127 currentAppenders.add(new FileLogAppender(logFilePath)); 128 } 129 130 public static void closeAllLoggers() { 131 currentAppenders.forEach(LogAppender::shutdown); 132 currentAppenders.clear(); 133 } 134 135 public static void cleanLogs(Path folderToClean) { 136 File[] logs = folderToClean.toFile().listFiles(); 137 if (logs == null) return; 138 LinkedList<File> logFileList = new LinkedList<>(Arrays.asList(logs)); 139 HashMap<File, Date> logFileStartDateMap = new HashMap<>(); 140 141 // Remove any files from the list for which we can't parse a start date from their name. 142 // Simultaneously populate our HashMap with Date objects representing the file-name 143 // indicated log start time. 144 logFileList.removeIf( 145 (File arg0) -> { 146 try { 147 logFileStartDateMap.put(arg0, PathManager.getInstance().logFnameToDate(arg0.getName())); 148 return false; 149 } catch (ParseException e) { 150 return true; 151 } 152 }); 153 154 // Execute a sort on the log file list by date in the filename. 155 logFileList.sort( 156 (File arg0, File arg1) -> { 157 Date date0 = logFileStartDateMap.get(arg0); 158 Date date1 = logFileStartDateMap.get(arg1); 159 return date1.compareTo(date0); 160 }); 161 162 int logCounter = 0; 163 for (File file : logFileList) { 164 // Due to filtering above, everything in logFileList should be a log file 165 if (logCounter < MAX_LOGS_TO_KEEP) { 166 // Skip over the first MAX_LOGS_TO_KEEP files 167 logCounter++; 168 } else { 169 // Delete this file. 170 file.delete(); 171 } 172 } 173 } 174 175 public static void setLevel(LogGroup group, LogLevel newLevel) { 176 levelMap.put(group, newLevel); 177 } 178 179 // TODO: profile 180 private static void log(String message, LogLevel level, LogGroup group, String clazz) { 181 for (var a : currentAppenders) { 182 var shouldColor = a instanceof ConsoleLogAppender; 183 var formattedMessage = format(message, level, group, clazz, shouldColor); 184 a.log(formattedMessage, level); 185 } 186 if (!connected) { 187 synchronized (uiBacklog) { 188 uiBacklog.add(Pair.of(format(message, level, group, clazz, false), level)); 189 } 190 } 191 } 192 193 public static void sendConnectedBacklog() { 194 connected = true; 195 synchronized (uiBacklog) { 196 for (var message : uiBacklog) { 197 uiLogAppender.log(message.getLeft(), message.getRight()); 198 } 199 uiBacklog.clear(); 200 } 201 } 202 203 public boolean shouldLog(LogLevel logLevel) { 204 return logLevel.code <= levelMap.get(group).code; 205 } 206 207 public void log(String message, LogLevel level) { 208 if (shouldLog(level)) { 209 log(message, level, group, className); 210 } 211 } 212 213 private void log(String message, LogLevel messageLevel, LogLevel conditionalLevel) { 214 if (shouldLog(conditionalLevel)) { 215 log(message, messageLevel, group, className); 216 } 217 } 218 219 private void log(Supplier<String> messageSupplier, LogLevel level) { 220 if (shouldLog(level)) { 221 log(messageSupplier.get(), level, group, className); 222 } 223 } 224 225 private void log( 226 Supplier<String> messageSupplier, LogLevel messageLevel, LogLevel conditionalLevel) { 227 if (shouldLog(conditionalLevel)) { 228 log(messageSupplier.get(), messageLevel, group, className); 229 } 230 } 231 232 public void error(Supplier<String> messageSupplier) { 233 log(messageSupplier, LogLevel.ERROR); 234 } 235 236 public void error(String message) { 237 log(message, LogLevel.ERROR); 238 } 239 240 /** 241 * Logs an error message with the stack trace of a Throwable. The stacktrace will only be printed 242 * if the current LogLevel is TRACE 243 * 244 * @param message 245 * @param t 246 */ 247 public void error(String message, Throwable t) { 248 log(message + ": " + t.getMessage(), LogLevel.ERROR); 249 log(convertStackTraceToString(t), LogLevel.ERROR, LogLevel.DEBUG); 250 } 251 252 public void warn(Supplier<String> messageSupplier) { 253 log(messageSupplier, LogLevel.WARN); 254 } 255 256 public void warn(String message) { 257 log(message, LogLevel.WARN); 258 } 259 260 public void info(Supplier<String> messageSupplier) { 261 log(messageSupplier, LogLevel.INFO); 262 } 263 264 public void info(String message) { 265 log(message, LogLevel.INFO); 266 } 267 268 public void debug(Supplier<String> messageSupplier) { 269 log(messageSupplier, LogLevel.DEBUG); 270 } 271 272 public void debug(String message) { 273 log(message, LogLevel.DEBUG); 274 } 275 276 public void trace(Supplier<String> messageSupplier) { 277 log(messageSupplier, LogLevel.TRACE); 278 } 279 280 public void trace(String message) { 281 log(message, LogLevel.TRACE); 282 } 283 284 private static String convertStackTraceToString(Throwable throwable) { 285 try (StringWriter sw = new StringWriter(); 286 PrintWriter pw = new PrintWriter(sw)) { 287 throwable.printStackTrace(pw); 288 return sw.toString(); 289 } catch (IOException ioe) { 290 throw new IllegalStateException(ioe); 291 } 292 } 293 294 private interface LogAppender { 295 void log(String message, LogLevel level); 296 297 /** Release any file or other resources currently held by the Logger */ 298 default void shutdown() {} 299 } 300 301 private static class ConsoleLogAppender implements LogAppender { 302 @Override 303 public void log(String message, LogLevel level) { 304 System.out.println(message); 305 } 306 } 307 308 private static class UILogAppender implements LogAppender { 309 @Override 310 public void log(String message, LogLevel level) { 311 var messageMap = new HashMap<String, Object>(); 312 messageMap.put("logMessage", message); 313 messageMap.put("logLevel", level.code); 314 var superMap = new HashMap<String, Object>(); 315 superMap.put("logMessage", messageMap); 316 DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("log", superMap)); 317 } 318 } 319 320 private static class FileLogAppender implements LogAppender { 321 private OutputStream out; 322 private boolean wantsFlush; 323 324 public FileLogAppender(Path logFilePath) { 325 try { 326 this.out = new FileOutputStream(logFilePath.toFile()); 327 TimedTaskManager.getInstance() 328 .addTask( 329 "FileLogAppender", 330 () -> { 331 try { 332 if (wantsFlush) { 333 out.flush(); 334 wantsFlush = false; 335 } 336 } catch (IOException ignored) { 337 } 338 }, 339 3000L); 340 } catch (FileNotFoundException e) { 341 out = null; 342 System.err.println("Unable to log to file " + logFilePath); 343 } 344 } 345 346 @Override 347 public void log(String message, LogLevel level) { 348 message += "\n"; 349 try { 350 out.write(message.getBytes()); 351 wantsFlush = true; 352 } catch (IOException e) { 353 e.printStackTrace(); 354 } catch (NullPointerException e) { 355 // Nothing to do - no stream available for writing 356 } 357 } 358 359 @Override 360 public void shutdown() { 361 try { 362 out.close(); 363 out = null; 364 } catch (IOException e) { 365 // TODO Auto-generated catch block 366 e.printStackTrace(); 367 } 368 } 369 } 370}