MDL-49399 task: Add admin log viewer
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 4 Dec 2018 02:46:25 +0000 (10:46 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 16 Jan 2019 04:14:25 +0000 (12:14 +0800)
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

12 files changed:
admin/classes/task_log_table.php [new file with mode: 0644]
admin/settings/server.php
admin/tasklogs.php [new file with mode: 0644]
admin/templates/tasklogs.mustache [new file with mode: 0644]
lang/en/admin.php
lib/classes/task/database_logger.php
lib/classes/task/logmanager.php
lib/classes/task/manager.php
lib/classes/task/task_logger.php
lib/tests/task_logging_test.php
theme/boost/scss/moodle/admin.scss
theme/boost/style/moodle.css

diff --git a/admin/classes/task_log_table.php b/admin/classes/task_log_table.php
new file mode 100644 (file)
index 0000000..554d92d
--- /dev/null
@@ -0,0 +1,280 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Task log table.
+ *
+ * @package    core_admin
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @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 <andrew@nicols.co.uk>
+ * @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('&nbsp;', $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);
+    }
+}
index 73efce1..eaa318c 100644 (file)
@@ -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 (file)
index 0000000..bcebd58
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Task log.
+ *
+ * @package    admin
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @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 (file)
index 0000000..c3180a2
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_admin/tasklogs
+
+    Task Logs template.
+}}
+<form class="form-inline" method="GET" action="{{{action}}}">
+    <label class="sr-only" for="tasklog-filter">{{#str}}filter{{/str}}</label>
+    <input class="form-control" type="text" id="tasklog-filter" name="filter" value="{{{filter}}}">
+
+    <label class="sr-only" for="tasklog-resultfilter">{{#str}}resultfilter, admin{{/str}}</label>
+    <select class="form-control custom-select" name="result" id="tasklog-resultfilter">
+        {{#resultfilteroptions}}
+        <option value="{{{value}}}"{{#selected}} selected="selected"{{/selected}}>{{title}}</option>
+        {{/resultfilteroptions}}
+    </select>
+
+    <input class="btn btn-primary" type="submit" value="{{#str}}filter{{/str}}">
+</form>
index 86f77cd..e21455d 100644 (file)
@@ -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';
index 253e0d2..65af0ea 100644 (file)
@@ -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.
      */
index 6e2e682..fe89d25 100644 (file)
@@ -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();
index 6e8f467..5952018 100644 (file)
@@ -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));
index c7b322a..d9118b6 100644 (file)
@@ -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;
 }
index aaed98c..d4f4e12 100644 (file)
@@ -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('');
+    }
+
 }
index 4faa33e..85d654a 100644 (file)
         }
     }
 }
+
+#page-admin-tasklogs {
+    .task-class {
+        font-size: $font-size-sm;
+        color: $gray-600;
+    }
+}
index 90b2cff..46bba85 100644 (file)
@@ -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; }