MDL-49399 task: Add admin log viewer
[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 may delete
93         // the logfile.
94         if (!self::$tasklogregistered) {
95             \core_shutdown_manager::register_function(function() {
96                 // These will only actually do anything if capturing is current active when the thread ended, which
97                 // constitutes a failure.
98                 \core\task\logmanager::finalise_log(true);
99             });
101             self::$tasklogregistered = true;
102         }
104         if (self::is_current_output_buffer()) {
105             // We cannot capture when we are already capturing.
106             throw new \coding_exception('Logging is already in progress for task "' . get_class(self::$task) . '". ' .
107                 'Nested logging is not supported.');
108         }
110         // Store the initial data about the task and current state.
111         self::$task = $task;
112         self::$taskloginfo = (object) [
113             'dbread'    => $DB->perf_get_reads(),
114             'dbwrite'   => $DB->perf_get_writes(),
115             'timestart' => microtime(true),
116         ];
118         // For simplicity's sake we always store logs on disk and flush at the end.
119         self::$logpath = make_request_directory() . DIRECTORY_SEPARATOR . "task.log";
120         self::$fh = fopen(self::$logpath, 'w+');
122         // Note the level of the current output buffer.
123         // Note: You cannot use ob_get_level() as it will return `1` when the default output buffer is enabled.
124         if ($obstatus = ob_get_status()) {
125             self::$oblevel = $obstatus['level'];
126         } else {
127             self::$oblevel = null;
128         }
130         // Start capturing output.
131         ob_start([\core\task\logmanager::class, 'add_line'], self::CHUNKSIZE);
132     }
134     /**
135      * Whether logging is possible and should be happening.
136      *
137      * @return  bool
138      */
139     protected static function should_log() : bool {
140         global $CFG;
142         // Respect the config setting.
143         if (isset($CFG->task_logmode) && empty($CFG->task_logmode)) {
144             return false;
145         }
147         $loggerclass = self::get_logger_classname();
148         if (empty($loggerclass)) {
149             return false;
150         }
152         return $loggerclass::is_configured();
153     }
155     /**
156      * Return the name of the logging class to use.
157      *
158      * @return  string
159      */
160     public static function get_logger_classname() : string {
161         global $CFG;
163         if (!empty($CFG->task_log_class)) {
164             // Configuration is present to use an alternative task logging class.
165             return $CFG->task_log_class;
166         }
168         // Fall back on the default database logger.
169         return database_logger::class;
170     }
172     /**
173      * Whether this task logger has a report available.
174      *
175      * @return  bool
176      */
177     public static function has_log_report() : bool {
178         $loggerclass = self::get_logger_classname();
180         return $loggerclass::has_log_report();
181     }
183     /**
184      * Whether to use the standard settings form.
185      */
186     public static function uses_standard_settings() : bool {
187         $classname = self::get_logger_classname();
188         if (!class_exists($classname)) {
189             return false;
190         }
192         if (is_a($classname, database_logger::class, true)) {
193             return true;
194         }
196         return false;
197     }
199     /**
200      * Get any URL available for viewing relevant task log reports.
201      *
202      * @param   string      $classname The task class to fetch for
203      * @return  \moodle_url
204      */
205     public static function get_url_for_task_class(string $classname) : \moodle_url {
206         $loggerclass = self::get_logger_classname();
208         return $loggerclass::get_url_for_task_class($classname);
209     }
211     /**
212      * Whether we are the current log collector.
213      *
214      * @return  bool
215      */
216     protected static function is_current_output_buffer() : bool {
217         if (empty(self::$taskloginfo)) {
218             return false;
219         }
221         if ($ob = ob_get_status()) {
222             return 'core\\task\\logmanager::add_line' == $ob['name'];
223         }
225         return false;
226     }
228     /**
229      * Whether we are capturing at all.
230      *
231      * @return  bool
232      */
233     protected static function is_capturing() : bool {
234         $buffers = ob_get_status(true);
235         foreach ($buffers as $ob) {
236             if ('core\\task\\logmanager::add_line' == $ob['name']) {
237                 return true;
238             }
239         }
241         return false;
242     }
244     /**
245      * Finish writing for the current task.
246      *
247      * @param   bool    $failed
248      */
249     public static function finalise_log(bool $failed = false) {
250         global $CFG, $DB, $PERF;
252         if (!self::should_log()) {
253             return;
254         }
256         if (!self::is_capturing()) {
257             // Not capturing anything.
258             return;
259         }
261         // Ensure that all logs are closed.
262         $buffers = ob_get_status(true);
263         foreach (array_reverse($buffers) as $ob) {
264             if (null !== self::$oblevel) {
265                 if ($ob['level'] <= self::$oblevel) {
266                     // Only close as far as the initial output buffer level.
267                     break;
268                 }
269             }
271             // End and flush this buffer.
272             ob_end_flush();
274             if ('core\\task\\logmanager::add_line' == $ob['name']) {
275                 break;
276             }
277         }
278         self::$oblevel = null;
280         // Flush any remaining buffer.
281         self::flush();
283         // Close and unset the FH.
284         fclose(self::$fh);
285         self::$fh = null;
287         if ($failed || empty($CFG->task_logmode) || self::MODE_ALL == $CFG->task_logmode) {
288             // Finalise the log.
289             $loggerclass = self::get_logger_classname();
290             $loggerclass::store_log_for_task(
291                 self::$task,
292                 self::$logpath,
293                 $failed,
294                 $DB->perf_get_reads() - self::$taskloginfo->dbread,
295                 $DB->perf_get_writes() - self::$taskloginfo->dbwrite - $PERF->logwrites,
296                 self::$taskloginfo->timestart,
297                 microtime(true)
298             );
299         }
301         // Tidy up.
302         self::$logpath = null;
303         self::$taskloginfo = null;
304     }
306     /**
307      * Flush the current output buffer.
308      *
309      * This function will ensure that we are the current output buffer handler.
310      */
311     public static function flush() {
312         // We only call ob_flush if the current output buffer belongs to us.
313         if (self::is_current_output_buffer()) {
314             ob_flush();
315         }
316     }
318     /**
319      * Add a log record to the task log.
320      *
321      * @param   string  $log
322      * @return  string
323      */
324     public static function add_line(string $log) : string {
325         if (empty(self::$taskloginfo)) {
326             return $log;
327         }
329         if (empty(self::$fh)) {
330             return $log;
331         }
333         if (self::is_current_output_buffer()) {
334             fwrite(self::$fh, $log);
335         }
337         return $log;
338     }