<?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;
/**
* @var bool Output logged content to screen.
*/
protected static $outputloggedcontent = true;
/**
* 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 $CFG, $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;
}
self::$outputloggedcontent = !empty($CFG->task_logtostdout);
// 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,
> $DB->perf_get_writes() - self::$taskloginfo->dbwrite,
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);
}
if (self::$outputloggedcontent) {
return $log;
} else {
return '';
}
}
}