MDL-49399 core: Allow creation of a new per-request basedir
[moodle.git] / lib / classes / task / logmanager.php
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/>.
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;
27 defined('MOODLE_INTERNAL') || die();
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 {
37     /** @var int Do not log anything */
38     const MODE_NONE = 0;
40     /** @var int Log all tasks */
41     const MODE_ALL = 1;
43     /** @var int Only log fails */
44     const MODE_FAILONLY = 2;
46     /** @var int The default chunksize to use in ob_start */
47     const CHUNKSIZE = 1;
49     /**
50      * @var \core\task\task_base The task being logged.
51      */
52     protected static $task = null;
54     /**
55      * @var \stdClass Metadata about the current log
56      */
57     protected static $taskloginfo = null;
59     /**
60      * @var \resource The current filehandle used for logging
61      */
62     protected static $fh = null;
64     /**
65      * @var string The path to the log file
66      */
67     protected static $logpath = null;
69     /**
70      * @var bool Whether the task logger has been registered with the shutdown handler
71      */
72     protected static $tasklogregistered = false;
74     /**
75      * @var int The level of output buffering in place before starting.
76      */
77     protected static $oblevel = null;
79     /**
80      * Create a new task logger for the specified task, and prepare for logging.
81      *
82      * @param   \core\task\task_base    $task The task being run
83      */
84     public static function start_logging(task_base $task) {
85         global $DB;
87         if (!self::should_log()) {
88             return;
89         }
91         // We register a shutdown handler to ensure that logs causing any failures are correctly disposed of.
92         // Note: This must happen before the per-request directory is requested because the shutdown handler deletes the logfile.
93         if (!self::$tasklogregistered) {
94             \core_shutdown_manager::register_function(function() {
95                 // These will only actually do anything if capturing is current active when the thread ended, which
96                 // constitutes a failure.
97                 \core\task\logmanager::finalise_log(true);
98             });
100             // Create a brand new per-request directory basedir.
101             get_request_storage_directory(true, true);
103             self::$tasklogregistered = true;
104         }
106         if (self::is_current_output_buffer()) {
107             // We cannot capture when we are already capturing.
108             throw new \coding_exception('Logging is already in progress for task "' . get_class(self::$task) . '". ' .
109                 'Nested logging is not supported.');
110         }
112         // Store the initial data about the task and current state.
113         self::$task = $task;
114         self::$taskloginfo = (object) [
115             'dbread'    => $DB->perf_get_reads(),
116             'dbwrite'   => $DB->perf_get_writes(),
117             'timestart' => microtime(true),
118         ];
120         // For simplicity's sake we always store logs on disk and flush at the end.
121         self::$logpath = make_request_directory() . DIRECTORY_SEPARATOR . "task.log";
122         self::$fh = fopen(self::$logpath, 'w+');
124         // Note the level of the current output buffer.
125         // Note: You cannot use ob_get_level() as it will return `1` when the default output buffer is enabled.
126         if ($obstatus = ob_get_status()) {
127             self::$oblevel = $obstatus['level'];
128         } else {
129             self::$oblevel = null;
130         }
132         // Start capturing output.
133         ob_start([\core\task\logmanager::class, 'add_line'], self::CHUNKSIZE);
134     }
136     /**
137      * Whether logging is possible and should be happening.
138      *
139      * @return  bool
140      */
141     protected static function should_log() : bool {
142         global $CFG;
144         // Respect the config setting.
145         if (isset($CFG->task_logmode) && empty($CFG->task_logmode)) {
146             return false;
147         }
149         $loggerclass = self::get_logger_classname();
150         if (empty($loggerclass)) {
151             return false;
152         }
154         return $loggerclass::is_configured();
155     }
157     /**
158      * Return the name of the logging class to use.
159      *
160      * @return  string
161      */
162     public static function get_logger_classname() : string {
163         global $CFG;
165         if (!empty($CFG->task_log_class)) {
166             // Configuration is present to use an alternative task logging class.
167             return $CFG->task_log_class;
168         }
170         // Fall back on the default database logger.
171         return database_logger::class;
172     }
174     /**
175      * Whether this task logger has a report available.
176      *
177      * @return  bool
178      */
179     public static function has_log_report() : bool {
180         $loggerclass = self::get_logger_classname();
182         return $loggerclass::has_log_report();
183     }
185     /**
186      * Whether to use the standard settings form.
187      */
188     public static function uses_standard_settings() : bool {
189         $classname = self::get_logger_classname();
190         if (!class_exists($classname)) {
191             return false;
192         }
194         if (is_a($classname, database_logger::class, true)) {
195             return true;
196         }
198         return false;
199     }
201     /**
202      * Get any URL available for viewing relevant task log reports.
203      *
204      * @param   string      $classname The task class to fetch for
205      * @return  \moodle_url
206      */
207     public static function get_url_for_task_class(string $classname) : \moodle_url {
208         $loggerclass = self::get_logger_classname();
210         return $loggerclass::get_url_for_task_class($classname);
211     }
213     /**
214      * Whether we are the current log collector.
215      *
216      * @return  bool
217      */
218     protected static function is_current_output_buffer() : bool {
219         if (empty(self::$taskloginfo)) {
220             return false;
221         }
223         if ($ob = ob_get_status()) {
224             return 'core\\task\\logmanager::add_line' == $ob['name'];
225         }
227         return false;
228     }
230     /**
231      * Whether we are capturing at all.
232      *
233      * @return  bool
234      */
235     protected static function is_capturing() : bool {
236         $buffers = ob_get_status(true);
237         foreach ($buffers as $ob) {
238             if ('core\\task\\logmanager::add_line' == $ob['name']) {
239                 return true;
240             }
241         }
243         return false;
244     }
246     /**
247      * Finish writing for the current task.
248      *
249      * @param   bool    $failed
250      */
251     public static function finalise_log(bool $failed = false) {
252         global $CFG, $DB, $PERF;
254         if (!self::should_log()) {
255             return;
256         }
258         if (!self::is_capturing()) {
259             // Not capturing anything.
260             return;
261         }
263         // Ensure that all logs are closed.
264         $buffers = ob_get_status(true);
265         foreach (array_reverse($buffers) as $ob) {
266             if (null !== self::$oblevel) {
267                 if ($ob['level'] <= self::$oblevel) {
268                     // Only close as far as the initial output buffer level.
269                     break;
270                 }
271             }
273             // End and flush this buffer.
274             ob_end_flush();
276             if ('core\\task\\logmanager::add_line' == $ob['name']) {
277                 break;
278             }
279         }
280         self::$oblevel = null;
282         // Flush any remaining buffer.
283         self::flush();
285         // Close and unset the FH.
286         fclose(self::$fh);
287         self::$fh = null;
289         if ($failed || empty($CFG->task_logmode) || self::MODE_ALL == $CFG->task_logmode) {
290             // Finalise the log.
291             $loggerclass = self::get_logger_classname();
292             $loggerclass::store_log_for_task(
293                 self::$task,
294                 self::$logpath,
295                 $failed,
296                 $DB->perf_get_reads() - self::$taskloginfo->dbread,
297                 $DB->perf_get_writes() - self::$taskloginfo->dbwrite - $PERF->logwrites,
298                 self::$taskloginfo->timestart,
299                 microtime(true)
300             );
301         }
303         // Tidy up.
304         self::$logpath = null;
305         self::$taskloginfo = null;
306     }
308     /**
309      * Flush the current output buffer.
310      *
311      * This function will ensure that we are the current output buffer handler.
312      */
313     public static function flush() {
314         // We only call ob_flush if the current output buffer belongs to us.
315         if (self::is_current_output_buffer()) {
316             ob_flush();
317         }
318     }
320     /**
321      * Add a log record to the task log.
322      *
323      * @param   string  $log
324      * @return  string
325      */
326     public static function add_line(string $log) : string {
327         if (empty(self::$taskloginfo)) {
328             return $log;
329         }
331         if (empty(self::$fh)) {
332             return $log;
333         }
335         if (self::is_current_output_buffer()) {
336             fwrite(self::$fh, $log);
337         }
339         return $log;
340     }