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}