6e2e6820abd4b880036d3f2534609401308a95c3
[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         return !empty(self::get_logger_classname());
148     }
150     /**
151      * Return the name of the logging class to use.
152      *
153      * @return  string
154      */
155     public static function get_logger_classname() : string {
156         global $CFG;
158         if (!empty($CFG->task_log_class)) {
159             // Configuration is present to use an alternative task logging class.
160             return $CFG->task_log_class;
161         }
163         // Fall back on the default database logger.
164         return database_logger::class;
165     }
167     /**
168      * Whether this task logger has a report available.
169      *
170      * @return  bool
171      */
172     public static function has_log_report() : bool {
173         $loggerclass = self::get_logger_classname();
175         return $loggerclass::has_log_report();
176     }
178     /**
179      * Whether to use the standard settings fore
180      */
181     public static function uses_standard_settings() : bool {
182         $classname = self::get_logger_classname();
183         if (!class_exists($classname)) {
184             return false;
185         }
187         if (is_a($classname, database_logger::class, true)) {
188             return true;
189         }
191         return false;
192     }
194     /**
195      * Get any URL available for viewing relevant task log reports.
196      *
197      * @param   string      $classname The task class to fetch for
198      * @return  \moodle_url
199      */
200     public static function get_url_for_task_class(string $classname) : \moodle_url {
201         $loggerclass = self::get_logger_classname();
203         return $loggerclass::get_url_for_task_class($classname);
204     }
206     /**
207      * Whether we are the current log collector.
208      *
209      * @return  bool
210      */
211     protected static function is_current_output_buffer() : bool {
212         if (empty(self::$taskloginfo)) {
213             return false;
214         }
216         if ($ob = ob_get_status()) {
217             return 'core\\task\\logmanager::add_line' == $ob['name'];
218         }
220         return false;
221     }
223     /**
224      * Whether we are capturing at all.
225      *
226      * @return  bool
227      */
228     protected static function is_capturing() : bool {
229         $buffers = ob_get_status(true);
230         foreach ($buffers as $ob) {
231             if ('core\\task\\logmanager::add_line' == $ob['name']) {
232                 return true;
233             }
234         }
236         return false;
237     }
239     /**
240      * Finish writing for the current task.
241      *
242      * @param   bool    $failed
243      */
244     public static function finalise_log(bool $failed = false) {
245         global $CFG, $DB, $PERF;
247         if (!self::should_log()) {
248             return;
249         }
251         if (!self::is_capturing()) {
252             // Not capturing anything.
253             return;
254         }
256         // Ensure that all logs are closed.
257         $buffers = ob_get_status(true);
258         foreach (array_reverse($buffers) as $ob) {
259             if (null !== self::$oblevel) {
260                 if ($ob['level'] <= self::$oblevel) {
261                     // Only close as far as the initial output buffer level.
262                     break;
263                 }
264             }
266             // End and flush this buffer.
267             ob_end_flush();
269             if ('core\\task\\logmanager::add_line' == $ob['name']) {
270                 break;
271             }
272         }
273         self::$oblevel = null;
275         // Flush any remaining buffer.
276         self::flush();
278         // Close and unset the FH.
279         fclose(self::$fh);
280         self::$fh = null;
282         if ($failed || empty($CFG->task_logmode) || self::MODE_ALL == $CFG->task_logmode) {
283             // Finalise the log.
284             $loggerclass = self::get_logger_classname();
285             $loggerclass::store_log_for_task(
286                 self::$task,
287                 self::$logpath,
288                 $failed,
289                 $DB->perf_get_reads() - self::$taskloginfo->dbread,
290                 $DB->perf_get_writes() - self::$taskloginfo->dbwrite - $PERF->logwrites,
291                 self::$taskloginfo->timestart,
292                 microtime(true)
293             );
294         }
296         // Tidy up.
297         self::$logpath = null;
298         self::$taskloginfo = null;
299     }
301     /**
302      * Flush the current output buffer.
303      *
304      * This function will ensure that we are the current output buffer handler.
305      */
306     public static function flush() {
307         // We only call ob_flush if the current output buffer belongs to us.
308         if (self::is_current_output_buffer()) {
309             ob_flush();
310         }
311     }
313     /**
314      * Add a log record to the task log.
315      *
316      * @param   string  $log
317      * @return  string
318      */
319     public static function add_line(string $log) : string {
320         if (empty(self::$taskloginfo)) {
321             return $log;
322         }
324         if (empty(self::$fh)) {
325             return $log;
326         }
328         if (self::is_current_output_buffer()) {
329             fwrite(self::$fh, $log);
330         }
332         return $log;
333     }