Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Task log manager.
  19   *
  20   * @package    core
  21   * @category   task
  22   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  namespace core\task;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Task log manager.
  31   *
  32   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class logmanager {
  36  
  37      /** @var int Do not log anything */
  38      const MODE_NONE = 0;
  39  
  40      /** @var int Log all tasks */
  41      const MODE_ALL = 1;
  42  
  43      /** @var int Only log fails */
  44      const MODE_FAILONLY = 2;
  45  
  46      /** @var int The default chunksize to use in ob_start */
  47      const CHUNKSIZE = 1;
  48  
  49      /**
  50       * @var \core\task\task_base The task being logged.
  51       */
  52      protected static $task = null;
  53  
  54      /**
  55       * @var \stdClass Metadata about the current log
  56       */
  57      protected static $taskloginfo = null;
  58  
  59      /**
  60       * @var \resource The current filehandle used for logging
  61       */
  62      protected static $fh = null;
  63  
  64      /**
  65       * @var string The path to the log file
  66       */
  67      protected static $logpath = null;
  68  
  69      /**
  70       * @var bool Whether the task logger has been registered with the shutdown handler
  71       */
  72      protected static $tasklogregistered = false;
  73  
  74      /**
  75       * @var int The level of output buffering in place before starting.
  76       */
  77      protected static $oblevel = null;
  78  
  79      /**
  80       * @var bool Output logged content to screen.
  81       */
  82      protected static $outputloggedcontent = true;
  83  
  84      /**
  85       * Create a new task logger for the specified task, and prepare for logging.
  86       *
  87       * @param   \core\task\task_base    $task The task being run
  88       */
  89      public static function start_logging(task_base $task) {
  90          global $CFG, $DB;
  91  
  92          if (!self::should_log()) {
  93              return;
  94          }
  95  
  96          // We register a shutdown handler to ensure that logs causing any failures are correctly disposed of.
  97          // Note: This must happen before the per-request directory is requested because the shutdown handler deletes the logfile.
  98          if (!self::$tasklogregistered) {
  99              \core_shutdown_manager::register_function(function() {
 100                  // These will only actually do anything if capturing is current active when the thread ended, which
 101                  // constitutes a failure.
 102                  \core\task\logmanager::finalise_log(true);
 103              });
 104  
 105              // Create a brand new per-request directory basedir.
 106              get_request_storage_directory(true, true);
 107  
 108              self::$tasklogregistered = true;
 109          }
 110  
 111          if (self::is_current_output_buffer()) {
 112              // We cannot capture when we are already capturing.
 113              throw new \coding_exception('Logging is already in progress for task "' . get_class(self::$task) . '". ' .
 114                  'Nested logging is not supported.');
 115          }
 116  
 117          // Store the initial data about the task and current state.
 118          self::$task = $task;
 119          self::$taskloginfo = (object) [
 120              'dbread'    => $DB->perf_get_reads(),
 121              'dbwrite'   => $DB->perf_get_writes(),
 122              'timestart' => microtime(true),
 123          ];
 124  
 125          // For simplicity's sake we always store logs on disk and flush at the end.
 126          self::$logpath = make_request_directory() . DIRECTORY_SEPARATOR . "task.log";
 127          self::$fh = fopen(self::$logpath, 'w+');
 128  
 129          // Note the level of the current output buffer.
 130          // Note: You cannot use ob_get_level() as it will return `1` when the default output buffer is enabled.
 131          if ($obstatus = ob_get_status()) {
 132              self::$oblevel = $obstatus['level'];
 133          } else {
 134              self::$oblevel = null;
 135          }
 136  
 137          self::$outputloggedcontent = !empty($CFG->task_logtostdout);
 138  
 139          // Start capturing output.
 140          ob_start([\core\task\logmanager::class, 'add_line'], self::CHUNKSIZE);
 141      }
 142  
 143      /**
 144       * Whether logging is possible and should be happening.
 145       *
 146       * @return  bool
 147       */
 148      protected static function should_log() : bool {
 149          global $CFG;
 150  
 151          // Respect the config setting.
 152          if (isset($CFG->task_logmode) && empty($CFG->task_logmode)) {
 153              return false;
 154          }
 155  
 156          $loggerclass = self::get_logger_classname();
 157          if (empty($loggerclass)) {
 158              return false;
 159          }
 160  
 161          return $loggerclass::is_configured();
 162      }
 163  
 164      /**
 165       * Return the name of the logging class to use.
 166       *
 167       * @return  string
 168       */
 169      public static function get_logger_classname() : string {
 170          global $CFG;
 171  
 172          if (!empty($CFG->task_log_class)) {
 173              // Configuration is present to use an alternative task logging class.
 174              return $CFG->task_log_class;
 175          }
 176  
 177          // Fall back on the default database logger.
 178          return database_logger::class;
 179      }
 180  
 181      /**
 182       * Whether this task logger has a report available.
 183       *
 184       * @return  bool
 185       */
 186      public static function has_log_report() : bool {
 187          $loggerclass = self::get_logger_classname();
 188  
 189          return $loggerclass::has_log_report();
 190      }
 191  
 192      /**
 193       * Whether to use the standard settings form.
 194       */
 195      public static function uses_standard_settings() : bool {
 196          $classname = self::get_logger_classname();
 197          if (!class_exists($classname)) {
 198              return false;
 199          }
 200  
 201          if (is_a($classname, database_logger::class, true)) {
 202              return true;
 203          }
 204  
 205          return false;
 206      }
 207  
 208      /**
 209       * Get any URL available for viewing relevant task log reports.
 210       *
 211       * @param   string      $classname The task class to fetch for
 212       * @return  \moodle_url
 213       */
 214      public static function get_url_for_task_class(string $classname) : \moodle_url {
 215          $loggerclass = self::get_logger_classname();
 216  
 217          return $loggerclass::get_url_for_task_class($classname);
 218      }
 219  
 220      /**
 221       * Whether we are the current log collector.
 222       *
 223       * @return  bool
 224       */
 225      protected static function is_current_output_buffer() : bool {
 226          if (empty(self::$taskloginfo)) {
 227              return false;
 228          }
 229  
 230          if ($ob = ob_get_status()) {
 231              return 'core\\task\\logmanager::add_line' == $ob['name'];
 232          }
 233  
 234          return false;
 235      }
 236  
 237      /**
 238       * Whether we are capturing at all.
 239       *
 240       * @return  bool
 241       */
 242      protected static function is_capturing() : bool {
 243          $buffers = ob_get_status(true);
 244          foreach ($buffers as $ob) {
 245              if ('core\\task\\logmanager::add_line' == $ob['name']) {
 246                  return true;
 247              }
 248          }
 249  
 250          return false;
 251      }
 252  
 253      /**
 254       * Finish writing for the current task.
 255       *
 256       * @param   bool    $failed
 257       */
 258      public static function finalise_log(bool $failed = false) {
 259          global $CFG, $DB, $PERF;
 260  
 261          if (!self::should_log()) {
 262              return;
 263          }
 264  
 265          if (!self::is_capturing()) {
 266              // Not capturing anything.
 267              return;
 268          }
 269  
 270          // Ensure that all logs are closed.
 271          $buffers = ob_get_status(true);
 272          foreach (array_reverse($buffers) as $ob) {
 273              if (null !== self::$oblevel) {
 274                  if ($ob['level'] <= self::$oblevel) {
 275                      // Only close as far as the initial output buffer level.
 276                      break;
 277                  }
 278              }
 279  
 280              // End and flush this buffer.
 281              ob_end_flush();
 282  
 283              if ('core\\task\\logmanager::add_line' == $ob['name']) {
 284                  break;
 285              }
 286          }
 287          self::$oblevel = null;
 288  
 289          // Flush any remaining buffer.
 290          self::flush();
 291  
 292          // Close and unset the FH.
 293          fclose(self::$fh);
 294          self::$fh = null;
 295  
 296          if ($failed || empty($CFG->task_logmode) || self::MODE_ALL == $CFG->task_logmode) {
 297              // Finalise the log.
 298              $loggerclass = self::get_logger_classname();
 299              $loggerclass::store_log_for_task(
 300                  self::$task,
 301                  self::$logpath,
 302                  $failed,
 303                  $DB->perf_get_reads() - self::$taskloginfo->dbread,
 304                  $DB->perf_get_writes() - self::$taskloginfo->dbwrite - $PERF->logwrites,
 305                  self::$taskloginfo->timestart,
 306                  microtime(true)
 307              );
 308          }
 309  
 310          // Tidy up.
 311          self::$logpath = null;
 312          self::$taskloginfo = null;
 313      }
 314  
 315      /**
 316       * Flush the current output buffer.
 317       *
 318       * This function will ensure that we are the current output buffer handler.
 319       */
 320      public static function flush() {
 321          // We only call ob_flush if the current output buffer belongs to us.
 322          if (self::is_current_output_buffer()) {
 323              ob_flush();
 324          }
 325      }
 326  
 327      /**
 328       * Add a log record to the task log.
 329       *
 330       * @param   string  $log
 331       * @return  string
 332       */
 333      public static function add_line(string $log) : string {
 334          if (empty(self::$taskloginfo)) {
 335              return $log;
 336          }
 337  
 338          if (empty(self::$fh)) {
 339              return $log;
 340          }
 341  
 342          if (self::is_current_output_buffer()) {
 343              fwrite(self::$fh, $log);
 344          }
 345  
 346          if (self::$outputloggedcontent) {
 347              return $log;
 348          } else {
 349              return '';
 350          }
 351      }
 352  }