Source of file LogHelper.php

Size: 18,195 Bytes - Last Modified: 2020-10-24T02:46:31+00:00

/home/travis/build/NextDom/nextdom-core/src/Helpers/LogHelper.php

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
<?php
/* This file is part of Jeedom.
 *
 * Jeedom is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Jeedom is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Jeedom. If not, see <http://www.gnu.org/licenses/>.
 */

/* This file is part of NextDom.
 *
 * NextDom is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * NextDom is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NextDom. If not, see <http://www.gnu.org/licenses/>.
 */

namespace NextDom\Helpers;

use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Logger;
use NextDom\Com\ComShell;
use NextDom\Exceptions\CoreException;
use NextDom\Managers\ConfigManager;
use NextDom\Managers\MessageManager;
use SplFileObject;

define('DEFAULT_MAX_LINES_IN_LOG', 200);

/**
 * Class LogHelper
 * @package NextDom\Helpers
 */
class LogHelper
{
    /**
     * @var array Logger cache
     */
    private static $logger = [];
    /**
     * @var array Config cache
     */
    private static $config = null;

    /**
     * Log an error
     *
     * @param string $targetLog Target log file
     * @param string $message Message to log
     * @param string $logicalId Logical id linked to this log (optional)
     *
     * @return bool True if log added
     *
     * @throws \Exception
     */
    public static function addError($targetLog, $message, $logicalId = '')
    {
        return self::add($targetLog, 'error', $message, $logicalId);
    }

    /**
     * Add a message to the log and ensure that there are never more than 1000 lines
     *
     * @param string $targetLog Target log file
     * @param string $logType Type of log
     * @param string $message Message to log
     * @param string $logicalId Logical id linked to this log (optional)
     *
     * @return bool True if log added
     *
     * @throws \Exception
     */
    public static function add($targetLog, $logType, $message, $logicalId = '')
    {
        if (trim($message) === '') {
            return false;
        }
        $logger = self::getLogger($targetLog);
        $action = 'add' . ucwords(strtolower($logType));
        if (method_exists($logger, $action)) {
            $logger->$action($message);
            try {
                $logLevel = Logger::toMonologLevel($logType);
                if ($logLevel > Logger::ALERT || ($logLevel === Logger::ERROR && self::getConfig('addMessageForErrorLog') == 1)) {
                    @MessageManager::add($targetLog, $message, '', $logicalId);
                }
            } catch (\Exception $e) {
                error_log('LOG ERROR : ' . $e->getMessage());
            }
        }
        return true;
    }

    /**
     * Get logger depends of target
     *
     * @param string $targetLog Target log
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public static function getLogger($targetLog)
    {
        if (isset(self::$logger[$targetLog])) {
            return self::$logger[$targetLog];
        }
        $formatter = new LineFormatter(str_replace('\n', "\n", self::getConfig('log::formatter')));
        self::$logger[$targetLog] = new Logger($targetLog);
        switch (self::getConfig('log::engine')) {
            case 'SyslogHandler':
                $handler = new SyslogHandler(self::getLogLevel($targetLog));
                break;
            case 'SyslogUdp':
                $handler = new SyslogUdpHandler(ConfigManager::byKey('log::syslogudphost'), ConfigManager::byKey('log::syslogudpport'), 'user', self::getLogLevel($targetLog));
                break;
            case 'StreamHandler':
            default:
                $handler = new StreamHandler(self::getPathToLog($targetLog), self::getLogLevel($targetLog));
                break;
        }
        $handler->setFormatter($formatter);
        self::$logger[$targetLog]->pushHandler($handler);
        return self::$logger[$targetLog];
    }

    /**
     * Get config data by key
     *
     * @param string $configKey
     * @param string $defaultValue
     * @return string
     * @throws \Exception
     */
    public static function getConfig($configKey, $defaultValue = '')
    {
        // Load config data
        if (self::$config === null) {
            self::$config = array_merge(ConfigManager::getLogLevelPlugin(), ConfigManager::byKeys(['log::engine', 'log::formatter', 'log::level', 'addMessageForErrorLog', 'maxLineLog']));
        }
        if (isset(self::$config[$configKey])) {
            return self::$config[$configKey];
        }
        return $defaultValue;
    }

    /**
     * Get log level from config
     *
     * @param $targetLog
     * @return int|string
     * @throws \Exception
     */
    public static function getLogLevel($targetLog)
    {
        $specificTargetLevel = self::getConfig('log::level::' . $targetLog);
        // Get log level when user configure different level
        if (is_array($specificTargetLevel)) {
            if (isset($specificTargetLevel['default']) && $specificTargetLevel['default'] == 1) {
                return self::getConfig('log::level');
            }
            // Test all levels and return the selected
            foreach ($specificTargetLevel as $level => $selected) {
                if (!is_numeric($level)) {
                    continue;
                }
                if ($selected == 1) {
                    return $level;
                }
            }
        }
        return self::getConfig('log::level');
    }

    /**
     * Get target path log (usually /var/log/nextdom)
     *
     * @param string $targetLog
     *
     * @return string Log path
     */
    public static function getPathToLog($targetLog = 'core'): string
    {
        return NEXTDOM_LOG . '/' . $targetLog;
    }

    /**
     * Log an information
     *
     * @param string $targetLog Target log file
     * @param string $message Message to log
     * @param string $logicalId Logical id linked to this log (optional)
     *
     * @return bool
     *
     * @throws \Exception
     */
    public static function addInfo($targetLog, $message, $logicalId = '')
    {
        return self::add($targetLog, 'info', $message, $logicalId);
    }

    /**
     * Log a debug
     *
     * @param string $targetLog Target log file
     * @param string $message Message to log
     * @param string $logicalId Logical id linked to this log (optional)
     *
     * @return bool
     *
     * @throws \Exception
     */
    public static function addDebug($targetLog, $message, $logicalId = '')
    {
        return self::add($targetLog, 'debug', $message, $logicalId);
    }

    /**
     * Log a critical message
     *
     * @param string $targetLog Target log file
     * @param string $message Message to log
     * @param string $logicalId Logical id linked to this log (optional)
     *
     * @return bool
     *
     * @throws \Exception
     */
    public static function addCritical($targetLog, $message, $logicalId = '')
    {
        return self::add($targetLog, 'critical', $message, $logicalId);
    }

    /**
     * Log an update message
     *
     * @param string $targetLog Target log file
     * @param string $message Message to log
     * @param string $logicalId Logical id linked to this log (optional)
     *
     * @return bool
     *
     * @throws \Exception
     */
    public static function addUpdate($targetLog, $message, $logicalId = '')
    {
        return self::add($targetLog, 'update', $message, $logicalId);
    }

    /**
     * Log an alert message
     *
     * @param string $targetLog Target log file
     * @param string $message Message to log
     * @param string $logicalId Logical id linked to this log (optional)
     *
     * @return bool
     *
     * @throws \Exception
     */
    public static function addAlert($targetLog, $message, $logicalId = '')
    {
        return self::add($targetLog, 'alert', $message, $logicalId);
    }

    /**
     * Remove all log by deleting files
     *
     * @return bool True on success
     *
     * @throws \Exception
     */
    public static function removeAll()
    {
        // find in log and sub folder scenrioLog
        foreach (['', 'scenarioLog/'] as $logPath) {
            $logs = FileSystemHelper::ls(self::getPathToLog($logPath), '*');
            foreach ($logs as $log) {
                self::remove($log);
            }
        }
        return true;
    }

    /**
     * Delete log file
     *
     * @param string $targetLog Name of the log file
     *
     * @return bool True on success
     *
     * @throws \Exception
     */
    public static function remove($targetLog)
    {
        // Skip Apache and Nginx log files
        if (strpos($targetLog, 'nginx.error') !== false || strpos($targetLog, 'http.error') !== false) {
            self::clear($targetLog);
            return false;
        }
        if (self::authorizeClearLog($targetLog)) {
            $path = self::getPathToLog($targetLog);
            ComShell::execute(SystemHelper::getCmdSudo() . 'chmod 664 ' . $path . ' > /dev/null 2>&1; rm ' . $path . ' 2>&1 > /dev/null');
            return false;
        }
        return null;
    }

    /**
     * Clear log file
     *
     * @param string $targetLog Name of the log file
     *
     * @return bool True on success
     *
     * @throws \Exception
     */
    public static function clear($targetLog): bool
    {
        if (self::authorizeClearLog($targetLog)) {
            $path = self::getPathToLog($targetLog);
            ComShell::execute(SystemHelper::getCmdSudo() . 'chmod 664 ' . $path . '> /dev/null 2>&1; cat /dev/null > ' . $path);
            return true;
        }
        return false;
    }

    /**
     * Check if target log file is good
     *
     * @param string $targetLog Name of the log file
     * @param string $subFolder Subfolder name
     *
     * @return bool
     */
    public static function authorizeClearLog($targetLog, $subFolder = '')
    {
        $path = self::getPathToLog($subFolder . $targetLog);
        return !((strpos($targetLog, '.htaccess') !== false)
            || (!file_exists($path) || !is_file($path)));
    }

    /**
     * Get log file content
     *
     * @param string $targetLog Target log
     * @param int $start Start row
     * @param int $nbLines Number of lines to get
     *
     * @return array|bool Content of the log or false on error
     *
     * @throws \Exception
     */
    public static function get($targetLog = 'core', $start = 0, $nbLines = DEFAULT_MAX_LINES_IN_LOG)
    {
        self::chunk($targetLog);
        $path = (!file_exists($targetLog) || !is_file($targetLog)) ? self::getPathToLog($targetLog) : $targetLog;
        if (!file_exists($path)) {
            return false;
        }
        $page = [];
        $log = new SplFileObject($path);
        if ($log) {
            $log->seek($start); //Seek to the beginning of lines
            $linesRead = 0;
            while ($log->valid() && $linesRead != $nbLines) {
                $line = trim($log->current()); //get current line
                if ($line != '') {
                    if (function_exists('mb_convert_encoding')) {
                        array_unshift($page, mb_convert_encoding($line, 'UTF-8'));
                    } else {
                        array_unshift($page, $line);
                    }
                }
                $log->next(); //go to next line
                $linesRead++;
            }
        }
        return $page;
    }

    /**
     * Reduce all logs files
     *
     * @param string $targetLog Target log or empty for all
     *
     * @throws \Exception
     */
    public static function chunk($targetLog = '')
    {
        $paths = [];
        if ($targetLog != '') {
            $paths[] = self::getPathToLog($targetLog);
        } else {
            $relativeLogPaths = ['', 'scenarioLog/'];
            foreach ($relativeLogPaths as $relativeLogPath) {
                $logPath = self::getPathToLog($relativeLogPath);
                $logs = FileSystemHelper::ls($logPath, '*');
                foreach ($logs as $log) {
                    $paths[] = $logPath . $log;
                }
            }
        }
        foreach ($paths as $path) {
            if (is_file($path)) {
                self::chunkLog($path);
            }
        }
    }

    /**
     * Reduce log file to DEFAULT_MAX_LINES_IN_LOG
     *
     * @param string $logFilePath Path of the file
     *
     * @throws \Exception
     */
    public static function chunkLog($logFilePath)
    {
        if (strpos($logFilePath, '.htaccess') !== false) {
            return;
        }
        $maxLineLog = self::getConfig('maxLineLog');
        if ($maxLineLog < DEFAULT_MAX_LINES_IN_LOG) {
            $maxLineLog = DEFAULT_MAX_LINES_IN_LOG;
        }
        try {
            ComShell::execute(SystemHelper::getCmdSudo() . 'chmod 664 ' . $logFilePath . ' > /dev/null 2>&1;echo "$(tail -n ' . $maxLineLog . ' ' . $logFilePath . ')" > ' . $logFilePath);
        } catch (\Exception $e) {

        }
        @chown($logFilePath, SystemHelper::getCommand('www-uid'));
        @chgrp($logFilePath, SystemHelper::getCommand('www-gid'));
        if (filesize($logFilePath) > (1024 * 1024 * 10)) {
            ComShell::execute(SystemHelper::getCmdSudo() . 'truncate -s 0 ' . $logFilePath);
        }
        if (filesize($logFilePath) > (1024 * 1024 * 10)) {
            ComShell::execute(SystemHelper::getCmdSudo() . 'cat /dev/null > ' . $logFilePath);
        }
        if (filesize($logFilePath) > (1024 * 1024 * 10)) {
            ComShell::execute(SystemHelper::getCmdSudo() . ' rm -f ' . $logFilePath);
        }
    }

    /**
     * Get list of log files
     *
     * @deprecated Use getLogFileList
     *
     * @param string $filter Pattern matchs
     *
     * @return array List of files
     */
    public static function liste($filter = null)
    {
        trigger_error('This method is deprecated', E_USER_DEPRECATED);
        return self::getLogFileList($filter);
    }

    /**
     * Get list of log files
     *
     * @param string $filter Pattern matchs
     *
     * @return array List of files
     */
    public static function getLogFileList($filter = null): array
    {
        $result = [];
        foreach (FileSystemHelper::ls(self::getPathToLog(''), '*') as $log) {
            if ($filter !== null && strpos($log, $filter) === false) {
                continue;
            }
            if (!is_dir(self::getPathToLog($log))) {
                $result[] = $log;
            }
        }
        return $result;
    }

    /**
     * Get list of all log files
     *
     * @param string $folder Log folder
     *
     * @return array List of files
     */
    public static function getAllLogFileList($folder = ''): array
    {
        $result = [];
        foreach (FileSystemHelper::ls(self::getPathToLog($folder), '*') as $log) {
            $item = ['name' => $log, 'content' => []];
            if (is_dir(self::getPathToLog($log))) {
                $item['content'] = self::getAllLogFileList($log);
            }
            $result[] = $item;
        }
        return $result;
    }

    /**
     * Fixe le niveau de rapport d'erreurs PHP
     * @param int $log_level
     * @throws \Exception
     * @since 2.1.4
     * @author KwiZeR <kwizer@kw12er.com>
     */
    public static function define_error_reporting($log_level)
    {
        switch ($log_level) {
            case logger::DEBUG:
            case logger::INFO:
            case logger::NOTICE:
                error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);
                break;
            case logger::WARNING:
                error_reporting(E_ERROR | E_WARNING | E_PARSE);
                break;
            case logger::ERROR:
                error_reporting(E_ERROR | E_PARSE);
                break;
            case logger::CRITICAL:
                error_reporting(E_ERROR | E_PARSE);
                break;
            case logger::ALERT:
                error_reporting(E_ERROR | E_PARSE);
                break;
            case logger::EMERGENCY:
                error_reporting(E_ERROR | E_PARSE);
                break;
            default:
                throw new CoreException('log::level invalide ("' . $log_level . '")');
                break;
        }
    }

    /**
     * Get exception message
     *
     * @param \Exception $e Exception
     *
     * @return mixed Message
     *
     * @throws \Exception
     */
    public static function exception($e)
    {
        if (self::getConfig('log::level') > 100) {
            return $e->getMessage();
        } else {
            return print_r($e, true);
        }
    }

    /**
     * Get log level name from log level value
     *
     * @param int $logLevelValue Log level value that correspond to specific level
     *
     * @return string|null Name of the log level or null
     */
    public static function convertLogLevel($logLevelValue = 100)
    {
        if ($logLevelValue > logger::EMERGENCY) {
            return 'none';
        }
        try {
            return strtolower(Logger::getLevelName($logLevelValue));
        } catch (\Exception $e) {

        }
        return null;
    }
}