// Cronicle JobDetails Page
Class.subclass( Page.Base, "Page.JobDetails", {
pie_colors: {
cool: 'green',
warm: 'rgb(240,240,0)',
hot: '#F7464A',
progress: '#3f7ed5',
empty: 'rgba(0, 0, 0, 0.05)'
},
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 = {};
this.args = args;
if (!args.id) {
app.doError("The Job Details page requires a Job ID.");
return true;
}
app.setWindowTitle( "Job Details: #" + args.id );
app.showTabBar(true);
this.tab.show();
this.tab[0]._page_id = Nav.currentAnchor();
this.retry_count = 3;
this.go_when_ready();
return true;
},
go_when_ready: function() {
// make sure we're not in the limbo state between starting a manual job,
// and waiting for activeJobs to be updated
var self = this;
var args = this.args;
if (this.find_job(args.id)) {
// job is currently active -- jump to real-time view
args.sub = 'live';
this.gosub_live(args);
}
else {
// job must be completed -- jump to archive view
args.sub = 'archive';
this.gosub_archive(args);
}
},
gosub_archive: function(args) {
// show job archive
var self = this;
Debug.trace("Showing archived job: " + args.id);
this.div.addClass('loading');
app.api.post( 'app/get_job_details', { id: args.id }, this.receive_details.bind(this), function(resp) {
// error capture
if (self.retry_count >= 0) {
Debug.trace("Failed to get_job_details, trying again in 1s...");
self.retry_count--;
setTimeout( function() { self.go_when_ready(); }, 1000 );
}
else {
// show error
app.doError("Error: " + resp.description);
self.div.removeClass('loading');
}
} );
},
get_job_result_banner: function(job) {
// render banner based on job result
var icon = '';
var type = '';
if (job.abort_reason || job.unknown) {
type = 'warning';
icon = 'exclamation-circle';
}
else if (job.code) {
type = 'error';
icon = 'exclamation-triangle';
}
else {
type = 'success';
icon = 'check-circle';
}
if (!job.description && job.code) {
job.description = "Job failed with code: " + job.code;
}
if (!job.code && (!job.description || job.description.replace(/\W+/, '').match(/^success(ful)?$/i))) {
job.description = "Job completed successfully at " + get_nice_date_time(job.time_end, false, true);
// add timezone abbreviation
job.description += " " + moment.tz(job.time_end * 1000, app.tz).format('z');
}
if (job.code && !job.description.match(/^\s*error/i)) {
var desc = job.description;
job.description = "Error";
if (job.code != 1) job.description += " " + job.code;
job.description += ": " + desc;
}
var job_desc_html = trim( job.description.replace(/\r\n/g, "\n") );
var multiline = !!job.description.match(/\n/);
job_desc_html = encode_entities( job_desc_html ).replace(/\n/g, " \n");
var html = '';
html += '
';
if (multiline) {
html += job_desc_html;
}
else {
html += '' + job_desc_html;
}
html += '
';
return html;
},
delete_job: function() {
// delete job, after confirmation
var self = this;
var job = this.job;
app.confirm( 'Delete Job', "Are you sure you want to delete the current job log and history?", "Delete", function(result) {
if (result) {
app.showProgress( 1.0, "Deleting job..." );
app.api.post( 'app/delete_job', job, function(resp) {
app.hideProgress();
app.showMessage('success', "Job ID '"+job.id+"' was deleted successfully.");
$('#tab_History').trigger('click');
self.tab.hide();
} );
}
} );
},
run_again: function() {
// run job again
var self = this;
var event = find_object( app.schedule, { id: this.job.event } ) || null;
if (!event) return app.doError("Could not locate event in schedule: " + this.job.event_title + " (" + this.job.event + ")");
var job = deep_copy_object( event );
job.now = this.job.now;
job.params = this.job.params;
app.showProgress( 1.0, "Starting job..." );
app.api.post( 'app/run_event', job, function(resp) {
// app.showMessage('success', "Event '"+event.title+"' has been started.");
self.jump_live_job_id = resp.ids[0];
self.jump_live_time_start = hires_time_now();
self.jump_to_live_when_ready();
} );
},
jump_to_live_when_ready: function() {
// make sure live view is ready (job may still be starting)
var self = this;
if (!this.active) return; // user navigated away from page
if (app.activeJobs[this.jump_live_job_id] || ((hires_time_now() - this.jump_live_time_start) >= 3.0)) {
app.hideProgress();
Nav.go( 'JobDetails?id=' + this.jump_live_job_id );
delete this.jump_live_job_id;
delete this.jump_live_time_start;
}
else {
setTimeout( self.jump_to_live_when_ready.bind(self), 250 );
}
},
receive_details: function(resp) {
// receive job details from server, render them
var html = '';
var job = this.job = resp.job;
this.div.removeClass('loading');
var size = get_inner_window_size();
var col_width = Math.floor( ((size.width * 0.9) - 300) / 4 );
// locate objects
var event = find_object( app.schedule, { id: job.event } ) || {};
var cat = job.category ? find_object( app.categories, { id: job.category } ) : null;
var group = event.target ? find_object( app.server_groups, { id: event.target } ) : null;
var plugin = job.plugin ? find_object( app.plugins, { id: job.plugin } ) : null;
if (group && event.multiplex) {
group = copy_object(group);
group.multiplex = 1;
}
html += '
';
html += 'Completed Job';
if (event.id && !event.multiplex) html += '
Run Again
';
if (app.isAdmin()) html += '
Delete Job
';
html += '';
html += '
';
// result banner
html += this.get_job_result_banner(job);
// fieldset header
html += '';
// pies
html += '
';
html += '
';
html += '
Performance Metrics
';
html += '';
// html += '';
html += '';
html += '
';
html += '
';
html += '';
html += '
Memory Usage
';
html += '';
// html += '';
html += '';
html += '
';
html += '
';
html += '';
html += '
CPU Usage
';
html += '';
// html += '';
html += '';
html += '
';
html += '
';
// custom data table
if (job.table && job.table.rows && job.table.rows.length) {
var table = job.table;
html += '
' + (table.title || 'Job Stats') + '
';
html += '
';
if (table.header && table.header.length) {
html += '
';
for (var idx = 0, len = table.header.length; idx < len; idx++) {
html += '
' + table.header[idx] + '
';
}
html += '
';
}
var filters = table.filters || [];
for (var idx = 0, len = table.rows.length; idx < len; idx++) {
var row = table.rows[idx];
if (row && row.length) {
html += '
';
for (var idy = 0, ley = row.length; idy < ley; idy++) {
var col = row[idy];
html += '
';
if (typeof(col) != 'undefined') {
if (filters[idy] && window[filters[idy]]) html += window[filters[idy]](col);
else if ((typeof(col) == 'string') && col.match(/^filter\:(\w+)\((.+)\)$/)) {
var filter = RegExp.$1;
var value = RegExp.$2;
if (window[filter]) html += window[filter](value);
else html += value;
}
else html += col;
}
html += '
';
} // foreach col
html += '
';
} // good row
} // foreach row
html += '
';
if (table.caption) html += '
' + table.caption + '
';
} // custom data table
// custom html table
if (job.html) {
html += '
' + (job.html.title || 'Job Report') + '
';
html += '
' + job.html.content + '
';
if (job.html.caption) html += '
' + job.html.caption + '
';
}
// job log (IFRAME)
html += '
';
html += 'Job Event Log';
if (job.log_file_size) html += ' (' + get_text_from_bytes(job.log_file_size, 1) + ')';
html += '
';
var max_log_file_size = config.max_log_file_size || 10485760;
if (job.log_file_size && (job.log_file_size >= max_log_file_size)) {
// too big to show? ask user
html += '
';
html += '
';
html += '
Warning: Job event log file is ' + get_text_from_bytes(job.log_file_size, 1) + '. Please consider downloading instead of viewing in browser.
';
html += '
Download Log
';
html += '
View Log
';
html += '';
html += '
';
html += '
';
}
else {
var size = get_inner_window_size();
var iheight = size.height - 100;
html += '';
}
this.div.html( html );
// arch perf chart
var suffix = ' sec';
var pscale = 1;
if (!job.perf) job.perf = { total: job.elapsed };
if (!isa_hash(job.perf)) job.perf = parse_query_string( job.perf.replace(/\;/g, '&') );
if (job.perf.scale) {
pscale = job.perf.scale;
delete job.perf.scale;
}
var perf = job.perf.perf ? job.perf.perf : job.perf;
// remove counters from pie
for (var key in perf) {
if (key.match(/^c_/)) delete perf[key];
}
// clean up total, add other
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;
}
}
delete perf.total; // only show total if by itself
}
// remove outer 'umbrella' perf keys if inner ones are more specific
// (i.e. remove "db" if we have "db_query" and/or "db_connect")
for (var key in perf) {
for (var subkey in perf) {
if ((subkey.indexOf(key + '_') == 0) && (subkey.length > key.length + 1)) {
delete perf[key];
break;
}
}
}
// divide everything by scale, so we get seconds
for (var key in perf) {
perf[key] /= pscale;
}
var colors = this.graph_colors;
var color_idx = 0;
var p_data = [];
var p_colors = [];
var p_labels = [];
var perf_keys = hash_keys_to_array(perf).sort();
for (var idx = 0, len = perf_keys.length; idx < len; idx++) {
var key = perf_keys[idx];
var value = perf[key];
p_data.push( short_float(value) );
p_colors.push( 'rgb(' + colors[color_idx] + ')' );
p_labels.push( key );
color_idx = (color_idx + 1) % colors.length;
}
var ctx = $("#c_arch_perf").get(0).getContext("2d");
var perf_chart = new Chart(ctx, {
type: 'pie',
data: {
datasets: [{
data: p_data,
backgroundColor: p_colors,
label: ''
}],
labels: p_labels
},
options: {
responsive: true,
responsiveAnimationDuration: 0,
maintainAspectRatio: false,
legend: {
display: false,
position: 'right',
},
title: {
display: false,
text: ''
},
animation: {
animateScale: true,
animateRotate: true
}
}
});
var legend_html = '';
legend_html += '
';
for (var idx = 0, len = perf_keys.length; idx < len; idx++) {
legend_html += '
' + p_labels[idx] + '
';
}
legend_html += '
';
var perf_legend = $('#d_arch_perf_legend');
perf_legend.html( legend_html );
this.charts.perf = perf_chart;
// arch cpu pie
var cpu_avg = 0;
if (!job.cpu) job.cpu = {};
if (job.cpu.total && job.cpu.count) {
cpu_avg = short_float( job.cpu.total / job.cpu.count );
}
var jcm = 100;
var ctx = $("#c_arch_cpu").get(0).getContext("2d");
var cpu_chart = new Chart(ctx, {
type: 'doughnut',
data: {
datasets: [{
data: [
Math.min(cpu_avg, jcm),
jcm - Math.min(cpu_avg, jcm),
],
backgroundColor: [
(cpu_avg < jcm*0.5) ? this.pie_colors.cool :
((cpu_avg < jcm*0.75) ? this.pie_colors.warm : this.pie_colors.hot),
this.pie_colors.empty
],
label: ''
}],
labels: []
},
options: {
events: [],
responsive: true,
responsiveAnimationDuration: 0,
maintainAspectRatio: false,
legend: {
display: false,
position: 'right',
},
title: {
display: false,
text: ''
},
animation: {
animateScale: true,
animateRotate: true
}
}
});
// arch cpu overlay
var html = '';
html += '
'+cpu_avg+'%
';
html += '
Average
';
$('#d_arch_cpu_overlay').html( html );
// arch cpu legend
var html = '';
html += '
MIN
';
html += '
' + short_float(job.cpu.min || 0) + '%
';
html += '
PEAK
';
html += '
' + short_float(job.cpu.max || 0) + '%
';
$('#d_arch_cpu_legend').html( html );
this.charts.cpu = cpu_chart;
// arch mem pie
var mem_avg = 0;
if (!job.mem) job.mem = {};
if (job.mem.total && job.mem.count) {
mem_avg = Math.floor( job.mem.total / job.mem.count );
}
var jmm = config.job_memory_max || 1073741824;
var ctx = $("#c_arch_mem").get(0).getContext("2d");
var mem_chart = new Chart(ctx, {
type: 'doughnut',
data: {
datasets: [{
data: [
Math.min(mem_avg, jmm),
jmm - Math.min(mem_avg, jmm),
],
backgroundColor: [
(mem_avg < jmm*0.5) ? this.pie_colors.cool :
((mem_avg < jmm*0.75) ? this.pie_colors.warm : this.pie_colors.hot),
this.pie_colors.empty
],
label: ''
}],
labels: []
},
options: {
events: [],
responsive: true,
responsiveAnimationDuration: 0,
maintainAspectRatio: false,
legend: {
display: false,
position: 'right',
},
title: {
display: false,
text: ''
},
animation: {
animateScale: true,
animateRotate: true
}
}
});
// arch mem overlay
var html = '';
html += '
'+get_text_from_bytes(mem_avg, 10)+'
';
html += '
Average
';
$('#d_arch_mem_overlay').html( html );
// arch mem legend
var html = '';
html += '
MIN
';
html += '
' + get_text_from_bytes(job.mem.min || 0, 1) + '
';
html += '
PEAK
';
html += '
' + get_text_from_bytes(job.mem.max || 0, 1) + '
';
$('#d_arch_mem_legend').html( html );
this.charts.mem = mem_chart;
},
do_download_log: function() {
// download job log file
var job = this.job;
window.location = '/api/app/get_job_log?id=' + job.id + '&download=1';
},
do_view_inline_log: function() {
// swap out job log size warning with IFRAME containing inline log
var job = this.job;
var html = '';
var size = get_inner_window_size();
var iheight = size.height - 100;
html += '';
$('#d_job_log_warning').html( html );
},
abort_job: function() {
// abort job, after confirmation
var job = this.find_job(this.args.id);
app.confirm( 'Abort Job', "Are you sure you want to abort the current job?", "Abort", function(result) {
if (result) {
app.showProgress( 1.0, "Aborting job..." );
app.api.post( 'app/abort_job', job, function(resp) {
app.hideProgress();
app.showMessage('success', "Job '"+job.event_title+"' was aborted successfully.");
} );
}
} );
},
check_watch_enabled: function(job) {
// check if watch is enabled on current live job
var watch_enabled = 0;
var email = app.user.email.toLowerCase();
if (email && job.notify_success && (job.notify_success.toLowerCase().indexOf(email) > -1)) watch_enabled++;
if (email && job.notify_fail && (job.notify_fail.toLowerCase().indexOf(email) > -1)) watch_enabled++;
return (watch_enabled == 2);
},
watch_add_me: function(job, key) {
// add current user's e-mail to job property
if (!job[key]) job[key] = '';
var value = trim( job[key].replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '') );
var email = app.user.email.toLowerCase();
var regexp = new RegExp( "\\b" + escape_regexp(email) + "\\b", "i" );
if (!value.match(regexp)) {
if (value) value += ', ';
job[key] = value + app.user.email;
}
},
watch_remove_me: function(job, key) {
// remove current user's email from job property
if (!job[key]) job[key] = '';
var value = trim( job[key].replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '') );
var email = app.user.email.toLowerCase();
var regexp = new RegExp( "\\b" + escape_regexp(email) + "\\b", "i" );
value = value.replace( regexp, '' ).replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '');
job[key] = trim(value);
},
toggle_watch: function() {
// toggle watch on/off on current live job
var job = this.find_job(this.args.id);
var watch_enabled = this.check_watch_enabled(job);
if (!watch_enabled) {
this.watch_add_me( job, 'notify_success' );
this.watch_add_me( job, 'notify_fail' );
}
else {
this.watch_remove_me( job, 'notify_success' );
this.watch_remove_me( job, 'notify_fail' );
}
// update on server
$('#s_watch_job > i').removeClass().addClass('fa fa-spin fa-spinner');
app.api.post( 'app/update_job', { id: job.id, notify_success: job.notify_success, notify_fail: job.notify_fail }, function(resp) {
watch_enabled = !watch_enabled;
if (watch_enabled) {
app.showMessage('success', "You will now be notified via e-mail when the job completes (success or fail).");
$('#s_watch_job').css('color', '#3f7ed5');
$('#s_watch_job > i').removeClass().addClass('fa fa-check-square-o');
}
else {
app.showMessage('success', "You will no longer be notified about this job.");
$('#s_watch_job').css('color', '#777');
$('#s_watch_job > i').removeClass().addClass('fa fa-square-o');
}
} );
},
gosub_live: function(args) {
// show live job status
Debug.trace("Showing live job: " + args.id);
var job = this.find_job(args.id);
var html = '';
this.div.removeClass('loading');
var size = get_inner_window_size();
var col_width = Math.floor( ((size.width * 0.9) - 300) / 4 );
// locate objects
var event = find_object( app.schedule, { id: job.event } ) || {};
var cat = job.category ? find_object( app.categories, { id: job.category } ) : { title: 'n/a' };
var group = event.target ? find_object( app.server_groups, { id: event.target } ) : null;
var plugin = job.plugin ? find_object( app.plugins, { id: job.plugin } ) : { title: 'n/a' };
if (group && event.multiplex) {
group = copy_object(group);
group.multiplex = 1;
}
// new header with watch & abort
var watch_enabled = this.check_watch_enabled(job);
html += '
';
html += 'Live Job Progress';
html += '
Abort Job
';
html += '
Watch Job
';
html += '';
html += '
';
// fieldset header
html += '';
// pies
html += '
';
html += '
';
html += '';
html += '
Job Progress
';
html += '';
// html += '';
// html += '';
html += '
';
html += '
';
html += '';
html += '
Memory Usage
';
html += '';
// html += '';
html += '';
html += '
';
html += '
';
html += '';
html += '
CPU Usage
';
html += '';
// html += '';
html += '';
html += '
';
html += '
';
// live job log tail
var remote_api_url = app.proto + job.hostname + ':' + app.port + config.base_api_uri;
if (config.custom_live_log_socket_url) {
// custom websocket URL for single-master systems behind an LB
remote_api_url = config.custom_live_log_socket_url + config.base_api_uri;
}
else if (!config.web_socket_use_hostnames && app.servers && app.servers[job.hostname] && app.servers[job.hostname].ip) {
// use ip if available, may work better in some setups
remote_api_url = app.proto + app.servers[job.hostname].ip + ':' + app.port + config.base_api_uri;
}
html += '