You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
341 lines
9.5 KiB
341 lines
9.5 KiB
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle 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.
|
|
//
|
|
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* Task log manager.
|
|
*
|
|
* @package core
|
|
* @category task
|
|
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
|
* @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 <andrew@nicols.co.uk>
|
|
* @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;
|
|
}
|
|
}
|
|
|