001/* 002 * Copyright 2015 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.plugins.admin.logs; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Calendar; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.Date; 024import java.util.Enumeration; 025import java.util.GregorianCalendar; 026import java.util.HashMap; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Map; 030import java.util.Set; 031import java.util.regex.Matcher; 032import java.util.regex.Pattern; 033 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.cocoon.ProcessingException; 037import org.apache.commons.lang3.Strings; 038import org.apache.excalibur.source.ModifiableSource; 039import org.apache.excalibur.source.ModifiableTraversableSource; 040import org.apache.excalibur.source.SourceException; 041import org.apache.excalibur.source.SourceResolver; 042import org.apache.excalibur.source.TraversableSource; 043import org.apache.log4j.Level; 044import org.apache.log4j.LogManager; 045import org.apache.log4j.Logger; 046import org.apache.log4j.spi.LoggerRepository; 047 048import org.ametys.core.ui.Callable; 049import org.ametys.core.ui.StaticClientSideElement; 050import org.ametys.core.util.DateUtils; 051 052/** 053 * Client side element for log actions 054 */ 055public class LogsClientSideElement extends StaticClientSideElement 056{ 057 /** The path to the application's logs folder */ 058 private static final String __LOGS_BASE = "ametys-home://logs/"; 059 private static final Pattern __DATED_LOG_FILENAME = Pattern.compile("(.*)[-_]\\d{4}[-_]\\d{2}(?:[-_]\\d{2})?.*\\.log(?:.gz)?"); 060 061 /** The Excalibur source resolver */ 062 private SourceResolver _sourceResolver; 063 064 @Override 065 public void service(ServiceManager serviceManager) throws ServiceException 066 { 067 super.service(serviceManager); 068 _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE); 069 } 070 071 /** 072 * Delete the log files 073 * @param files the files to delete 074 * @return the result map with the successes and failures files. 075 * @throws IOException if an error occurred during deletion 076 * @throws ProcessingException if an error occurred during deletion 077 */ 078 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 079 public Map<String, Object> deleteLogs(List<String> files) throws ProcessingException, IOException 080 { 081 Map<String, Object> result = new HashMap<>(); 082 List<String> failures = new ArrayList<> (); 083 List<String> successes = new ArrayList<> (); 084 085 if (getLogger().isInfoEnabled()) 086 { 087 getLogger().info("Administrator starts a deletion of " + files.size() + " logged file"); 088 } 089 090 // Delete the selected files one by one 091 for (String location : files) 092 { 093 if (_deleteFile(location)) 094 { 095 successes.add(location); 096 } 097 else 098 { 099 failures.add(location); 100 } 101 } 102 103 if (getLogger().isInfoEnabled()) 104 { 105 getLogger().info("Process terminated with following results : failure '" + failures.toString() + " successes '" + successes.toString()); 106 } 107 108 result.put("failures", failures); 109 result.put("successes", successes); 110 return result; 111 } 112 113 /** 114 * Delete the log entries that are at least 12 days old 115 * @return the result 116 * @throws ProcessingException if an exception occurs while processing the deletion 117 */ 118 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 119 public Map<String, Object> purgeLogs() throws ProcessingException 120 { 121 Calendar purgeCalendar = new GregorianCalendar(); 122 purgeCalendar.add(Calendar.DAY_OF_MONTH, -12); 123 Date purgeDate = new Date(purgeCalendar.getTimeInMillis()); 124 125 if (getLogger().isDebugEnabled()) 126 { 127 getLogger().debug("Starting purge..."); 128 getLogger().debug("Purge date is " + purgeDate); 129 } 130 131 List<String> filesList = new ArrayList<>(); 132 TraversableSource logsSources = null; 133 try 134 { 135 logsSources = (TraversableSource) _sourceResolver.resolveURI(__LOGS_BASE); 136 for (Object log : logsSources.getChildren()) 137 { 138 ModifiableTraversableSource logSource = (ModifiableTraversableSource) log; 139 if (!logSource.isCollection() 140 && (logSource.getURI().endsWith(".log") || logSource.getURI().endsWith(".log.gz")) 141 && !(logSource.getName().startsWith("forensic-")) 142 && new Date(logSource.getLastModified()).before(purgeDate)) 143 { 144 String location = logSource.getURI(); 145 String name = location.substring(location.lastIndexOf('/') + 1); 146 filesList.add(name); 147 if (getLogger().isDebugEnabled()) 148 { 149 getLogger().debug("Adding file to purge : " + name); 150 } 151 } 152 } 153 154 return deleteLogs(filesList); 155 } 156 catch (SourceException e) 157 { 158 String message = "The purge of old log files failed"; 159 getLogger().error(message, e); 160 throw new ProcessingException(message, e); 161 } 162 catch (IOException e) 163 { 164 String message = "The log directory was not found"; 165 getLogger().error(message, e); 166 throw new ProcessingException(message, e); 167 } 168 finally 169 { 170 if (logsSources != null) 171 { 172 _sourceResolver.release(logsSources); 173 } 174 } 175 } 176 177 private boolean _deleteFile(String fileLocation) throws IOException, ProcessingException 178 { 179 if (fileLocation.indexOf("/") != -1 || fileLocation.indexOf('\\') != -1) 180 { 181 String message = "The LogsDeleteAction has been call with the forbiden parameter '" + fileLocation + "'"; 182 getLogger().error(message); 183 throw new ProcessingException(message); 184 } 185 186 ModifiableSource logsource = null; 187 try 188 { 189 if (fileLocation.endsWith(".log")) 190 { 191 logsource = (ModifiableSource) _sourceResolver.resolveURI(__LOGS_BASE + fileLocation); 192 if (getLogger().isInfoEnabled()) 193 { 194 getLogger().info("Removing log file " + logsource.getURI()); 195 } 196 logsource.delete(); 197 return true; 198 } 199 else if (getLogger().isWarnEnabled()) 200 { 201 getLogger().warn("Ignoring this file during deletion '" + "' because it does not ends with .log"); 202 } 203 return false; 204 } 205 catch (SourceException e) 206 { 207 getLogger().error("The administrator tried unsuccessfully to remove the following log file '" + (logsource != null ? logsource.getURI() : fileLocation) + "'.", e); 208 return false; 209 } 210 finally 211 { 212 if (logsource != null) 213 { 214 _sourceResolver.release(logsource); 215 } 216 } 217 } 218 219 /** 220 * Change the log level of the selected category to the selected level 221 * @param level the selected level 222 * @param category the selected category 223 * @return a map 224 */ 225 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 226 public Map<String, Object> changeLogLevel(String level, String category) 227 { 228 Map<String, Object> result = new HashMap<>(); 229 230 if (getLogger().isInfoEnabled()) 231 { 232 getLogger().info("Administrator change log level"); 233 } 234 235 LoggerRepository loggerRepository = LogManager.getLoggerRepository(); 236 237 if (getLogger().isInfoEnabled()) 238 { 239 getLogger().info("Log level changing category '" + category + "' " + level); 240 } 241 242 try 243 { 244 _changeLogLevel(loggerRepository, category, level); 245 } 246 catch (Throwable t) 247 { 248 String errorMessage = "Cannot change log level correctly : changing category '" + category + "'"; 249 getLogger().error(errorMessage, t); 250 result.put("error", "error"); 251 return result; 252 } 253 254 return result; 255 } 256 257 private void _changeLogLevel(LoggerRepository loggerRepository, String category, String mode) 258 { 259 boolean inherited = "INHERIT".equals(mode) || "INHERITFORCED".equals(mode); 260 boolean force = "FORCE".equals(mode) || "INHERITFORCED".equals(mode); 261 262 Logger logger; 263 boolean isRoot = false; 264 265 if ("root".equals(category)) 266 { 267 isRoot = true; 268 logger = loggerRepository.getRootLogger(); 269 } 270 else 271 { 272 logger = loggerRepository.getLogger(category); 273 } 274 275 if (inherited && !isRoot) 276 { 277 logger.setLevel(null); 278 } 279 280 if (force) 281 { 282 Enumeration<Logger> e = loggerRepository.getCurrentLoggers(); 283 while (e.hasMoreElements()) 284 { 285 Logger l = e.nextElement(); 286 if (l.getParent() == logger) 287 { 288 _changeLogLevel(loggerRepository, l.getName(), "INHERITFORCED"); 289 } 290 } 291 } 292 293 if (!inherited && !force) 294 { 295 Level level = Level.toLevel(mode); 296 logger.setLevel(level); 297 } 298 } 299 300 /** 301 * Get the list of log files 302 * @return the list of log files 303 * @throws IOException if a error occurs 304 */ 305 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 306 public List<Map<String, Object>> getLogFiles() throws IOException 307 { 308 List<Map<String, Object>> logs = new ArrayList<>(); 309 TraversableSource logsDirectorySource = (TraversableSource) _sourceResolver.resolveURI(__LOGS_BASE); 310 311 try 312 { 313 Map<String, List<TraversableSource>> logSources = _prepareLogs(logsDirectorySource); 314 315 for (String logname : logSources.keySet()) 316 { 317 for (TraversableSource logSource : logSources.get(logname)) 318 { 319 Map<String, Object> log = new HashMap<>(); 320 log.put("name", logname); 321 log.put("location", logSource.getURI().substring(logSource.getURI().lastIndexOf('/') + 1)); 322 log.put("lastModified", DateUtils.dateToString(new Date(logSource.getLastModified()))); 323 log.put("size", Long.toString(logSource.getContentLength())); 324 325 logs.add(log); 326 } 327 328 } 329 } 330 finally 331 { 332 _sourceResolver.release(logsDirectorySource); 333 } 334 335 return logs; 336 } 337 338 private Map<String, List<TraversableSource>> _prepareLogs(TraversableSource logsDirectorySource) throws SourceException 339 { 340 Map<String, List<TraversableSource>> logs = new HashMap<>(); 341 342 if (logsDirectorySource.exists()) 343 { 344 Collection<TraversableSource> logFiles = logsDirectorySource.getChildren(); 345 for (TraversableSource logSource : logFiles) 346 { 347 if (!logSource.isCollection()) 348 { 349 String name = logSource.getName(); 350 String canonicalName = _getCanonicalName(name); 351 352 if (canonicalName != null) 353 { 354 List<TraversableSource> underLogs = logs.get(canonicalName); 355 if (underLogs == null) 356 { 357 underLogs = new ArrayList<>(); 358 logs.put(canonicalName, underLogs); 359 } 360 361 underLogs.add(logSource); 362 } 363 } 364 } 365 } 366 367 return logs; 368 } 369 370 private String _getCanonicalName(String name) 371 { 372 Matcher matcher = __DATED_LOG_FILENAME.matcher(name); 373 if (matcher.matches()) 374 { 375 return matcher.group(1); 376 } 377 else if (name.endsWith(".log")) 378 { 379 return name; 380 } 381 else 382 { 383 return null; 384 } 385 } 386 387 /** 388 * Get the log levels 389 * @return the JSON tree of log level by categories 390 */ 391 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 392 public Map<String, Object> getLogLevels() 393 { 394 LoggerRepository loggerRepository = LogManager.getLoggerRepository(); 395 List<Logger> loggers = new ArrayList<>(); 396 Enumeration<org.apache.log4j.Logger> enumLogger = loggerRepository.getCurrentLoggers(); 397 398 while (enumLogger.hasMoreElements()) 399 { 400 loggers.add(enumLogger.nextElement()); 401 } 402 403 loggers.add(loggerRepository.getRootLogger()); 404 405 Collections.sort(loggers, new LoggerComparator()); 406 407 Map<String, Object> logCategories = new HashMap<>(); 408 Set<Map<String, String>> categoryLevels = new HashSet<> (); 409 410 for (Logger logger : loggers) 411 { 412 Level level = logger.getLevel(); 413 414 String category = logger.getName(); 415 if (category.equals("root")) 416 { 417 logCategories.put("children", new ArrayList<> ()); 418 logCategories.put("name", "root"); 419 logCategories.put("fullname", "root"); 420 logCategories.put("level", level.toString()); 421 } 422 else 423 { 424 Map<String, String> categoryLevel = new HashMap<>(); 425 categoryLevel.put("category", category); 426 categoryLevel.put("level", level == null ? "inherit" : level.toString()); 427 categoryLevels.add(categoryLevel); 428 } 429 } 430 431 String fullName = null; 432 for (Map<String, String> categoryLevel : categoryLevels) 433 { 434 fullName = categoryLevel.get("category"); 435 @SuppressWarnings("unchecked") 436 List<Map<String, Object>> childrenCategories = (List<Map<String, Object>>) logCategories.get("children"); 437 createCategory(childrenCategories, fullName, categoryLevel.get("level"), fullName); 438 } 439 440 return Map.of("children", logCategories); 441 } 442 443 /** 444 * Creates a log category 445 * @param children the list of categories already created 446 * @param categoryName the name of the category to create 447 * @param level the level of the category to create 448 * @param fullName the full name of the category 449 */ 450 protected void createCategory(List<Map<String, Object>> children, String categoryName, String level, String fullName) 451 { 452 int i = categoryName.indexOf("."); 453 if (i != -1) 454 { 455 // Creates intermediary tree nodes 456 String parentCategoryName = categoryName.substring(0, i); 457 String childCategoryName = categoryName.substring(i + 1); 458 459 Map<String, Object> parentCategoryNode = _getNode(children, parentCategoryName); 460 461 if (parentCategoryNode == null) 462 { 463 // We did not find any existing node, let's create it 464 parentCategoryNode = new HashMap<> (); 465 parentCategoryNode.put("children", new ArrayList<> ()); 466 parentCategoryNode.put("leaf", false); 467 parentCategoryNode.put("level", "inherit"); 468 parentCategoryNode.put("name", parentCategoryName); 469 parentCategoryNode.put("fullname", fullName.substring(0, fullName.indexOf(parentCategoryName)) + parentCategoryName); 470 471 children.add(parentCategoryNode); 472 } 473 474 parentCategoryNode.put("leaf", false); 475 476 @SuppressWarnings("unchecked") 477 List<Map<String, Object>> childrenCategories = (List<Map<String, Object>>) parentCategoryNode.get("children"); 478 createCategory(childrenCategories, childCategoryName, level, fullName); 479 } 480 else 481 { 482 Map<String, Object> categoryNode = _getNode(children, categoryName); 483 if (categoryNode == null) 484 { 485 // Creates tree leaves 486 categoryNode = new HashMap<> (); 487 488 categoryNode.put("children", new ArrayList<> ()); 489 categoryNode.put("leaf", true); 490 categoryNode.put("level", level); 491 categoryNode.put("name", categoryName); 492 categoryNode.put("fullname", fullName); 493 494 children.add(categoryNode); 495 } 496 else 497 { 498 // The intermediary node was already created, let's update it 499 categoryNode.put("level", level); 500 } 501 } 502 } 503 504 private Map<String, Object> _getNode(List<Map<String, Object>> children, String categoryName) 505 { 506 for (Map<String, Object> child : children) 507 { 508 if (Strings.CS.equals((String) child.get("name"), categoryName)) 509 { 510 return child; 511 } 512 } 513 return null; 514 } 515}