From 8c69e86cd4830d0246c1c0efa31aa9366342b114 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 4 Dec 2018 10:46:25 +0800 Subject: [PATCH] MDL-49399 task: Add admin log viewer AMOS BEGIN CPY [eventstarttime,core_calendar],[task_starttime,core_admin] CPY [eventduration,core_calendar],[task_duration,core_admin] CPY [result,core_cache],[task_result,core_admin] CPY [database,install],[task_dbstats,core_admin] CPY [fail,install],[task_result:failed,core_admin] AMOS END --- admin/classes/task_log_table.php | 280 +++++++++++++++++++++++++++ admin/settings/server.php | 9 +- admin/tasklogs.php | 92 +++++++++ admin/templates/tasklogs.mustache | 34 ++++ lang/en/admin.php | 11 ++ lib/classes/task/database_logger.php | 23 +++ lib/classes/task/logmanager.php | 9 +- lib/classes/task/manager.php | 8 +- lib/classes/task/task_logger.php | 14 ++ lib/tests/task_logging_test.php | 24 +++ theme/boost/scss/moodle/admin.scss | 7 + theme/boost/style/moodle.css | 4 + 12 files changed, 508 insertions(+), 7 deletions(-) create mode 100644 admin/classes/task_log_table.php create mode 100644 admin/tasklogs.php create mode 100644 admin/templates/tasklogs.mustache diff --git a/admin/classes/task_log_table.php b/admin/classes/task_log_table.php new file mode 100644 index 00000000000..554d92db791 --- /dev/null +++ b/admin/classes/task_log_table.php @@ -0,0 +1,280 @@ +. + +/** + * Task log table. + * + * @package core_admin + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_admin; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); + +/** + * Table to display list of task logs. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class task_log_table extends \table_sql { + + /** + * Constructor for the task_log table. + * + * @param string $filter + * @param int $resultfilter + */ + public function __construct(string $filter = '', int $resultfilter = null) { + global $DB; + + if (-1 === $resultfilter) { + $resultfilter = null; + } + + parent::__construct('tasklogs'); + + $columnheaders = [ + 'classname' => get_string('name'), + 'type' => get_string('tasktype', 'admin'), + 'userid' => get_string('user', 'admin'), + 'timestart' => get_string('task_starttime', 'admin'), + 'duration' => get_string('task_duration', 'admin'), + 'db' => get_string('task_dbstats', 'admin'), + 'result' => get_string('task_result', 'admin'), + 'actions' => '', + ]; + $this->define_columns(array_keys($columnheaders)); + $this->define_headers(array_values($columnheaders)); + + // The name column is a header. + $this->define_header_column('classname'); + + // This table is not collapsible. + $this->collapsible(false); + + // The actions class should not wrap. Use the BS text utility class. + $this->column_class('actions', 'text-nowrap'); + + // Allow pagination. + $this->pageable(true); + + // Allow sorting. Default to sort by timestarted DESC. + $this->sortable(true, 'timestart', SORT_DESC); + + // Add filtering. + $where = []; + $params = []; + if (!empty($filter)) { + $where[] = $DB->sql_like('classname', ':filter', false, false); + $filter = str_replace('\\', '\\\\', $filter); + $params['filter'] = '%' . $DB->sql_like_escape($filter) . '%'; + } + + if (null !== $resultfilter) { + $where[] = 'tl.result = :result'; + $params['result'] = $resultfilter; + } + + $where = implode(' AND ', $where); + + $this->set_sql('', '', $where, $params); + } + + /** + * Query the db. Store results in the table object for use by build_table. + * + * @param int $pagesize size of page for paginated displayed table. + * @param bool $useinitialsbar do you want to use the initials bar. Bar + * will only be used if there is a fullname column defined for the table. + */ + public function query_db($pagesize, $useinitialsbar = true) { + global $DB; + + // Fetch the attempts. + $sort = $this->get_sql_sort(); + if ($sort) { + $sort = "ORDER BY $sort"; + } + + $extrafields = get_extra_user_fields(\context_system::instance()); + $userfields = \user_picture::fields('u', $extrafields, 'userid2', 'user'); + + $where = ''; + if (!empty($this->sql->where)) { + $where = "WHERE {$this->sql->where}"; + } + + $sql = "SELECT + tl.*, + tl.dbreads + tl.dbwrites AS db, + tl.timeend - tl.timestart AS duration, + {$userfields} + FROM {task_log} tl + LEFT JOIN {user} u ON u.id = tl.userid + {$where} + {$sort}"; + + $this->pagesize($pagesize, $DB->count_records_sql("SELECT COUNT('x') FROM {task_log} tl {$where}", $this->sql->params)); + if (!$this->is_downloading()) { + $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); + } else { + $this->rawdata = $DB->get_records_sql($sql, $this->sql->params); + } + } + + /** + * Format the name cell. + * + * @param \stdClass $row + * @return string + */ + public function col_classname($row) : string { + $output = ''; + if (class_exists($row->classname)) { + $task = new $row->classname; + if ($task instanceof \core\task\scheduled_task) { + $output = $task->get_name(); + } + } + + $output .= \html_writer::tag('div', "\\{$row->classname}", [ + 'class' => 'task-class', + ]); + return $output; + } + + /** + * Format the type cell. + * + * @param \stdClass $row + * @return string + */ + public function col_type($row) : string { + if (\core\task\database_logger::TYPE_SCHEDULED == $row->type) { + return get_string('task_type:scheduled', 'admin'); + } else { + return get_string('task_type:adhoc', 'admin'); + } + } + + /** + * Format the timestart cell. + * + * @param \stdClass $row + * @return string + */ + public function col_result($row) : string { + if ($row->result) { + return get_string('task_result:failed', 'admin'); + } else { + return get_string('success'); + } + } + + /** + * Format the timestart cell. + * + * @param \stdClass $row + * @return string + */ + public function col_timestart($row) : string { + return userdate($row->timestart, get_string('strftimedatetimeshort', 'langconfig')); + } + + /** + * Format the duration cell. + * + * @param \stdClass $row + * @return string + */ + public function col_duration($row) : string { + $duration = round($row->timeend - $row->timestart, 2); + + if (empty($duration)) { + // The format_time function returns 'now' when the difference is exactly 0. + // Note: format_time performs concatenation in exactly this fashion so we should do this for consistency. + return '0 ' . get_string('secs', 'moodle'); + } + + return format_time($duration); + } + + /** + * Format the DB details cell. + * + * @param \stdClass $row + * @return string + */ + public function col_db($row) : string { + $output = ''; + + $output .= \html_writer::div(get_string('task_stats:dbreads', 'admin', $row->dbreads)); + $output .= \html_writer::div(get_string('task_stats:dbwrites', 'admin', $row->dbwrites)); + + return $output; + } + + /** + * Format the actions cell. + * + * @param \stdClass $row + * @return string + */ + public function col_actions($row) : string { + global $OUTPUT; + + $actions = []; + + $url = new \moodle_url('/admin/tasklogs.php', ['logid' => $row->id]); + + // Quick view. + $actions[] = $OUTPUT->action_icon( + $url, + new \pix_icon('e/search', get_string('view')), + new \popup_action('click', $url) + ); + + // Download. + $actions[] = $OUTPUT->action_icon( + new \moodle_url($url, ['download' => true]), + new \pix_icon('t/download', get_string('download')) + ); + + return implode(' ', $actions); + } + + /** + * Format the user cell. + * + * @param \stdClass $row + * @return string + */ + public function col_userid($row) : string { + if (empty($row->userid)) { + return ''; + } + + $user = (object) []; + username_load_fields_from_object($user, $row, 'user'); + + return fullname($user); + } +} diff --git a/admin/settings/server.php b/admin/settings/server.php index 73efce12e45..eaa318c72c5 100644 --- a/admin/settings/server.php +++ b/admin/settings/server.php @@ -246,10 +246,17 @@ if (\core\task\logmanager::uses_standard_settings()) { PARAM_INT ) ); - } $ADMIN->add('taskconfig', $temp); +if (empty($CFG->task_log_class) || '\\core\\task\\database_logger' == $CFG->task_log_class) { + $ADMIN->add('taskconfig', new admin_externalpage( + 'tasklogs', + new lang_string('tasklogs','admin'), + "{$CFG->wwwroot}/{$CFG->admin}/tasklogs.php" + )); +} + // E-mail settings. $ADMIN->add('server', new admin_category('email', new lang_string('categoryemail', 'admin'))); diff --git a/admin/tasklogs.php b/admin/tasklogs.php new file mode 100644 index 00000000000..bcebd581d38 --- /dev/null +++ b/admin/tasklogs.php @@ -0,0 +1,92 @@ +. + +/** + * Task log. + * + * @package admin + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../config.php'); +require_once("{$CFG->libdir}/adminlib.php"); +require_once("{$CFG->libdir}/tablelib.php"); +require_once("{$CFG->libdir}/filelib.php"); + +$filter = optional_param('filter', '', PARAM_ALPHANUMEXT); +$result = optional_param('result', null, PARAM_INT); + +$pageurl = new \moodle_url('/admin/tasklogs.php'); +$pageurl->param('filter', $filter); + +$PAGE->set_url($pageurl); +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('admin'); +$strheading = get_string('tasklogs', 'tool_task'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +require_capability('moodle/site:config', context_system::instance()); +admin_externalpage_setup('tasklogs'); + +$logid = optional_param('logid', null, PARAM_INT); +$download = optional_param('download', false, PARAM_BOOL); + +if (null !== $logid) { + $log = $DB->get_record('task_log', ['id' => $logid], '*', MUST_EXIST); + + $fs = get_file_storage(); + $file = $fs->get_file(\context_system::instance()->id, 'core', 'task_logs', $log->id, '/', 'log.txt'); + + $filename = str_replace('\\', '_', $log->classname) . "-{$log->id}.log"; + send_stored_file($file, null, 0, $download, [ + 'filename' => $filename, + ]); +} + +$renderer = $PAGE->get_renderer('tool_task'); + +echo $OUTPUT->header(); +echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [ + 'action' => $pageurl->out(), + 'filter' => $filter, + 'resultfilteroptions' => [ + (object) [ + 'value' => -1, + 'title' => get_string('all'), + 'selected' => (-1 === $result), + ], + (object) [ + 'value' => 0, + 'title' => get_string('success'), + 'selected' => (0 === $result), + ], + (object) [ + 'value' => 1, + 'title' => get_string('task_result:failed', 'admin'), + 'selected' => (1 === $result), + ], + ], +]); + +$table = new \core_admin\task_log_table($filter, $result); +$table->baseurl = $pageurl; +$table->out(100, false); + +echo $OUTPUT->footer(); diff --git a/admin/templates/tasklogs.mustache b/admin/templates/tasklogs.mustache new file mode 100644 index 00000000000..c3180a2cb22 --- /dev/null +++ b/admin/templates/tasklogs.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_admin/tasklogs + + Task Logs template. +}} +
+ + + + + + + +
diff --git a/lang/en/admin.php b/lang/en/admin.php index 86f77cd08a2..e21455dc7f0 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -1030,6 +1030,7 @@ $string['requestcategoryselection'] = 'Enable category selection'; $string['restorecourse'] = 'Restore course'; $string['restorernewroleid'] = 'Restorers\' role in courses'; $string['restorernewroleid_help'] = 'If the user does not already have the permission to manage the newly restored course, the user is automatically assigned this role and enrolled if necessary. Select "None" if you do not want restorers to be able to manage every restored course.'; +$string['resultfilter'] = 'Filter by result'; $string['reverseproxy'] = 'Reverse proxy'; $string['riskconfig'] = 'Users could change site configuration and behaviour'; $string['riskconfigshort'] = 'Configuration risk'; @@ -1173,6 +1174,16 @@ $string['task_logretention'] = 'Retention period'; $string['task_logretention_desc'] = 'The maximum period that logs should be kept for. This setting interacts with the \'Retain runs\' setting: whichever is reached first will apply'; $string['task_logretainruns'] = 'Retain runs'; $string['task_logretainruns_desc'] = 'The number of runs of each task to retain. This setting interacts with the \'Retention period\' setting: whichever is reached first will apply.'; +$string['task_type:adhoc'] = 'Adhoc'; +$string['task_type:scheduled'] = 'Scheduled'; +$string['task_result:failed'] = 'Fail'; +$string['task_stats:dbreads'] = '{$a} reads'; +$string['task_stats:dbwrites'] = '{$a} writes'; +$string['task_starttime'] = 'Start time'; +$string['task_duration'] = 'Duration'; +$string['task_dbstats'] = 'Database'; +$string['task_result'] = 'Result'; +$string['tasktype'] = 'Type'; $string['taskadmintitle'] = 'Tasks'; $string['taskanalyticscleanup'] = 'Analytics cleanup'; $string['taskautomatedbackup'] = 'Automated backups'; diff --git a/lib/classes/task/database_logger.php b/lib/classes/task/database_logger.php index 253e0d2645f..65af0ea3c65 100644 --- a/lib/classes/task/database_logger.php +++ b/lib/classes/task/database_logger.php @@ -84,6 +84,29 @@ class database_logger implements task_logger { $logdata->id = $DB->insert_record('task_log', $logdata); } + /** + * Whether this task logger has a report available. + * + * @return bool + */ + public static function has_log_report() : bool { + return true; + } + + /** + * Get any URL available for viewing relevant task log reports. + * + * @param string $classname The task class to fetch for + * @return \moodle_url + */ + public static function get_url_for_task_class(string $classname) : \moodle_url { + global $CFG; + + return new \moodle_url("/{$CFG->admin}/tasklogs.php", [ + 'filter' => $classname, + ]); + } + /** * Cleanup old task logs. */ diff --git a/lib/classes/task/logmanager.php b/lib/classes/task/logmanager.php index 6e2e6820abd..fe89d25769f 100644 --- a/lib/classes/task/logmanager.php +++ b/lib/classes/task/logmanager.php @@ -144,7 +144,12 @@ class logmanager { return false; } - return !empty(self::get_logger_classname()); + $loggerclass = self::get_logger_classname(); + if (empty($loggerclass)) { + return false; + } + + return $loggerclass::is_configured(); } /** @@ -176,7 +181,7 @@ class logmanager { } /** - * Whether to use the standard settings fore + * Whether to use the standard settings form. */ public static function uses_standard_settings() : bool { $classname = self::get_logger_classname(); diff --git a/lib/classes/task/manager.php b/lib/classes/task/manager.php index 6e8f467178c..59520182142 100644 --- a/lib/classes/task/manager.php +++ b/lib/classes/task/manager.php @@ -592,7 +592,7 @@ class manager { $task->get_lock()->release(); // Finalise the log output. - \core\task\logmanager::finalise_log(true); + logmanager::finalise_log(true); } /** @@ -604,7 +604,7 @@ class manager { global $DB; // Finalise the log output. - \core\task\logmanager::finalise_log(); + logmanager::finalise_log(); // Delete the adhoc task record - it is finished. $DB->delete_records('task_adhoc', array('id' => $task->get_id())); @@ -651,7 +651,7 @@ class manager { $task->get_lock()->release(); // Finalise the log output. - \core\task\logmanager::finalise_log(true); + logmanager::finalise_log(true); } /** @@ -680,7 +680,7 @@ class manager { global $DB; // Finalise the log output. - \core\task\logmanager::finalise_log(); + logmanager::finalise_log(); $classname = self::get_canonical_class_name($task); $record = $DB->get_record('task_scheduled', array('classname' => $classname)); diff --git a/lib/classes/task/task_logger.php b/lib/classes/task/task_logger.php index c7b322ac21c..d9118b69e89 100644 --- a/lib/classes/task/task_logger.php +++ b/lib/classes/task/task_logger.php @@ -54,4 +54,18 @@ interface task_logger { public static function store_log_for_task(task_base $task, string $logpath, bool $failed, int $dbreads, int $dbwrites, float $timestart, float $timeend); + /** + * Whether this task logger has a report available. + * + * @return bool + */ + public static function has_log_report() : bool; + + /** + * Get any URL available for viewing relevant task log reports. + * + * @param string $classname The task class to fetch for + * @return \moodle_url + */ + public static function get_url_for_task_class(string $classname) : \moodle_url; } diff --git a/lib/tests/task_logging_test.php b/lib/tests/task_logging_test.php index aaed98c5de4..d4f4e12de8b 100644 --- a/lib/tests/task_logging_test.php +++ b/lib/tests/task_logging_test.php @@ -521,6 +521,11 @@ class task_logging_test_mocked_logger implements \core\task\task_logger { */ public static $storelogfortask = []; + /** + * @var bool Whether this logger has a report. + */ + public static $haslogreport = true; + /** * Reset the test class. */ @@ -555,4 +560,23 @@ class task_logging_test_mocked_logger implements \core\task\task_logger { self::$storelogfortask[] = func_get_args(); } + /** + * Whether this task logger has a report available. + * + * @return bool + */ + public static function has_log_report() : bool { + return self::$haslogreport; + } + + /** + * Get any URL available for viewing relevant task log reports. + * + * @param string $classname The task class to fetch for + * @return \moodle_url + */ + public static function get_url_for_task_class(string $classname) : \moodle_url { + return new \moodle_url(''); + } + } diff --git a/theme/boost/scss/moodle/admin.scss b/theme/boost/scss/moodle/admin.scss index 4faa33e8c3a..85d654a9ee5 100644 --- a/theme/boost/scss/moodle/admin.scss +++ b/theme/boost/scss/moodle/admin.scss @@ -827,3 +827,10 @@ } } } + +#page-admin-tasklogs { + .task-class { + font-size: $font-size-sm; + color: $gray-600; + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 90b2cff93e3..46bba859d13 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -11151,6 +11151,10 @@ div.editor_atto_toolbar button .icon { padding-left: 0.5rem; content: "/"; } +#page-admin-tasklogs .task-class { + font-size: 0.8203125rem; + color: #868e96; } + .blockmovetarget .accesshide { position: relative; left: initial; }