. /** * Task log manager. * * @package core * @category task * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\task; defined('MOODLE_INTERNAL') || die(); /** * Task log manager. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class logmanager { /** @var int Do not log anything */ const MODE_NONE = 0; /** @var int Log all tasks */ const MODE_ALL = 1; /** @var int Only log fails */ const MODE_FAILONLY = 2; /** @var int The default chunksize to use in ob_start */ const CHUNKSIZE = 1; /** * @var \core\task\task_base The task being logged. */ protected static $task = null; /** * @var \stdClass Metadata about the current log */ protected static $taskloginfo = null; /** * @var \resource The current filehandle used for logging */ protected static $fh = null; /** * @var string The path to the log file */ protected static $logpath = null; /** * @var bool Whether the task logger has been registered with the shutdown handler */ protected static $tasklogregistered = false; /** * @var int The level of output buffering in place before starting. */ protected static $oblevel = null; /** * Create a new task logger for the specified task, and prepare for logging. * * @param \core\task\task_base $task The task being run */ public static function start_logging(task_base $task) { global $DB; if (!self::should_log()) { return; } // We register a shutdown handler to ensure that logs causing any failures are correctly disposed of. // Note: This must happen before the per-request directory is requested because the shutdown handler deletes the logfile. if (!self::$tasklogregistered) { \core_shutdown_manager::register_function(function() { // These will only actually do anything if capturing is current active when the thread ended, which // constitutes a failure. \core\task\logmanager::finalise_log(true); }); // Create a brand new per-request directory basedir. get_request_storage_directory(true, true); self::$tasklogregistered = true; } if (self::is_current_output_buffer()) { // We cannot capture when we are already capturing. throw new \coding_exception('Logging is already in progress for task "' . get_class(self::$task) . '". ' . 'Nested logging is not supported.'); } // Store the initial data about the task and current state. self::$task = $task; self::$taskloginfo = (object) [ 'dbread' => $DB->perf_get_reads(), 'dbwrite' => $DB->perf_get_writes(), 'timestart' => microtime(true), ]; // For simplicity's sake we always store logs on disk and flush at the end. self::$logpath = make_request_directory() . DIRECTORY_SEPARATOR . "task.log"; self::$fh = fopen(self::$logpath, 'w+'); // Note the level of the current output buffer. // Note: You cannot use ob_get_level() as it will return `1` when the default output buffer is enabled. if ($obstatus = ob_get_status()) { self::$oblevel = $obstatus['level']; } else { self::$oblevel = null; } // Start capturing output. ob_start([\core\task\logmanager::class, 'add_line'], self::CHUNKSIZE); } /** * Whether logging is possible and should be happening. * * @return bool */ protected static function should_log() : bool { global $CFG; // Respect the config setting. if (isset($CFG->task_logmode) && empty($CFG->task_logmode)) { return false; } $loggerclass = self::get_logger_classname(); if (empty($loggerclass)) { return false; } return $loggerclass::is_configured(); } /** * Return the name of the logging class to use. * * @return string */ public static function get_logger_classname() : string { global $CFG; if (!empty($CFG->task_log_class)) { // Configuration is present to use an alternative task logging class. return $CFG->task_log_class; } // Fall back on the default database logger. return database_logger::class; } /** * Whether this task logger has a report available. * * @return bool */ public static function has_log_report() : bool { $loggerclass = self::get_logger_classname(); return $loggerclass::has_log_report(); } /** * Whether to use the standard settings form. */ public static function uses_standard_settings() : bool { $classname = self::get_logger_classname(); if (!class_exists($classname)) { return false; } if (is_a($classname, database_logger::class, true)) { return true; } return false; } /** * Get any URL available for viewing relevant task log reports. * * @param string $classname The task class to fetch for * @return \moodle_url */ public static function get_url_for_task_class(string $classname) : \moodle_url { $loggerclass = self::get_logger_classname(); return $loggerclass::get_url_for_task_class($classname); } /** * Whether we are the current log collector. * * @return bool */ protected static function is_current_output_buffer() : bool { if (empty(self::$taskloginfo)) { return false; } if ($ob = ob_get_status()) { return 'core\\task\\logmanager::add_line' == $ob['name']; } return false; } /** * Whether we are capturing at all. * * @return bool */ protected static function is_capturing() : bool { $buffers = ob_get_status(true); foreach ($buffers as $ob) { if ('core\\task\\logmanager::add_line' == $ob['name']) { return true; } } return false; } /** * Finish writing for the current task. * * @param bool $failed */ public static function finalise_log(bool $failed = false) { global $CFG, $DB, $PERF; if (!self::should_log()) { return; } if (!self::is_capturing()) { // Not capturing anything. return; } // Ensure that all logs are closed. $buffers = ob_get_status(true); foreach (array_reverse($buffers) as $ob) { if (null !== self::$oblevel) { if ($ob['level'] <= self::$oblevel) { // Only close as far as the initial output buffer level. break; } } // End and flush this buffer. ob_end_flush(); if ('core\\task\\logmanager::add_line' == $ob['name']) { break; } } self::$oblevel = null; // Flush any remaining buffer. self::flush(); // Close and unset the FH. fclose(self::$fh); self::$fh = null; if ($failed || empty($CFG->task_logmode) || self::MODE_ALL == $CFG->task_logmode) { // Finalise the log. $loggerclass = self::get_logger_classname(); $loggerclass::store_log_for_task( self::$task, self::$logpath, $failed, $DB->perf_get_reads() - self::$taskloginfo->dbread, $DB->perf_get_writes() - self::$taskloginfo->dbwrite - $PERF->logwrites, self::$taskloginfo->timestart, microtime(true) ); } // Tidy up. self::$logpath = null; self::$taskloginfo = null; } /** * Flush the current output buffer. * * This function will ensure that we are the current output buffer handler. */ public static function flush() { // We only call ob_flush if the current output buffer belongs to us. if (self::is_current_output_buffer()) { ob_flush(); } } /** * Add a log record to the task log. * * @param string $log * @return string */ public static function add_line(string $log) : string { if (empty(self::$taskloginfo)) { return $log; } if (empty(self::$fh)) { return $log; } if (self::is_current_output_buffer()) { fwrite(self::$fh, $log); } return $log; } }