gboudreau/Greyhole

View on GitHub
includes/Log.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
/*
Copyright 2014-2017 Guillaume Boudreau

This file is part of Greyhole.

Greyhole 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.

Greyhole 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 Greyhole.  If not, see <http://www.gnu.org/licenses/>.
*/

define('ACTION_INITIALIZE', 'initialize');
define('ACTION_UNKNOWN', 'unknown');
define('ACTION_DAEMON', 'daemon');
define('ACTION_PAUSE', 'pause');
define('ACTION_RESUME', 'resume');
define('ACTION_FSCK', 'fsck');
define('ACTION_CANCEL_FSCK', 'cancel-fsck');
define('ACTION_BALANCE', 'balance');
define('ACTION_CANCEL_BALANCE', 'cancel-balance');
define('ACTION_STATS', 'stats');
define('ACTION_STATUS', 'status');
define('ACTION_LOGS', 'logs');
define('ACTION_EMPTY_TRASH', 'empty-trash');
define('ACTION_VIEW_QUEUE', 'view-queue');
define('ACTION_IOSTAT', 'iostat');
define('ACTION_GETUID', 'getuid');
define('ACTION_MD5_WORKER', 'md5-worker');
define('ACTION_FIX_SYMLINKS', 'fix-symlinks');
define('ACTION_REPLACE', 'replaced');
define('ACTION_WAIT_FOR', 'wait-for');
define('ACTION_REMOVE', 'remove');
define('ACTION_THAW', 'thaw');
define('ACTION_DEBUG', 'debug');
define('ACTION_DELETE_METADATA', 'delete-metadata');
define('ACTION_REMOVE_SHARE', 'remove-share');

define('ACTION_CHECK_POOL', 'check_pool');
define('ACTION_SLEEP', 'sleep');
define('ACTION_READ_SAMBA_POOL', 'read_smb_spool');
define('ACTION_FSCK_FILE', 'fsck_file');
define('ACTION_MOVE', 'move');
define('ACTION_CP', 'cp');

final class Log {
    const PERF     = 9;
    const TEST     = 8;
    const DEBUG    = 7;
    const INFO     = 6;
    const WARN     = 4;
    const ERROR    = 3;
    const CRITICAL = 2;

    const EVENT_CODE_ALL_DRIVES_FULL = 'all_drives_full';
    const EVENT_CODE_CONFIG_DEPRECATED_OPTION = 'config_deprecated_option';
    const EVENT_CODE_CONFIG_TESTPARM_FAILED = 'config_testparm_failed';
    const EVENT_CODE_CONFIG_FILE_PARSING_FAILED = 'config_file_parsing_failed';
    const EVENT_CODE_CONFIG_HOOK_SCRIPT_NOT_EXECUTABLE = 'config_hook_script_not_executable';
    const EVENT_CODE_CONFIG_INCLUDE_INSECURE_PERMISSIONS = 'config_include_insecure_permissions';
    const EVENT_CODE_CONFIG_INVALID_VALUE = 'config_invalid_value';
    const EVENT_CODE_CONFIG_LZ_INSIDE_STORAGE_POOL = 'config_lz_inside_storage_pool';
    const EVENT_CODE_CONFIG_NO_STORAGE_POOL = 'config_no_storage_pool';
    const EVENT_CODE_CONFIG_SHARE_MISSING_FROM_SMB_CONF = 'config_share_missing_from_smb_conf';
    const EVENT_CODE_CONFIG_STORAGE_POOL_DRIVES_SAME_PARTITION = 'config_storage_pool_drives_same_partition';
    const EVENT_CODE_CONFIG_STORAGE_POOL_DRIVE_NOT_IN_DRIVE_SELECTION_ALGO = 'config_storage_pool_drive_not_in_drive_selection_algo';
    const EVENT_CODE_CONFIG_STORAGE_POOL_INSIDE_LZ = 'config_storage_pool_inside_lz';
    const EVENT_CODE_CONFIG_UNPARSEABLE_LINE = 'config_unparseable_line';
    const EVENT_CODE_DB_CONNECT_FAILED = 'db_connect_failed';
    const EVENT_CODE_DB_MIGRATION_FAILED = 'db_migration_failed';
    const EVENT_CODE_DB_TABLE_CRASHED = 'db_table_crashed';
    const EVENT_CODE_DB_TZ_CHANGE_FAILED = 'db_tz_change_failed';
    const EVENT_CODE_FILE_COPY_FAILED = 'file_copy_failed';
    const EVENT_CODE_FILE_INVALID = 'file_invalid_character';
    const EVENT_CODE_FSCK_REPORT = 'fsck_report';
    const EVENT_CODE_FSCK_EMPTY_FILE_COPY_FOUND = 'fsck_empty_file_copy_found';
    const EVENT_CODE_FSCK_MD5_LOG_FAILURE = 'fsck_md5_log_failure';
    const EVENT_CODE_FSCK_MD5_MISMATCH = 'fsck_md5_mismatch';
    const EVENT_CODE_FSCK_METAFILE_ROOT_PATH_NOT_FOUND = 'fsck_metafile_root_path_not_found';
    const EVENT_CODE_FSCK_NO_FILE_COPIES = 'fsck_no_file_copies';
    const EVENT_CODE_FSCK_SIZE_MISMATCH_FILE_COPY_FOUND = 'fsck_size_mismatch_file_copy_found';
    const EVENT_CODE_FSCK_SYMLINK_FOUND_IN_STORAGE_POOL = 'fsck_symlink_found_in_storage_pool';
    const EVENT_CODE_FSCK_UNKNOWN_FOLDER = 'fsck_unknown_folder';
    const EVENT_CODE_FSCK_UNKNOWN_SHARE = 'fsck_unknown_share';
    const EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE = 'hook_non_zero_exit_code';
    const EVENT_CODE_HOOK_NOT_EXECUTABLE = 'hook_not_executable';
    const EVENT_CODE_IDLE = "idle";
    const EVENT_CODE_IDLE_NOT = "busy";
    const EVENT_CODE_LIST_DIR_FAILED = 'list_dir_failed';
    const EVENT_CODE_MEMORY_LIMIT_REACHED = 'memory_limit_reached';
    const EVENT_CODE_METADATA_POINTS_TO_GONE_DRIVE = 'metadata_points_to_gone_drive';
    const EVENT_CODE_MKDIR_CHGRP_FAILED = 'mkdir_chgrp_failed';
    const EVENT_CODE_MKDIR_CHMOD_FAILED = 'mkdir_chmod_failed';
    const EVENT_CODE_MKDIR_CHOWN_FAILED = 'mkdir_chown_failed';
    const EVENT_CODE_MKDIR_FAILED = 'mkdir_failed';
    const EVENT_CODE_NO_METADATA_SAVED = 'no_metadata_saved';
    const EVENT_CODE_PHP_CRITICAL = 'php_critical';
    const EVENT_CODE_PHP_ERROR = 'php_error';
    const EVENT_CODE_PHP_WARNING = 'php_warning';
    const EVENT_CODE_RENAME_FILE_COPY_FAILED = 'rename_file_copy_failed';
    const EVENT_CODE_SAMBA_RESTART_FAILED = 'samba_restart_failed';
    const EVENT_CODE_SETTINGS_READ_ERROR = 'settings_read_error';
    const EVENT_CODE_SHARE_MISSING_FROM_GREYHOLE_CONF = 'share_missing_from_greyhole_conf';
    const EVENT_CODE_SPOOL_MOUNT_FAILED = 'spool_mount_failed';
    const EVENT_CODE_STORAGE_POOL_DRIVE_DF_FAILED = 'storage_pool_drive_df_failed';
    const EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED = 'storage_pool_drive_uuid_changed';
    const EVENT_CODE_STORAGE_POOL_FOLDER_NOT_FOUND = 'storage_pool_folder_not_found';
    const EVENT_CODE_TASK_FOR_UNKNOWN_SHARE = 'task_for_unknown_share';
    const EVENT_CODE_TRASH_NOT_FOUND = 'trash_not_found';
    const EVENT_CODE_TRASH_SYMLINK_FAILED = 'trash_symlink_failed';
    const EVENT_CODE_UNEXPECTED_VAR = 'unexpected_var';
    const EVENT_CODE_VFS_MODULE_WRONG = 'vfs_module_wrong';
    const EVENT_CODE_ZFS_UNKNOWN_DEVICE = 'zfs_unknown_device';

    private static $log_level_names = array(
        9 => 'PERF',
        8 => 'TEST',
        7 => 'DEBUG',
        6 => 'INFO',
        4 => 'WARN',
        3 => 'ERROR',
        2 => 'CRITICAL',
    );

    private static $action = ACTION_INITIALIZE;
    private static $old_action;
    private static $level = Log::INFO; // Default, until we are able to read the config file

    public static function setLevel($level) {
        self::$level = $level;
    }

    public static function getLevel() {
        return self::$level;
    }

    public static function setAction($action) {
        self::$old_action = self::$action;
        self::$action = str_replace(':', '', $action);
    }

    public static function actionIs($action) {
        return self::$action == $action;
    }

    public static function restorePreviousAction() {
        self::$action = self::$old_action;
    }

    public static function perf($text, $event_code = NULL) {
        self::_log(self::PERF, $text, $event_code);
    }

    public static function debug($text, $event_code = NULL) {
        self::_log(self::DEBUG, $text, $event_code);
    }

    public static function info($text, $event_code = NULL) {
        self::_log(self::INFO, $text, $event_code);
    }

    public static function warn($text, $event_code) {
        self::_log(self::WARN, $text, $event_code);
    }

    public static function error($text, $event_code) {
        self::_log(self::ERROR, $text, $event_code);
    }

    public static function critical($text, $event_code) {
        self::_log(self::CRITICAL, $text, $event_code);
    }

    private static $last_log;

    private static function _log($local_log_level, $text, $event_code) {
        if (self::$action == 'test-config' || self::$action == ACTION_CP) {
            $greyhole_log_file = NULL;
            $use_syslog = FALSE;
            if (self::$action == ACTION_CP && $local_log_level > self::$level) {
                return;
            }
        } elseif (self::$action == ACTION_INITIALIZE && !DaemonRunner::isCurrentProcessDaemon()) {
            if ($local_log_level === self::CRITICAL) {
                if (defined('IS_WEB_APP')) {
                    throw new Exception($text);
                }
                exit(1);
            }
            return;
        } else {
            if (DaemonRunner::isCurrentProcessDaemon() && self::$action != ACTION_READ_SAMBA_POOL) {
                try {
                    if ($text != static::$last_log) { // Don't log duplicates (Sleeping... for example)
                        $q = "INSERT INTO status SET log = :log, action = :action";
                        DB::insert($q, array('log' => $text, 'action' => empty(self::$action) ? ACTION_UNKNOWN : self::$action));
                    }
                    static::$last_log = $text;
                } catch (\Exception $ex) {
                    $text .= " [ERROR logging status in database: " . $ex->getMessage() . "]";
                    $local_log_level = self::ERROR;
                }
            }
            if ($local_log_level > self::$level) {
                return;
            }
            $greyhole_log_file = Config::get(CONFIG_GREYHOLE_LOG_FILE);
            $use_syslog = strtolower($greyhole_log_file) == 'syslog';
        }

        $date = date("M d H:i:s");
        if (self::$level >= self::PERF) {
            $utimestamp = microtime(true);
            $timestamp = floor($utimestamp);
            $date .= '.' . round(($utimestamp - $timestamp) * 1000000);
        }

        $log_level_string = $use_syslog ? $local_log_level : self::$log_level_names[$local_log_level];
        $log_text = sprintf("%s%s%s\n",
            "$date $log_level_string " . self::$action . ": ",
            $text,
            Config::get(CONFIG_LOG_MEMORY_USAGE) ? " [" . memory_get_usage() . "]" : ''
        );

        if ($use_syslog) {
            $worked = syslog($local_log_level, $log_text);
        } else if (!empty($greyhole_log_file)) {
            $worked = @error_log($log_text, 3, $greyhole_log_file);

            // Log to error log too?
            $greyhole_error_log_file = Config::get(CONFIG_GREYHOLE_ERROR_LOG_FILE);
            if ($local_log_level <= self::WARN && !empty($greyhole_error_log_file)) {
                $worked &= @error_log($log_text, 3, $greyhole_error_log_file);
            }
        } else {
            $worked = FALSE;
        }
        if (!$worked || $local_log_level === self::CRITICAL) {
            error_log(trim($log_text));
        }

        if ($local_log_level == self::WARN) {
            // Prevent infinite loop; this warning came from running a warning Hook, so...
            if ($event_code != LogHook::EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE_IN_WARN) {
                LogHook::trigger(LogHook::EVENT_TYPE_WARNING, $event_code, $log_text);
            }
        } elseif ($local_log_level == self::ERROR) {
            LogHook::trigger(LogHook::EVENT_TYPE_ERROR, $event_code, $log_text);
        } elseif ($local_log_level == self::CRITICAL) {
            LogHook::trigger(LogHook::EVENT_TYPE_CRITICAL, $event_code, $log_text);
        }

        if ($local_log_level === self::CRITICAL) {
            if (defined('IS_WEB_APP')) {
                throw new Exception($text);
            }
            exit(1);
        }
    }

    public static function cleanStatusTable() {
        $q = "SELECT MAX(id) FROM status";
        $max_id = DB::getFirstValue($q);
        $q = "DELETE FROM status WHERE id < :id";
        DB::execute($q, array('id' => max(0, $max_id - 100)));
    }
}

?>