MDL-49399 core: Allow creation of a new per-request basedir
[moodle.git] / lib / classes / task / logmanager.php
CommitLineData
4b71596f
AN
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 */
25namespace core\task;
26
27defined('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 */
35class 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 * 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;
86
87 if (!self::should_log()) {
88 return;
89 }
90
91 // We register a shutdown handler to ensure that logs causing any failures are correctly disposed of.
cca12f68 92 // Note: This must happen before the per-request directory is requested because the shutdown handler deletes the logfile.
4b71596f
AN
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 });
99
cca12f68
AN
100 // Create a brand new per-request directory basedir.
101 get_request_storage_directory(true, true);
102
4b71596f
AN
103 self::$tasklogregistered = true;
104 }
105
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 }
111
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 ];
119
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+');
123
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 }
131
132 // Start capturing output.
133 ob_start([\core\task\logmanager::class, 'add_line'], self::CHUNKSIZE);
134 }
135
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;
143
144 // Respect the config setting.
145 if (isset($CFG->task_logmode) && empty($CFG->task_logmode)) {
146 return false;
147 }
148
8c69e86c
AN
149 $loggerclass = self::get_logger_classname();
150 if (empty($loggerclass)) {
151 return false;
152 }
153
154 return $loggerclass::is_configured();
4b71596f
AN
155 }
156
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;
164
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 }
169
170 // Fall back on the default database logger.
171 return database_logger::class;
172 }
173
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();
181
182 return $loggerclass::has_log_report();
183 }
184
185 /**
8c69e86c 186 * Whether to use the standard settings form.
4b71596f
AN
187 */
188 public static function uses_standard_settings() : bool {
189 $classname = self::get_logger_classname();
190 if (!class_exists($classname)) {
191 return false;
192 }
193
194 if (is_a($classname, database_logger::class, true)) {
195 return true;
196 }
197
198 return false;
199 }
200
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();
209
210 return $loggerclass::get_url_for_task_class($classname);
211 }
212
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 }
222
223 if ($ob = ob_get_status()) {
224 return 'core\\task\\logmanager::add_line' == $ob['name'];
225 }
226
227 return false;
228 }
229
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 }
242
243 return false;
244 }
245
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;
253
254 if (!self::should_log()) {
255 return;
256 }
257
258 if (!self::is_capturing()) {
259 // Not capturing anything.
260 return;
261 }
262
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 }
272
273 // End and flush this buffer.
274 ob_end_flush();
275
276 if ('core\\task\\logmanager::add_line' == $ob['name']) {
277 break;
278 }
279 }
280 self::$oblevel = null;
281
282 // Flush any remaining buffer.
283 self::flush();
284
285 // Close and unset the FH.
286 fclose(self::$fh);
287 self::$fh = null;
288
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 }
302
303 // Tidy up.
304 self::$logpath = null;
305 self::$taskloginfo = null;
306 }
307
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 }
319
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 }
330
331 if (empty(self::$fh)) {
332 return $log;
333 }
334
335 if (self::is_current_output_buffer()) {
336 fwrite(self::$fh, $log);
337 }
338
339 return $log;
340 }
341}