MDL-49399 task: Add admin log viewer
[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.
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 });
100
101 self::$tasklogregistered = true;
102 }
103
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 }
109
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 ];
117
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+');
121
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 }
129
130 // Start capturing output.
131 ob_start([\core\task\logmanager::class, 'add_line'], self::CHUNKSIZE);
132 }
133
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;
141
142 // Respect the config setting.
143 if (isset($CFG->task_logmode) && empty($CFG->task_logmode)) {
144 return false;
145 }
146
8c69e86c
AN
147 $loggerclass = self::get_logger_classname();
148 if (empty($loggerclass)) {
149 return false;
150 }
151
152 return $loggerclass::is_configured();
4b71596f
AN
153 }
154
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;
162
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 }
167
168 // Fall back on the default database logger.
169 return database_logger::class;
170 }
171
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();
179
180 return $loggerclass::has_log_report();
181 }
182
183 /**
8c69e86c 184 * Whether to use the standard settings form.
4b71596f
AN
185 */
186 public static function uses_standard_settings() : bool {
187 $classname = self::get_logger_classname();
188 if (!class_exists($classname)) {
189 return false;
190 }
191
192 if (is_a($classname, database_logger::class, true)) {
193 return true;
194 }
195
196 return false;
197 }
198
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();
207
208 return $loggerclass::get_url_for_task_class($classname);
209 }
210
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 }
220
221 if ($ob = ob_get_status()) {
222 return 'core\\task\\logmanager::add_line' == $ob['name'];
223 }
224
225 return false;
226 }
227
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 }
240
241 return false;
242 }
243
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;
251
252 if (!self::should_log()) {
253 return;
254 }
255
256 if (!self::is_capturing()) {
257 // Not capturing anything.
258 return;
259 }
260
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 }
270
271 // End and flush this buffer.
272 ob_end_flush();
273
274 if ('core\\task\\logmanager::add_line' == $ob['name']) {
275 break;
276 }
277 }
278 self::$oblevel = null;
279
280 // Flush any remaining buffer.
281 self::flush();
282
283 // Close and unset the FH.
284 fclose(self::$fh);
285 self::$fh = null;
286
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 }
300
301 // Tidy up.
302 self::$logpath = null;
303 self::$taskloginfo = null;
304 }
305
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 }
317
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 }
328
329 if (empty(self::$fh)) {
330 return $log;
331 }
332
333 if (self::is_current_output_buffer()) {
334 fwrite(self::$fh, $log);
335 }
336
337 return $log;
338 }
339}