MDL-75191 task: implement task logs datasource for custom reporting.
[moodle.git] / admin / classes / local / entities / task_log.php
blobe454eece5abf039bfbf87d2c9a309a0ec837d349
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.
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 namespace core_admin\local\entities;
19 use core_reportbuilder\local\filters\date;
20 use core_reportbuilder\local\filters\duration;
21 use core_reportbuilder\local\filters\number;
22 use core_reportbuilder\local\filters\select;
23 use core_reportbuilder\local\filters\text;
24 use core_reportbuilder\local\filters\autocomplete;
25 use core_reportbuilder\local\helpers\format;
26 use lang_string;
27 use core_reportbuilder\local\entities\base;
28 use core_reportbuilder\local\report\column;
29 use core_reportbuilder\local\report\filter;
30 use stdClass;
31 use core_collator;
33 /**
34 * Task log entity class implementation
36 * @package core_admin
37 * @copyright 2021 David Matamoros <davidmc@moodle.com>
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class task_log extends base {
42 /** @var int Result success */
43 protected const SUCCESS = 0;
45 /** @var int Result failed */
46 protected const FAILED = 1;
48 /**
49 * Database tables that this entity uses and their default aliases
51 * @return array
53 protected function get_default_table_aliases(): array {
54 return ['task_log' => 'tl'];
57 /**
58 * The default title for this entity in the list of columns/conditions/filters in the report builder
60 * @return lang_string
62 protected function get_default_entity_title(): lang_string {
63 return new lang_string('entitytasklog', 'admin');
66 /**
67 * Initialise the entity
69 * @return base
71 public function initialise(): base {
72 $columns = $this->get_all_columns();
73 foreach ($columns as $column) {
74 $this->add_column($column);
77 // All the filters defined by the entity can also be used as conditions.
78 $filters = $this->get_all_filters();
79 foreach ($filters as $filter) {
80 $this
81 ->add_filter($filter)
82 ->add_condition($filter);
85 return $this;
88 /**
89 * Returns list of all available columns
91 * @return column[]
93 protected function get_all_columns(): array {
94 global $DB;
96 $tablealias = $this->get_table_alias('task_log');
98 // Name column.
99 $columns[] = (new column(
100 'name',
101 new lang_string('name'),
102 $this->get_entity_name()
104 ->add_joins($this->get_joins())
105 ->set_type(column::TYPE_TEXT)
106 ->add_field("$tablealias.classname")
107 ->set_is_sortable(true)
108 ->add_callback(static function(string $classname): string {
109 $output = '';
110 if (class_exists($classname)) {
111 $task = new $classname;
112 if ($task instanceof \core\task\task_base) {
113 $output = $task->get_name();
116 $output .= \html_writer::tag('div', "\\{$classname}", [
117 'class' => 'small text-muted',
119 return $output;
122 // Component column.
123 $columns[] = (new column(
124 'component',
125 new lang_string('plugin'),
126 $this->get_entity_name()
128 ->add_joins($this->get_joins())
129 ->set_type(column::TYPE_TEXT)
130 ->add_field("{$tablealias}.component")
131 ->set_is_sortable(true);
133 // Type column.
134 $columns[] = (new column(
135 'type',
136 new lang_string('tasktype', 'admin'),
137 $this->get_entity_name()
139 ->add_joins($this->get_joins())
140 ->set_type(column::TYPE_TEXT)
141 ->add_field("{$tablealias}.type")
142 ->set_is_sortable(true)
143 ->add_callback(static function($value): string {
144 if (\core\task\database_logger::TYPE_SCHEDULED === (int) $value) {
145 return get_string('task_type:scheduled', 'admin');
147 return get_string('task_type:adhoc', 'admin');
150 // Start time column.
151 $columns[] = (new column(
152 'starttime',
153 new lang_string('task_starttime', 'admin'),
154 $this->get_entity_name()
156 ->add_joins($this->get_joins())
157 ->set_type(column::TYPE_TIMESTAMP)
158 ->add_field("{$tablealias}.timestart")
159 ->set_is_sortable(true)
160 ->add_callback([format::class, 'userdate'], get_string('strftimedatetimeshortaccurate', 'core_langconfig'));
162 // End time column.
163 $columns[] = (new column(
164 'endtime',
165 new lang_string('task_endtime', 'admin'),
166 $this->get_entity_name()
168 ->add_joins($this->get_joins())
169 ->set_type(column::TYPE_TIMESTAMP)
170 ->add_field("{$tablealias}.timeend")
171 ->set_is_sortable(true)
172 ->add_callback([format::class, 'userdate'], get_string('strftimedatetimeshortaccurate', 'core_langconfig'));
174 // Duration column.
175 $columns[] = (new column(
176 'duration',
177 new lang_string('task_duration', 'admin'),
178 $this->get_entity_name()
180 ->add_joins($this->get_joins())
181 ->set_type(column::TYPE_FLOAT)
182 ->add_field("{$tablealias}.timeend - {$tablealias}.timestart", 'duration')
183 ->set_is_sortable(true)
184 ->add_callback(static function(float $value): string {
185 $duration = round($value, 2);
186 if (empty($duration)) {
187 // The format_time function returns 'now' when the difference is exactly 0.
188 // Note: format_time performs concatenation in exactly this fashion so we should do this for consistency.
189 return '0 ' . get_string('secs', 'moodle');
191 return format_time($duration);
194 // Hostname column.
195 $columns[] = (new column(
196 'hostname',
197 new lang_string('hostname', 'admin'),
198 $this->get_entity_name()
200 ->add_joins($this->get_joins())
201 ->set_type(column::TYPE_TEXT)
202 ->add_field("$tablealias.hostname")
203 ->set_is_sortable(true);
205 // PID column.
206 $columns[] = (new column(
207 'pid',
208 new lang_string('pid', 'admin'),
209 $this->get_entity_name()
211 ->add_joins($this->get_joins())
212 ->set_type(column::TYPE_INTEGER)
213 ->add_field("{$tablealias}.pid")
214 ->set_is_sortable(true)
215 // Although this is an integer column, it doesn't make sense to perform numeric aggregation on it.
216 ->set_disabled_aggregation(['avg', 'count', 'countdistinct', 'max', 'min', 'sum']);
218 // Database column.
219 $columns[] = (new column(
220 'database',
221 new lang_string('task_dbstats', 'admin'),
222 $this->get_entity_name()
224 ->add_joins($this->get_joins())
225 ->set_type(column::TYPE_INTEGER)
226 ->add_fields("{$tablealias}.dbreads, {$tablealias}.dbwrites")
227 ->set_is_sortable(true, ["{$tablealias}.dbreads", "{$tablealias}.dbwrites"])
228 ->add_callback(static function(int $value, stdClass $row): string {
229 $output = '';
230 $output .= \html_writer::div(get_string('task_stats:dbreads', 'admin', $row->dbreads));
231 $output .= \html_writer::div(get_string('task_stats:dbwrites', 'admin', $row->dbwrites));
232 return $output;
234 // Although this is an integer column, it doesn't make sense to perform numeric aggregation on it.
235 ->set_disabled_aggregation(['avg', 'count', 'countdistinct', 'max', 'min', 'sum']);
237 // Database reads column.
238 $columns[] = (new column(
239 'dbreads',
240 new lang_string('task_dbreads', 'admin'),
241 $this->get_entity_name()
243 ->add_joins($this->get_joins())
244 ->set_type(column::TYPE_INTEGER)
245 ->add_fields("{$tablealias}.dbreads")
246 ->set_is_sortable(true);
248 // Database writes column.
249 $columns[] = (new column(
250 'dbwrites',
251 new lang_string('task_dbwrites', 'admin'),
252 $this->get_entity_name()
254 ->add_joins($this->get_joins())
255 ->set_type(column::TYPE_INTEGER)
256 ->add_fields("{$tablealias}.dbwrites")
257 ->set_is_sortable(true);
259 // Result column.
260 $columns[] = (new column(
261 'result',
262 new lang_string('task_result', 'admin'),
263 $this->get_entity_name()
265 ->add_joins($this->get_joins())
266 ->set_type(column::TYPE_BOOLEAN)
267 // For accurate aggregation, we need to return boolean success = true by xor'ing the field value.
268 ->add_field($DB->sql_bitxor("{$tablealias}.result", 1), 'success')
269 ->set_is_sortable(true)
270 ->add_callback(static function(bool $success): string {
271 if (!$success) {
272 return get_string('task_result:failed', 'admin');
274 return get_string('success');
277 return $columns;
281 * Return list of all available filters
283 * @return filter[]
285 protected function get_all_filters(): array {
286 $filters = [];
288 $tablealias = $this->get_table_alias('task_log');
290 // Name filter (Filter by classname).
291 $filters[] = (new filter(
292 autocomplete::class,
293 'name',
294 new lang_string('classname', 'tool_task'),
295 $this->get_entity_name(),
296 "{$tablealias}.classname"
298 ->add_joins($this->get_joins())
299 ->set_options_callback(static function(): array {
300 global $DB;
301 $classnames = $DB->get_fieldset_sql('SELECT DISTINCT classname FROM {task_log} ORDER BY classname ASC');
303 $options = [];
304 foreach ($classnames as $classname) {
305 if (class_exists($classname)) {
306 $task = new $classname;
307 $options[$classname] = $task->get_name();
311 core_collator::asort($options);
312 return $options;
315 // Component filter.
316 $filters[] = (new filter(
317 text::class,
318 'component',
319 new lang_string('plugin'),
320 $this->get_entity_name(),
321 "{$tablealias}.component"
323 ->add_joins($this->get_joins());
325 // Type filter.
326 $filters[] = (new filter(
327 select::class,
328 'type',
329 new lang_string('tasktype', 'admin'),
330 $this->get_entity_name(),
331 "{$tablealias}.type"
333 ->add_joins($this->get_joins())
334 ->set_options([
335 \core\task\database_logger::TYPE_ADHOC => new lang_string('task_type:adhoc', 'admin'),
336 \core\task\database_logger::TYPE_SCHEDULED => new lang_string('task_type:scheduled', 'admin'),
339 // Output filter (Filter by task output).
340 $filters[] = (new filter(
341 text::class,
342 'output',
343 new lang_string('task_logoutput', 'admin'),
344 $this->get_entity_name(),
345 "{$tablealias}.output"
347 ->add_joins($this->get_joins());
349 // Start time filter.
350 $filters[] = (new filter(
351 date::class,
352 'timestart',
353 new lang_string('task_starttime', 'admin'),
354 $this->get_entity_name(),
355 "{$tablealias}.timestart"
357 ->add_joins($this->get_joins())
358 ->set_limited_operators([
359 date::DATE_ANY,
360 date::DATE_RANGE,
361 date::DATE_PREVIOUS,
362 date::DATE_CURRENT,
365 // End time.
366 $filters[] = (new filter(
367 date::class,
368 'timeend',
369 new lang_string('task_endtime', 'admin'),
370 $this->get_entity_name(),
371 "{$tablealias}.timeend"
373 ->add_joins($this->get_joins())
374 ->set_limited_operators([
375 date::DATE_ANY,
376 date::DATE_RANGE,
377 date::DATE_PREVIOUS,
378 date::DATE_CURRENT,
381 // Duration filter.
382 $filters[] = (new filter(
383 duration::class,
384 'duration',
385 new lang_string('task_duration', 'admin'),
386 $this->get_entity_name(),
387 "{$tablealias}.timeend - {$tablealias}.timestart"
389 ->add_joins($this->get_joins());
391 // Database reads.
392 $filters[] = (new filter(
393 number::class,
394 'dbreads',
395 new lang_string('task_dbreads', 'admin'),
396 $this->get_entity_name(),
397 "{$tablealias}.dbreads"
399 ->add_joins($this->get_joins());
401 // Database writes.
402 $filters[] = (new filter(
403 number::class,
404 'dbwrites',
405 new lang_string('task_dbwrites', 'admin'),
406 $this->get_entity_name(),
407 "{$tablealias}.dbwrites"
409 ->add_joins($this->get_joins());
411 // Result filter.
412 $filters[] = (new filter(
413 select::class,
414 'result',
415 new lang_string('task_result', 'admin'),
416 $this->get_entity_name(),
417 "{$tablealias}.result"
419 ->add_joins($this->get_joins())
420 ->set_options([
421 self::SUCCESS => get_string('success'),
422 self::FAILED => get_string('task_result:failed', 'admin'),
425 return $filters;