// Cronicle History Page Class.subclass( Page.Base, "Page.History", { default_sub: 'history', onInit: function() { // called once at page load // var html = ''; // this.div.html( html ); this.charts = {}; }, onActivate: function(args) { // page activation if (!this.requireLogin(args)) return true; if (!args) args = {}; if (!args.sub) args.sub = this.default_sub; this.args = args; app.showTabBar(true); this.tab[0]._page_id = Nav.currentAnchor(); this.div.addClass('loading'); this['gosub_'+args.sub](args); return true; }, gosub_history: function(args) { // show history app.setWindowTitle( "History" ); var html = ''; // html += '
'; html += '
'; html += '
'; html += 'All Completed Jobs'; // html += '
Refresh
'; // html += '
 
'; var sorted_events = app.schedule.sort( function(a, b) { return a.title.toLowerCase().localeCompare( b.title.toLowerCase() ); } ); html += '
 
'; html += '
'; html += '
'; html += '
'; html += '
'; // padding this.div.html( html ); this.get_history(); }, get_history: function() { var args = this.args; if (!args.offset) args.offset = 0; if (!args.limit) args.limit = 25; app.api.post( 'app/get_history', copy_object(args), this.receive_history.bind(this) ); }, receive_history: function(resp) { // receive page of history from server, render it this.lastHistoryResp = resp; var html = ''; this.div.removeClass('loading'); var size = get_inner_window_size(); var col_width = Math.floor( ((size.width * 0.9) - 50) / 8 ); this.events = []; if (resp.rows) this.events = resp.rows; var cols = ['Job ID', 'Event Name', 'Category', 'Plugin', 'Hostname', 'Result', 'Start Date/Time', 'Elapsed Time']; var self = this; var num_visible_items = 0; html += this.getPaginatedTable( resp, cols, 'event', function(job, idx) { /*var actions = [ 'Job Details', 'Event History' ];*/ // suppress row view if job was deleted if (job.action != 'job_complete') return null; num_visible_items++; var event = find_object( app.schedule, { id: job.event } ); var event_link = '(None)'; if (event && job.id) { event_link = '
' + self.getNiceEvent('' + (event.title || job.event) + '', col_width + 40) + '
'; } else if (job.event_title) { event_link = self.getNiceEvent(job.event_title, col_width + 40); } var cat = job.category ? find_object( app.categories, { id: job.category } ) : null; if (!cat && job.category_title) cat = { id: job.category, title: job.category_title }; var plugin = job.plugin ? find_object( app.plugins, { id: job.plugin } ) : null; if (!plugin && job.plugin_title) plugin = { id: job.plugin, title: job.plugin_title }; var job_link = '
--
'; if (job.id) job_link = '
' + self.getNiceJob('' + job.id + '') + '
'; var tds = [ job_link, event_link, self.getNiceCategory( cat, col_width ), self.getNicePlugin( plugin, col_width ), self.getNiceGroup( null, job.hostname, col_width ), (job.code == 0) ? ' Success' : ' Error', get_nice_date_time( job.time_start, false, true ), get_text_from_seconds( job.elapsed, true, false ) // actions.join(' | ') ]; if (!job.id) tds.className = 'disabled'; if (cat && cat.color) { if (tds.className) tds.className += ' '; else tds.className = ''; tds.className += cat.color; } return tds; } ); if (resp.rows && resp.rows.length && !num_visible_items) { html += ''; html += 'All items were deleted on this page.'; html += ''; } this.div.find('#d_history_table').html( html ); }, jump_to_event_history: function() { // make a selection from the event filter menu var id = $('#fe_hist_event').val(); if (id) Nav.go( '#History?sub=event_history&id=' + id ); }, gosub_event_stats: function(args) { // request event stats if (!args.offset) args.offset = 0; if (!args.limit) args.limit = 50; app.api.post( 'app/get_event_history', copy_object(args), this.receive_event_stats.bind(this) ); }, receive_event_stats: function(resp) { // render event stats page this.lastEventStatsResp = resp; var html = ''; var args = this.args; var rows = this.rows = resp.rows; var size = get_inner_window_size(); var col_width = Math.floor( ((size.width * 0.9) - 300) / 4 ); var event = find_object( app.schedule, { id: args.id } ) || null; if (!event) return app.doError("Could not locate event in schedule: " + args.id); var cat = event.category ? find_object( app.categories, { id: event.category } ) : null; var group = event.target ? find_object( app.server_groups, { id: event.target } ) : null; var plugin = event.plugin ? find_object( app.plugins, { id: event.plugin } ) : null; if (group && event.multiplex) { group = copy_object(group); group.multiplex = 1; } app.setWindowTitle( "Event Stats: " + event.title ); this.div.removeClass('loading'); html += this.getSidebarTabs( 'event_stats', [ ['history', "All Completed"], ['event_stats', "Event Stats"], ['event_history&id=' + args.id, "Event History"] ] ); html += '
'; html += '
Event Stats'; html += '
'; html += '
EVENT NAME
'; html += ''; html += '
CATEGORY NAME
'; html += '
' + this.getNiceCategory(cat, col_width) + '
'; html += '
EVENT TIMING
'; html += '
' + (event.enabled ? summarize_event_timing(event.timing, event.timezone) : '(Disabled)') + '
'; html += '
'; html += '
'; html += '
USERNAME
'; html += '
' + this.getNiceUsername(event, false, col_width) + '
'; html += '
PLUGIN NAME
'; html += '
' + this.getNicePlugin(plugin, col_width) + '
'; html += '
EVENT TARGET
'; html += '
' + this.getNiceGroup(group, event.target, col_width) + '
'; html += '
'; var total_elapsed = 0; var total_cpu = 0; var total_mem = 0; var total_success = 0; var total_log_size = 0; var count = 0; for (var idx = 0, len = rows.length; idx < len; idx++) { var job = rows[idx]; if (job.action != 'job_complete') continue; count++; total_elapsed += (job.elapsed || 0); if (job.cpu && job.cpu.total) total_cpu += (job.cpu.total / (job.cpu.count || 1)); if (job.mem && job.mem.total) total_mem += (job.mem.total / (job.mem.count || 1)); if (job.code == 0) total_success++; total_log_size += (job.log_file_size || 0); } if (!count) count = 1; var nice_last_result = 'n/a'; if (rows.length > 0) { var job = find_object( rows, { action: 'job_complete' } ); if (job) nice_last_result = (job.code == 0) ? ' Success' : ' Error'; } html += '
'; html += '
AVG. ELAPSED
'; html += '
' + get_text_from_seconds(total_elapsed / count, true, false) + '
'; html += '
AVG. CPU
'; html += '
' + short_float(total_cpu / count) + '%
'; html += '
AVG. MEMORY
'; html += '
' + get_text_from_bytes( total_mem / count ) + '
'; html += '
'; html += '
'; html += '
SUCCESS RATE
'; html += '
' + pct(total_success, count) + '
'; html += '
LAST RESULT
'; html += '
' + nice_last_result + '
'; html += '
AVG. LOG SIZE
'; html += '
' + get_text_from_bytes( total_log_size / count ) + '
'; html += '
'; html += '
'; html += '
'; // graph containers html += '
'; html += '
Performance History
'; html += '
'; html += '
'; html += '
'; // cpu / mem graphs html += '
'; html += '
'; html += '
CPU Usage History
'; html += '
'; html += '
'; html += '
'; html += '
Memory Usage History
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; // padding html += '
'; // sidebar tabs this.div.html( html ); // graphs this.render_perf_line_chart(); this.render_cpu_line_chart(); this.render_mem_line_chart(); }, render_perf_line_chart: function() { // event perf over time var rows = this.rows; var perf_keys = {}; var perf_data = []; var perf_times = []; // build perf data for chart // read backwards as server data is unshifted (descending by date, newest first) for (var idx = rows.length - 1; idx >= 0; idx--) { var job = rows[idx]; if (job.action != 'job_complete') continue; if (!job.perf) job.perf = { total: job.elapsed }; if (!isa_hash(job.perf)) job.perf = parse_query_string( job.perf.replace(/\;/g, '&') ); var pscale = 1; if (job.perf.scale) { pscale = job.perf.scale; } var perf = deep_copy_object( job.perf.perf ? job.perf.perf : job.perf ); delete perf.scale; // remove counters from pie for (var key in perf) { if (key.match(/^c_/)) delete perf[key]; } if (perf.t) { perf.total = perf.t; delete perf.t; } if ((num_keys(perf) > 1) && perf.total) { if (!perf.other) { var totes = 0; for (var key in perf) { if (key != 'total') totes += perf[key]; } if (totes < perf.total) { perf.other = perf.total - totes; } } } // divide everything by scale, so we get seconds for (var key in perf) { perf[key] /= pscale; } perf_data.push( perf ); for (var key in perf) { perf_keys[key] = 1; } // track times as well perf_times.push( job.time_end || (job.time_start + job.elapsed) ); } // foreach row // build up timestamp data var tstamp_col = []; for (var idy = 0, ley = perf_times.length; idy < ley; idy++) { tstamp_col.push( perf_times[idy] * 1000 ); } // foreach row var sorted_keys = hash_keys_to_array(perf_keys).sort(); var datasets = []; for (var idx = 0, len = sorted_keys.length; idx < len; idx++) { var perf_key = sorted_keys[idx]; var clr = 'rgb(' + this.graph_colors[ idx % this.graph_colors.length ] + ')'; var dataset = { label: perf_key, backgroundColor: clr, borderColor: clr, fill: false, data: [] }; for (var idy = 0, ley = perf_data.length; idy < ley; idy++) { var perf = perf_data[idy]; var value = Math.max( 0, perf[perf_key] || 0 ); dataset.data.push({ x: tstamp_col[idy], y: short_float(value) }); } // foreach row datasets.push( dataset ); } // foreach key this.charts.perf = new Chart( $('#c_graph_hist_perf').get(0).getContext('2d'), { type: 'line', data: { datasets: datasets }, options: { animation: { duration: 0 }, responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: true, position: 'bottom', labels: { fontStyle: 'bold', padding: 15 } }, title:{ display: false, text: "" }, scales: { xAxes: [{ type: "time", display: true, time: { parser: 'MM/DD/YYYY HH:mm', round: 'minute', tooltipFormat: 'll hh:mm a' }, scaleLabel: { display: false, labelString: 'Date' } }, ], yAxes: [{ ticks: { beginAtZero: true, callback: function(value, index, values) { if (value < 0) return ''; return '' + get_text_from_seconds_round_custom(value, true); } }, scaleLabel: { display: true, // labelString: 'value' } }] }, tooltips: { mode: 'nearest', intersect: false, callbacks: { label: function(tooltip, data) { var value = short_float(tooltip.yLabel); if (value >= 60) value = get_text_from_seconds( Math.floor(value), 1, 0 ).replace(/ \;/ig, ' '); else value = '' + value + " sec"; return " " + datasets[tooltip.datasetIndex].label + ": " + value; } } } } }); }, render_cpu_line_chart: function() { // event cpu usage over time var rows = this.rows; var color = Chart.helpers.color; var col_avg = []; var col_max = []; // build data for chart // read backwards as server data is unshifted (descending by date, newest first) for (var idx = rows.length - 1; idx >= 0; idx--) { var job = rows[idx]; if (job.action != 'job_complete') continue; if (!job.cpu) job.cpu = {}; var x = (job.time_end || (job.time_start + job.elapsed)) * 1000; col_avg.push({ x: x, y: short_float( (job.cpu.total || 0) / (job.cpu.count || 1) ) }); col_max.push({ x: x, y: short_float( job.cpu.max || 0 ) }); } // foreach row var datasets = [ { label: "CPU Peak", borderColor: '#888888', fill: false, data: col_max }, { label: "CPU Avg", borderColor: '#3f7ed5', backgroundColor: color('#3f7ed5').alpha(0.5).rgbString(), data: col_avg } ]; this.charts.cpu = new Chart( $('#c_graph_hist_cpu').get(0).getContext('2d'), { type: 'line', data: { datasets: datasets }, options: { animation: { duration: 0 }, responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: true, position: 'bottom', labels: { fontStyle: 'bold', padding: 15 } }, title:{ display: false, text: "" }, scales: { xAxes: [{ type: "time", display: true, time: { parser: 'MM/DD/YYYY HH:mm', round: 'minute', tooltipFormat: 'll hh:mm a' }, scaleLabel: { display: false, labelString: 'Date' } }, ], yAxes: [{ ticks: { beginAtZero: true, callback: function(value, index, values) { return '' + Math.round(value) + '%'; } }, scaleLabel: { display: true, // labelString: 'value' } }] }, tooltips: { mode: 'index', intersect: false, callbacks: { label: function(tooltip, data) { return " " + datasets[tooltip.datasetIndex].label + ": " + short_float(tooltip.yLabel) + '%'; } } } } }); }, render_mem_line_chart: function() { // event mem usage over time var rows = this.rows; var color = Chart.helpers.color; var col_avg = []; var col_max = []; // build data for chart // read backwards as server data is unshifted (descending by date, newest first) for (var idx = rows.length - 1; idx >= 0; idx--) { var job = rows[idx]; if (job.action != 'job_complete') continue; if (!job.mem) job.mem = {}; var x = (job.time_end || (job.time_start + job.elapsed)) * 1000; col_avg.push({ x: x, y: short_float( (job.mem.total || 0) / (job.mem.count || 1) ) }); col_max.push({ x: x, y: short_float( job.mem.max || 0 ) }); } // foreach row var datasets = [ { label: "Mem Peak", borderColor: '#888888', fill: false, data: col_max }, { label: "Mem Avg", borderColor: '#279321', backgroundColor: color('#279321').alpha(0.5).rgbString(), data: col_avg } ]; this.charts.mem = new Chart( $('#c_graph_hist_mem').get(0).getContext('2d'), { type: 'line', data: { datasets: datasets }, options: { animation: { duration: 0 }, responsive: true, responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { display: true, position: 'bottom', labels: { fontStyle: 'bold', padding: 15 } }, title:{ display: false, text: "" }, scales: { xAxes: [{ type: "time", display: true, time: { parser: 'MM/DD/YYYY HH:mm', round: 'minute', tooltipFormat: 'll hh:mm a' }, scaleLabel: { display: false, labelString: 'Date' } }, ], yAxes: [{ ticks: { beginAtZero: true, callback: function(value, index, values) { return '' + get_text_from_bytes(value, 1); } }, scaleLabel: { display: true, // labelString: 'value' } }] }, tooltips: { mode: 'index', intersect: false, callbacks: { label: function(tooltip, data) { return " " + datasets[tooltip.datasetIndex].label + ": " + get_text_from_bytes(tooltip.yLabel); } } } } }); }, gosub_event_history: function(args) { // show table of all history for a single event if (!args.offset) args.offset = 0; if (!args.limit) args.limit = 25; app.api.post( 'app/get_event_history', copy_object(args), this.receive_event_history.bind(this) ); }, receive_event_history: function(resp) { // render event history this.lastEventHistoryResp = resp; var html = ''; var args = this.args; var rows = this.rows = resp.rows; var size = get_inner_window_size(); var col_width = Math.floor( ((size.width * 0.9) - 300) / 7 ); var event = find_object( app.schedule, { id: args.id } ) || null; if (!event) return app.doError("Could not locate event in schedule: " + args.id); app.setWindowTitle( "Event History: " + event.title ); this.div.removeClass('loading'); html += this.getSidebarTabs( 'event_history', [ ['history', "All Completed"], ['event_stats&id=' + args.id, "Event Stats"], ['event_history', "Event History"] ] ); html += '
'; var cols = ['Job ID', 'Hostname', 'Result', 'Start Date/Time', 'Elapsed Time', 'Avg CPU', 'Avg Mem']; html += '
'; html += 'Event History: ' + event.title; html += '
'; html += '
'; var self = this; var num_visible_items = 0; html += this.getPaginatedTable( resp, cols, 'event', function(job, idx) { if (job.action != 'job_complete') return null; num_visible_items++; var cpu_avg = 0; var mem_avg = 0; if (job.cpu) cpu_avg = short_float( (job.cpu.total || 0) / (job.cpu.count || 1) ); if (job.mem) mem_avg = short_float( (job.mem.total || 0) / (job.mem.count || 1) ); var tds = [ '
 ' + job.id.substring(0, 11) + '
', self.getNiceGroup( null, job.hostname, col_width ), (job.code == 0) ? ' Success' : ' Error', get_nice_date_time( job.time_start, false, true ), get_text_from_seconds( job.elapsed, true, false ), '' + cpu_avg + '%', get_text_from_bytes(mem_avg) // actions.join(' | ') ]; return tds; } ); if (resp.rows && resp.rows.length && !num_visible_items) { html += ''; html += 'All items were deleted on this page.'; html += ''; } html += '
'; // padding html += ''; // sidebar tabs this.div.html( html ); }, onStatusUpdate: function(data) { // received status update (websocket), update sub-page if needed if (data.jobs_changed && (this.args.sub == 'history')) this.get_history(); }, onResizeDelay: function(size) { // called 250ms after latest window resize // so we can run more expensive redraw operations switch (this.args.sub) { case 'history': if (this.lastHistoryResp) { this.receive_history( this.lastHistoryResp ); } break; case 'event_stats': if (this.lastEventStatsResp) { this.receive_event_stats( this.lastEventStatsResp ); } break; case 'event_history': if (this.lastEventHistoryResp) { this.receive_event_history( this.lastEventHistoryResp ); } break; } }, onDeactivate: function() { // called when page is deactivated for (var key in this.charts) { this.charts[key].destroy(); } this.charts = {}; delete this.rows; if (this.args && (this.args.sub == 'event_stats')) this.div.html( '' ); return true; } });