ntopng/httpdocs/js/graph_utils.js
emanuele-f d1ae6b545a Improve last point add logic for active monitoring
Now the point is only added if the current time is contained between the last update and the next
expected update
2020-04-27 10:49:44 +02:00

1442 lines
47 KiB
JavaScript

// 2019 - ntop.org
var schema_2_label = {};
var data_2_label = {};
var graph_i18n = {};
function initLabelMaps(_schema_2_label, _data_2_label, _graph_i18n) {
schema_2_label = _schema_2_label;
data_2_label = _data_2_label;
graph_i18n = _graph_i18n;
};
function getSerieLabel(schema, serie, visualization, serie_index) {
var data_label = serie.label;
var new_label = data_2_label[data_label];
if(visualization && visualization.metrics_labels && visualization.metrics_labels[serie_index])
return visualization.metrics_labels[serie_index];
if(serie.ext_label)
return serie.ext_label;
else if((schema == "top:local_senders") || (schema == "top:local_receivers")) {
if(serie.ext_label)
return serie.ext_label;
else
return serie.tags.host
} else if(schema.startsWith("top:")) { // topk graphs
if(serie.tags.protocol)
return serie.tags.protocol;
else if(serie.tags.category)
return serie.tags.category;
else if(serie.tags.l4proto)
return serie.tags.l4proto;
else if(serie.tags.device && serie.tags.if_index) { // SNMP interface
if(serie.ext_label != "")
return serie.ext_label;
else
return "(" + serie.tags.if_index + ")";
} else if(serie.tags.device && serie.tags.port) // Flow device
return serie.tags.port;
else if(serie.tags.exporter && serie.tags.ifname) // Event exporter
return serie.tags.ifname;
else if(serie.tags.profile)
return serie.tags.profile;
else if(serie.tags.user_script)
return serie.tags.user_script;
else if(serie.tags.command)
return serie.tags.command.substring(4).toUpperCase();
} else if(data_label != "bytes") { // single series
if(serie.tags.protocol)
return serie.tags.protocol + " (" + new_label + ")";
else if(serie.tags.category)
return serie.tags.category + " (" + new_label + ")";
else if(serie.tags.device && serie.tags.if_index) // SNMP interface
return serie.ext_label + " (" + new_label + ")";
else if(serie.tags.device && serie.tags.port) // Flow device
return serie.tags.port + " (" + new_label + ")";
} else {
if(serie.tags.protocol)
return serie.tags.protocol;
else if(serie.tags.category)
return serie.tags.category;
else if(serie.tags.profile)
return serie.tags.profile;
else if(data_label == "bytes") {
if(schema.contains("volume"))
return graph_i18n.traffic_volume;
else
return graph_i18n.traffic;
}
}
if(schema_2_label[schema])
return capitaliseFirstLetter(schema_2_label[schema]);
if(new_label)
return capitaliseFirstLetter(new_label);
// default
return capitaliseFirstLetter(data_label);
}
// Value formatter
function getValueFormatter(schema, metric_type, series, custom_formatter, stats) {
if(series && series.length && series[0].label) {
if(custom_formatter) {
var formatters = [];
if(typeof(custom_formatter) != "object")
custom_formatter = [custom_formatter];
for(var i=0; i<custom_formatter.length; i++) {
// translate function name to actual function
var fn = window[custom_formatter[i]];
if(typeof fn !== "function")
console.error("Cannot find custom value formatter \"" + custom_formatter + "\"");
formatters[i] = fn;
}
return(formatters);
}
var label = series[0].label;
if(label.contains("bytes")) {
if(schema.contains("volume") || schema.contains("memory") || schema.contains("size"))
return [bytesToSize, bytesToSize];
else
return [fbits_from_bytes, bytesToSize];
} else if(label.contains("packets"))
return [fpackets, formatPackets];
else if(label.contains("points"))
return [fpoints, formatPoints];
else if(label.contains("flows")) {
var as_counter = ((metric_type === "counter") && (schema !== "custom:memory_vs_flows_hosts"));
return [as_counter ? fflows : formatValue, formatFlows, as_counter ? fflows : formatFlows];
} else if(label.contains("millis") || label.contains("_ms")) {
return [fmillis, fmillis];
} else if(label.contains("alerts") && (metric_type === "counter")) {
return [falerts, falerts];
} else if(label.contains("percent")) {
return [fpercent, fpercent];
}
}
// fallback
if(stats && (stats.max_val < 1)) {
/* Use the float formatter to avoid having the same 0 value repeated into the scale */
return [ffloat, ffloat];
}
return [fint,fint];
}
function makeFlatLineValues(tstart, tstep, num, data) {
var t = tstart;
var values = [];
for(var i=0; i<num; i++) {
values[i] = [t, data ];
t += tstep;
}
return values;
}
function checkSeriesConsinstency(schema_name, count, series) {
var rv = true;
for(var i=0; i<series.length; i++) {
var data = series[i].data;
if(data.length > count) {
console.error("points mismatch: serie '" + getSerieLabel(schema_name, series[i]) +
"' has " + data.length + " points, expected " + count);
rv = false;
} else if(data.length < count) {
/* upsample */
series[i].data = upsampleSerie(data, count);
}
}
return rv;
}
function upsampleSerie(serie, num_points) {
if(num_points <= serie.length)
return serie;
var res = [];
var intervals = num_points / serie.length;
function lerp(v0, v1, t) {
return (1 - t) * v0 + t * v1;
}
for(var i=0; i<num_points; i++) {
var index = i / intervals;
var prev_i = Math.floor(index);
var next_i = Math.min(Math.ceil(index), serie.length-1);
var t = index % 1; // fractional part
var v = lerp(serie[prev_i], serie[next_i], t);
//console.log(prev_i, next_i, t, ">>", v);
res.push(v);
}
return res.slice(0, num_points);
}
// the stacked total serie
function buildTotalSerie(data_series) {
var series = [];
for(var i=0; i<data_series.length; i++)
series.push(data_series[i].data);
return d3.transpose(series).map(function(x) {
return x.map(function(g) {
return g;
});
}).map(function(x) {return d3.sum(x);});
}
function arrayToNvSerie(serie_data, start, step) {
var values = [];
var t = start;
for(var i=0; i<serie_data.length; i++) {
values[i] = [t, serie_data[i]];
t += step;
}
return values;
}
// computes the difference between visual_total and total_serie
function buildOtherSerie(total_serie, visual_total) {
if(total_serie.length !== visual_total.length) {
console.warn("Total/Visual length mismatch: " + total_serie.length + " vs " + visual_total.length);
return;
}
var res = [];
var max_val = 0;
for(var i=0; i<total_serie.length; i++) {
var value = Math.max(0, total_serie[i] - visual_total[i]);
max_val = Math.max(max_val, value);
res.push(value);
}
if(max_val > 0.1)
return res;
}
function buildTimeArray(start_time, end_time, step) {
var arr = [];
for(var t=start_time; t<end_time; t+=step)
arr.push(t);
return arr;
}
function fixTimeRange(chart, params, align_step, actual_step) {
var diff_epoch = (params.epoch_end - params.epoch_begin);
var frame, align, tick_step, resolution, fmt = "%H:%M:%S";
// must be sorted by ascending max_diff
// max_diff / tick_step indicates the number of ticks, which should be <= 15
// max_diff / resolution indicates the number of actual points, which should be ~300
var range_params = [
// max_diff, resolution, x_format, alignment, tick_step
[15, 1, "%H:%M:%S", 1, 1], // <= 15 sec
[60, 1, "%H:%M:%S", 1, 5], // <= 1 min
[120, 1, "%H:%M:%S", 10, 10], // <= 2 min
[300, 1, "%H:%M:%S", 10, 30], // <= 5 min
[600, 5, "%H:%M:%S", 30, 60], // <= 10 min
[1200, 5, "%H:%M:%S", 60, 120], // <= 20 min
[3600, 10, "%H:%M:%S", 60, 300], // <= 1 h
[5400, 15, "%H:%M", 300, 900], // <= 1.5 h
[10800, 30, "%H:%M", 300, 900], // <= 3 h
[21600, 60, "%H:%M", 3600, 1800], // <= 6 h
[43200, 120, "%H:%M", 3600, 3600], // <= 12 h
[86400, 240, "%H:%M", 3600, 7200], // <= 1 d
[172800, 480, "%a, %H:%M", 3600, 14400], // <= 2 d
[604800, 1800, "%Y-%m-%d", 86400, 86400], // <= 7 d
[1209600, 3600, "%Y-%m-%d", 86400, 172800], // <= 14 d
[2678400, 7200, "%Y-%m-%d", 86400, 259200], // <= 1 m
[15768000, 43200, "%Y-%m-%d", 2678400, 1314000], // <= 6 m
[31622400, 86400, "%Y-%m-%d", 2678400, 2678400], // <= 1 y
];
for(var i=0; i<range_params.length; i++) {
var range = range_params[i];
if(diff_epoch <= range[0]) {
frame = range[0];
resolution = range[1];
fmt = range[2];
align = range[3];
tick_step = range[4];
break;
}
}
resolution = Math.max(actual_step, resolution);
if(align) {
align = (align_step && (frame != 86400) /* do not align daily traffic to avoid jumping to other RRA */) ? Math.max(align, align_step) : 1;
params.epoch_begin -= params.epoch_begin % align;
params.epoch_end -= params.epoch_end % align;
diff_epoch = (params.epoch_end - params.epoch_begin);
params.limit = Math.ceil(diff_epoch / resolution);
// align epoch end wrt params.limit
params.epoch_end += Math.ceil(diff_epoch / params.limit) * params.limit - diff_epoch;
chart.align = align;
chart.tick_step = tick_step;
} else
chart.tick_step = null;
chart.x_fmt = fmt;
}
function findActualStep(raw_step, tstart) {
if(typeof supported_steps === "object") {
if(supported_steps[raw_step]) {
var retention = supported_steps[raw_step].retention;
if(retention) {
var now_ts = Date.now() / 1000;
var delta = now_ts - tstart;
for(var i=0; i<retention.length; i++) {
var partial = raw_step * retention[i].aggregation_dp;
var tframe = partial * retention[i].retention_dp;
delta -= tframe;
if(delta <= 0)
return partial;
}
}
}
}
return raw_step;
}
function has_initial_zoom() {
return typeof parseQuery(window.location.search).epoch_begin !== "undefined";
}
var current_zoom_level = (history.state) ? (history.state.zoom_level) : 0;
function canCompareBackwards(epoch_begin, epoch_end) {
var jump_duration = $("#btn-jump-time-ahead").data("duration");
var current_duration = epoch_end - epoch_begin;
return(jump_duration == current_duration);
}
function fixJumpButtons(epoch_begin, epoch_end) {
var duration = $("#btn-jump-time-ahead").data("duration");
if((epoch_end + duration)*1000 > $.now())
$("#btn-jump-time-ahead").addClass("disabled");
else
$("#btn-jump-time-ahead").removeClass("disabled");
}
function showQuerySlow() {
$("#query-slow-alert").show();
}
function hideQuerySlow() {
$("#query-slow-alert").hide();
}
function chart_data_sum(series) {
return(series.reduce(function(acc, x) {
return(acc + x.values.reduce(
function(acc, pt) {
return(acc + pt[1] || 0);
}, 0)
)
}, 0));
}
function redrawExtraLines(chart, chart_id, extra_lines) {
/* Remove the previous extra lines */
d3.selectAll(chart_id + " line.extra-line").remove();
if(extra_lines.length > 0) {
var xValueScale = chart.xAxis.scale();
var yValueScale = chart.yAxis1.scale();
var g = d3.select(chart_id + " .stack1Wrap");
for(var i=0; i<extra_lines.length; i++) {
var d = extra_lines[i];
g.append("line")
.style("stroke", "#FF5B56")
.style("stroke-width", "2.5px")
.attr("x1", xValueScale(d[0]))
.attr("y1", yValueScale(d[2]))
.attr("x2", xValueScale(d[1]))
.attr("y2", yValueScale(d[3]))
.attr("class", "extra-line")
}
}
}
// add a new updateStackedChart function
function attachStackedChartCallback(chart, schema_name, chart_id, zoom_reset_id, params, step,
metric_type, align_step, show_all_smooth, initial_range, ts_table_shown) {
var pending_chart_request = null;
var pending_table_request = null;
var d3_sel = d3.select(chart_id);
var $chart = $(chart_id);
var $zoom_reset = $(zoom_reset_id);
var $graph_zoom = $("#graph_zoom");
var max_interval = findActualStep(step, params.epoch_begin) * 8;
var initial_interval = (params.epoch_end - params.epoch_begin);
var is_max_zoom = (initial_interval <= max_interval);
var url = http_prefix + "/lua/rest/get/timeseries/ts.lua";
var first_load = true;
var first_time_loaded = true;
var manual_trigger_extra_series = {}; // keeps track of series manually shown/hidden by the user
var datetime_format = "dd/MM/yyyy hh:mm:ss";
var max_cmp_over_total_ratio = 3; // if the comparison serie max value is too big compared to the actual chart series, hide it
var max_line_over_total_ratio = 10; // if the extra line series max value is too big compared to the actual chart series, hide them
var query_timer = null;
var seconds_before_query_slow = 6;
var query_completed = 0;
var query_was_aborted = false;
let last_known_t = null; // only set if show_unreachable is set
const visualization = chart.visualization_options || {};
chart.is_zoomed = ((current_zoom_level > 0) || has_initial_zoom());
/* Extra lines to draw into the chart. Each item is in the format [x_start, x_end, y_start, y_end] */
let extra_lines = [];
let unreachable_timestamps = {};
//var spinner = $("<img class='chart-loading-spinner' src='" + spinner_url + "'/>");
var spinner = $('<i class="chart-loading-spinner fas fa-spinner fa-lg fa-spin"></i>');
$chart.parent().css("position", "relative");
var chart_colors_full = [
"#69B87F",
"#94CFA4",
"#B3DEB6",
"#E5F1A6",
"#FFFCC6",
"#FEDEA5",
"#FFB97B",
"#FF8D6D",
"#E27B85"
];
var chart_colors_min = ["#7CC28F", "#FCD384", "#FD977B"];
var split_directions_colors = ["#69B87F", "#FF7C00", "#FF4700"];
/* This is used to show the "unreachable" label when the chart "show_unreachable"
* options is set. See the extra_lines computation below. */
function format_unreachable(formatter) {
return function(y, d) {
if(d && unreachable_timestamps[d[0]])
return(i18n.unreachable_host);
// Not unreachable, use the provided formatter
return(formatter(y));
}
}
/* The default number of y points */
var num_ticks_y1 = null;
var num_ticks_y2 = null;
var domain_y1 = null;
var domain_y2 = null;
var first_run = true;
var update_chart_data = function(new_data) {
/* reset chart data so that the next transition animation will be gracefull */
d3_sel.datum([]).call(chart);
d3_sel.datum(new_data);
/* This additional refresh is needed to determine the yticks
* and domain, needed below.
* NOTE: calling transition().duration(500) is important to properly refresh
* the tooltip position. */
d3_sel.transition().duration(500).call(chart);
if(first_run) {
num_ticks_y1 = chart.yAxis1.ticks();
num_ticks_y2 = chart.yAxis2.ticks();
domain_y1 = chart.yDomain1();
domain_y2 = chart.yDomain2();
first_run = false;
}
if(metric_type === "gauge") {
var cur_domain_y1 = chart.yAxis1.scale().domain();
var cur_domain_y2 = chart.yAxis2.scale().domain();
cur_domain_y1 = cur_domain_y1[1] - cur_domain_y1[0];
cur_domain_y2 = cur_domain_y2[1] - cur_domain_y2[0];
/* If there are not enough points available, reduce the number of
* ticks to avoid repeated ticks with same integer value.
* Other solutions (documented in https://stackoverflow.com/questions/21075245/nvd3-prevent-repeated-values-on-y-axis)
* are not easily applicable in this case.
*
* NOTE: the problem should not occur when using ffloat
*/
if(chart.yAxis1.tickFormat() != ffloat)
chart.yAxis1.ticks(Math.min(cur_domain_y1, num_ticks_y1));
if(chart.yAxis2.tickFormat() != ffloat)
chart.yAxis2.ticks(Math.min(cur_domain_y2, num_ticks_y2));
}
var y1_sum = chart_data_sum(new_data.filter(function(x) { return(x.yAxis == 1); }))
var y2_sum = chart_data_sum(new_data.filter(function(x) { return(x.yAxis == 2); }))
/* Fix negative ydomain values appearing when dataset is empty */
if(y1_sum == 0)
chart.yDomain1([0, 1]);
else
chart.yDomain1(domain_y1);
if(y2_sum == 0)
chart.yDomain2([0, 1]);
else
chart.yDomain2(domain_y2);
/* Refresh the chart */
d3_sel.call(chart);
nv.utils.windowResize(function() {
chart.update();
redrawExtraLines(chart, chart_id, extra_lines);
})
redrawExtraLines(chart, chart_id, extra_lines);
spinner.remove();
}
function isLegendDisabled(key, default_val) {
if(typeof localStorage !== "undefined") {
var val = localStorage.getItem("chart_series.disabled." + key);
if(val != null)
return(val === "true");
}
return default_val;
}
chart.legend.dispatch.on('legendClick', function(d,i) {
manual_trigger_extra_series[d.legend_key] = true;
if(typeof localStorage !== "undefined")
localStorage.setItem("chart_series.disabled." + d.legend_key, (!d.disabled) ? true : false);
});
chart.dispatch.on("zoom", function(e) {
var cur_zoom = [params.epoch_begin, params.epoch_end];
var t_start = Math.floor(e.xDomain[0]);
var t_end = Math.ceil(e.xDomain[1]);
var old_zoomed = chart.is_zoomed;
var is_user_zoom = (typeof e.is_user_zoom !== "undefined") ? e.is_user_zoom : true;
chart.is_zoomed = true;
if(chart.updateStackedChart(t_start, t_end, false, is_user_zoom)) {
if(is_user_zoom || e.push_state) {
//console.log("zoom IN!");
current_zoom_level += 1;
var url = getHistoryParameters({epoch_begin: t_start, epoch_end: t_end});
history.pushState({zoom_level: current_zoom_level, range: [t_start, t_end]}, "", url);
}
chart.fixChartButtons();
} else
chart.is_zoomed = old_zoomed;
});
function updateZoom(zoom, is_user_zoom, force) {
var t_start = zoom[0];
var t_end = zoom[1];
chart.updateStackedChart(t_start, t_end, false, is_user_zoom, null, force);
chart.fixChartButtons();
}
chart.zoom_in = function() {
var cur_interval = params.epoch_end - params.epoch_begin;
if(cur_interval > 60) {
var delta = cur_interval/4;
$("#period_begin").datetimepicker("date", new Date((params.epoch_begin + delta) * 1000));
$("#period_end").datetimepicker("date", new Date((params.epoch_end - delta) * 1000));
updateChartFromPickers();
}
}
chart.zoom_out = function() {
var cur_interval = params.epoch_end - params.epoch_begin;
//if(current_zoom_level) {
// Zoom out from history
//console.log("zoom OUT");
//history.back();
//} else {
// Zoom out with fixed interval
//var delta = zoom_out_value;
var delta = cur_interval/2;
//if((params.epoch_end + delta)*1000 <= $.now())
//delta /= 2;
$("#period_begin").datetimepicker("date", new Date((params.epoch_begin - delta) * 1000));
$("#period_end").datetimepicker("date", new Date((params.epoch_end + delta) * 1000));
updateChartFromPickers();
//}
}
$chart.on('dblclick', function(event) {
if($(event.target).hasClass("nv-legend-text"))
// legend was double-clicked, keep the original behavior
return;
chart.zoom_out();
});
$zoom_reset.on("click", function() {
if(current_zoom_level) {
//console.log("zoom RESET");
history.go(-current_zoom_level);
}
});
window.addEventListener('popstate', function(e) {
var zoom = initial_range;
//console.log("popstate: ", e.state);
if(e.state) {
zoom = e.state.range;
current_zoom_level = e.state.zoom_level;
} else
current_zoom_level = 0;
updateZoom(zoom, true, true /* force */);
});
chart.fixChartButtons = function() {
if((current_zoom_level > 0) || has_initial_zoom()) {
$graph_zoom.find(".btn-warning:not(.custom-zoom-btn)")
.addClass("initial-zoom-sel")
.removeClass("btn-warning");
$graph_zoom.find(".custom-zoom-btn").css("visibility", "visible");
var zoom_link = $graph_zoom.find(".custom-zoom-btn");
var link = zoom_link.val().replace(/&epoch_begin=.*/, "");
link += "&epoch_begin=" + params.epoch_begin + "&epoch_end=" + params.epoch_end;
zoom_link.val(link);
} else {
$graph_zoom.find(".initial-zoom-sel")
.addClass("btn-warning");
$graph_zoom.find(".custom-zoom-btn").css("visibility", "hidden");
chart.is_zoomed = false;
}
fixJumpButtons(params.epoch_begin, params.epoch_end);
if(current_zoom_level > 0)
$zoom_reset.show();
else
$zoom_reset.hide();
}
function checkQueryCompleted() {
var flows_dt = $("#chart1-flows");
var wait_num_queries = (ts_table_shown && ($("#chart1-flows").css("display") !== "none")) ? 2 : 1;
query_completed += 1;
if(query_completed >= wait_num_queries) {
if(query_timer) {
clearInterval(query_timer);
query_timer = null;
}
hideQuerySlow();
}
}
chart.queryWasAborted = function() {
return query_was_aborted;
}
chart.abortQuery = function() {
query_was_aborted = true;
if(pending_chart_request) {
pending_chart_request.abort();
chart.noData(i18n.query_was_aborted);
update_chart_data([]);
}
if(pending_table_request)
pending_table_request.abort();
if(query_timer) {
clearInterval(query_timer);
query_timer = null;
}
hideQuerySlow();
}
chart.tableRequestCompleted = function() {
checkQueryCompleted();
pending_table_request = null;
}
chart.getDataUrl = function() {
var data_params = jQuery.extend({}, params);
delete data_params.zoom;
delete data_params.ts_compare;
data_params.extended = 1; /* with extended timestamps */
return url + "?" + $.param(data_params, true);
}
var old_start, old_end, old_interval;
/* Returns false if zoom update is rejected. */
chart.updateStackedChart = function (tstart, tend, no_spinner, is_user_zoom, on_load_callback, force_update) {
if(tstart) params.epoch_begin = tstart;
if(tend) params.epoch_end = tend;
const series_formatted_labels = {};
const now = Date.now() / 1000;
var cur_interval = (params.epoch_end - params.epoch_begin);
var actual_step = findActualStep(step, params.epoch_begin);
max_interval = actual_step * 6; /* host traffic 30 min */
if(cur_interval < max_interval) {
if((is_max_zoom && (cur_interval < old_interval)) && !force_update) {
old_interval = cur_interval;
return false;
}
if(!force_update) {
/* Ensure that a minimal number of points is available */
var epoch = params.epoch_begin + (params.epoch_end - params.epoch_begin) / 2;
var new_end = Math.floor(epoch + max_interval / 2);
if(new_end >= now) {
/* Only expand on the left side of the interval */
params.epoch_begin = params.epoch_end - max_interval;
} else {
params.epoch_begin = Math.floor(epoch - max_interval / 2);
params.epoch_end = Math.floor(epoch + max_interval / 2);
}
is_max_zoom = true;
chart.zoomType(null); // disable zoom
}
} else if (cur_interval > max_interval) {
is_max_zoom = false;
chart.zoomType('x'); // enable zoom
}
old_interval = cur_interval;
if(!first_load || has_initial_zoom() || force_update)
align_step = null;
fixTimeRange(chart, params, align_step, actual_step);
if(first_load)
initial_range = [params.epoch_begin, params.epoch_end];
if((old_start == params.epoch_begin) && (old_end == params.epoch_end) && (!force_update))
return false;
old_start = params.epoch_begin;
old_end = params.epoch_end;
if(pending_table_request)
pending_table_request.abort();
if(pending_chart_request)
pending_chart_request.abort();
else if(!no_spinner)
spinner.appendTo($chart.parent());
// Update datetime selection
$("#period_begin").datetimepicker("date", new Date(params.epoch_begin * 1000));
$("#period_end").datetimepicker("date", new Date(Math.min(params.epoch_end * 1000, $.now())));
if(query_timer)
clearInterval(query_timer);
query_timer = setInterval(showQuerySlow, seconds_before_query_slow * 1000);
query_completed = 0;
query_was_aborted = false;
chart.noData(i18n.no_data_available);
hideQuerySlow();
var req_params = $.extend({}, params);
// skip past period comparison if a custom interval is selected
if(!canCompareBackwards(req_params.epoch_begin, req_params.epoch_end))
delete req_params.ts_compare;
/* Disable the null data filling only for the charts which support the
* "unreachable" status (unreachable reported as a 0 value instead of null). */
if(visualization.show_unreachable)
req_params.no_fill = 1;
// Load data via ajax
pending_chart_request = $.get(url, req_params, function(data) {
if(data && data.error)
chart.noData(data.error);
if(!data || !data.series || !data.series.length || !checkSeriesConsinstency(schema_name, data.count, data.series)) {
update_chart_data([]);
return;
}
// Fix x axis
var tick_step = Math.ceil(chart.tick_step / data.step) * data.step;
chart.xAxis.tickValues(buildTimeArray(data.start, data.start + data.count * data.step, tick_step));
chart.xAxis.tickFormat(function(d) { return d3.time.format(chart.x_fmt)(new Date(d*1000)) });
// Adapt data
var res = [];
var series = data.series;
var total_serie;
var color_i = 0;
var chart_colors = (series.length <= chart_colors_min.length) ? chart_colors_min : chart_colors_full;
for(var j=0; j<series.length; j++) {
var values = [];
var serie_data = series[j].data;
var t = data.start;
for(var i=0; i<serie_data.length; i++) {
values[i] = [t, serie_data[i] ];
t += data.step;
}
var label = getSerieLabel(schema_name, series[j], visualization, j);
var legend_key = schema_name + ":" + label;
chart.current_step = data.step;
let serie_type = series[j].type;
let serie_color = chart_colors[color_i++]
if(!serie_type) {
if(visualization.split_directions) {
/* RX and TX directions are splitted, drow the second serie
* (TX) as a line */
serie_type = (j == 0) ? "area" : "line";
serie_color = split_directions_colors[j] || serie_color;
} else
serie_type = "area";
}
series_formatted_labels[j] = label;
res.push({
key: label,
yAxis: series[j].axis || 1,
values: values,
type: serie_type,
color: serie_color,
legend_key: legend_key,
disabled: isLegendDisabled(legend_key, false),
});
}
var visual_total = buildTotalSerie(series);
var has_full_data = false;
if(data.additional_series && data.additional_series.total) {
total_serie = data.additional_series.total;
/* Total -> Other */
var other_serie = buildOtherSerie(total_serie, visual_total);
if(other_serie) {
res.push({
key: graph_i18n.other,
yAxis: 1,
values: arrayToNvSerie(other_serie, data.start, data.step),
type: "area",
color: chart_colors[color_i++],
legend_key: "other",
disabled: isLegendDisabled("other", false),
});
has_full_data = true;
}
} else {
total_serie = visual_total;
has_full_data = !schema_name.startsWith("top:");
}
var past_serie = null;
if(data.additional_series) {
for(var key in data.additional_series) {
if(key == "total") {
// handle manually as "other" above
continue;
}
var serie_data = upsampleSerie(data.additional_series[key], data.count);
var ratio_over_total = d3.max(serie_data) / d3.max(visual_total);
var values = arrayToNvSerie(serie_data, data.start, data.step);
var is_disabled = isLegendDisabled(key, false);
past_serie = serie_data; // TODO: more reliable way to determine past serie
/* Hide comparison serie at first load if it's too high */
if((first_time_loaded || !manual_trigger_extra_series[key]) && (ratio_over_total > max_cmp_over_total_ratio))
is_disabled = true;
res.push({
key: capitaliseFirstLetter(key),
yAxis: 1,
values: values,
type: "line",
classed: "line-dashed line-animated",
color: "#7E91A0",
legend_key: key,
disabled: is_disabled,
});
}
}
/* Extra horizontal series */
if(visualization && visualization.extra_series) {
for(var i=0; i<visualization.extra_series.length; i++) {
var serie = visualization.extra_series[i];
if(!serie.label) {
console.warn("Missing extra_series label");
continue;
}
if(!serie.value) {
console.warn("Missing extra_series value");
continue;
}
var ratio_over_total = serie.value / d3.max(visual_total);
var is_disabled = isLegendDisabled(serie.label, false);
/* Hide the line serie at first load if it's too high */
if((first_time_loaded || !manual_trigger_extra_series[serie.label]) && (ratio_over_total > max_line_over_total_ratio))
is_disabled = true;
res.push({
key: serie.label,
yAxis: serie.axis || 1,
values: arrayToNvSerie(upsampleSerie([serie.value], data.count), data.start, data.step),
type: serie.type || "line",
color: serie.color || "red",
classed: serie.class,
legend_key: serie.label,
disabled: is_disabled,
});
}
}
if(!data.no_trend && has_full_data && (total_serie.length >= 3)) {
// Smoothed serie
/* num_smoothed_points determines the window size to use while computing rolling functions */
var num_smoothed_points = Math.min(Math.max(Math.floor(total_serie.length / 5), 3), 12);
var smooth_functions = {
trend: [graph_i18n.trend, "#62ADF6", smooth, num_smoothed_points],
ema: ["EMA", "#F96BFF", exponentialMovingAverageArray, {periods: num_smoothed_points}],
sma: ["SMA", "#A900FF", simpleMovingAverageArray, {periods: num_smoothed_points}],
rsi: ["RSI cur vs past", "#00FF5D", relativeStrengthIndexArray, {periods: num_smoothed_points}],
}
function add_smoothed_serie(fn_to_use) {
var options = smooth_functions[fn_to_use];
var smoothed;
if(fn_to_use == "rsi") {
if(!past_serie)
return;
var delta_serie = [];
for(var i=0; i<total_serie.length; i++) {
delta_serie[i] = total_serie[i] - past_serie[i];
}
smoothed = options[2](delta_serie, options[3]);
} else
smoothed = options[2](total_serie, options[3]);
// remove the first point as it's used as the base window in the rolling functions
if(smoothed[0])
delete smoothed[0];
var max_val = d3.max(smoothed);
if(max_val > 0) {
var aligned;
if((fn_to_use != "ema") && (fn_to_use != "sma") && (fn_to_use != "rsi")) {
var scale = d3.max(total_serie) / max_val;
var scaled = $.map(smoothed, function(x) { return x * scale; });
aligned = upsampleSerie(scaled, data.count);
} else {
var remaining = (data.count - smoothed.length);
var to_fill = remaining < num_smoothed_points ? remaining : num_smoothed_points;
/* Fill the initial buffering space */
for(var i=0; i<to_fill; i++)
smoothed.splice(0, 0, smoothed[0]);
aligned = upsampleSerie(smoothed, data.count);
}
if(fn_to_use == "rsi")
chart.yDomainRatioY2(1.0);
res.push({
key: options[0],
yAxis: (fn_to_use != "rsi") ? 1 : 2,
values: arrayToNvSerie(aligned, data.start, data.step),
type: "line",
classed: "line-animated",
color: options[1],
legend_key: fn_to_use,
disabled: isLegendDisabled(fn_to_use, false),
});
}
}
if(show_all_smooth) {
for(fn_to_use in smooth_functions)
add_smoothed_serie(fn_to_use);
} else
add_smoothed_serie("trend");
}
/* Add extra lines. These are different from the extra series as
* they are simple lines, so they are not bound to an axis. */
extra_lines = [];
if((visualization.show_unreachable) && (res.length > 0)) {
var ref_serie = res[0].values;
let tok = ref_serie[0][0];
let was_unreachable = false;
unreachable_timestamps = {};
for(var i=0; i<ref_serie.length; i++) {
const is_unreachable = (ref_serie[i][1] === 0);
const tval = ref_serie[i][0];
if((ref_serie[i][1] == ref_serie[i][1]))
/* The most recent time for non NaN values */
last_known_t = tval;
if(!is_unreachable) {
if(was_unreachable)
extra_lines.push([tok, tval, 0, 0]);
tok = tval;
was_unreachable = false;
} else {
/* Change the reference serie point to null to fix interpolation issues */
ref_serie[i][1] = null;
unreachable_timestamps[tval] = true;
was_unreachable = true;
}
}
if(was_unreachable) {
const tlast = ref_serie[ref_serie.length - 1][0];
if(tlast != tok)
extra_lines.push([tok, tlast, 0, 0]);
}
}
// get the value formatter
var formatter1 = getValueFormatter(schema_name, metric_type, series.filter(function(d) { return(d.axis != 2); }), visualization.value_formatter, data.statistics);
var value_formatter = formatter1[0];
var tot_formatter = formatter1[1] || value_formatter;
var stats_formatter = formatter1[2] || value_formatter;
chart.yAxis1.tickFormat(value_formatter);
chart.yAxis1_formatter = visualization.show_unreachable ? format_unreachable(value_formatter) : value_formatter;
var second_axis_series = series.filter(function(d) { return(d.axis == 2); });
var formatter2 = getValueFormatter(schema_name, metric_type, second_axis_series, visualization.value_formatter2 || visualization.value_formatter, data.statistics);
var value_formatter2 = formatter2[0];
chart.yAxis2.tickFormat(value_formatter2);
chart.yAxis2_formatter = value_formatter2;
var stats_table = $("#ts-chart-stats");
var stats = data.statistics;
if(stats) {
if(stats.average) {
var values = makeFlatLineValues(data.start, data.step, data.count, stats.average);
res.push({
key: graph_i18n.avg,
yAxis: 1,
values: values,
type: "line",
classed: "line-dashed line-animated",
color: "#AC9DDF",
legend_key: "avg",
disabled: isLegendDisabled("avg", true),
});
}
var total_cell = stats_table.find(".graph-val-total");
var average_cell = stats_table.find(".graph-val-average");
var min_cell = stats_table.find(".graph-val-min");
var max_cell = stats_table.find(".graph-val-max");
var perc_cell = stats_table.find(".graph-val-95percentile");
// fill the stats
if(stats.total || total_cell.is(':visible'))
total_cell.show().find("span").html(tot_formatter(stats.total));
if(stats.average || average_cell.is(':visible'))
average_cell.show().find("span").html(stats_formatter(stats.average));
if((stats.min_val || min_cell.is(':visible')) && res[0].values[stats.min_val_idx])
min_cell.show().find("span").html(stats_formatter(stats.min_val) + " @ " + (new Date(res[0].values[stats.min_val_idx][0] * 1000)).format(datetime_format));
if((stats.max_val || max_cell.is(':visible')) && res[0].values[stats.max_val_idx])
max_cell.show().find("span").html(stats_formatter(stats.max_val) + " @ " + (new Date(res[0].values[stats.max_val_idx][0] * 1000)).format(datetime_format));
if(stats["95th_percentile"] || perc_cell.is(':visible')) {
let perc_val = "";
if(visualization.split_directions && stats.by_serie) {
const values = [];
for(var i=0; i<series.length; i++) {
if(stats.by_serie[i])
values.push(stats_formatter(stats.by_serie[i]["95th_percentile"]) + " [" + series_formatted_labels[i] + "]");
}
perc_val = values.join(", ");
} else
perc_val = stats_formatter(stats["95th_percentile"]);
if(perc_val)
perc_cell.show().find("span").html(perc_val);
if(!visualization.split_directions) {
/* When directions are split, hide the total 95th percentile */
var values = makeFlatLineValues(data.start, data.step, data.count, stats["95th_percentile"]);
res.push({
key: graph_i18n["95_perc"],
yAxis: 1,
values: values,
type: "line",
classed: "line-dashed line-animated",
color: "#476DFF",
legend_key: "95perc",
disabled: isLegendDisabled("95perc", true),
});
}
}
// check if there are visible elements
//if(stats_table.find("td").filter(function(){ return $(this).css("display") != "none"; }).length > 0)
}
stats_table.show();
if(visualization.show_unreachable && last_known_t &&
(last_known_t + data.step > now) && (now < last_known_t + 2*data.step)) {
/* For the active monitoring chart, we show an additional point with the
* last value and the now timestamp as requested for
* https://github.com/ntop/ntopng/issues/3822 */
for(var j=0; j<res.length; j++) {
const serie = res[j].values;
if(serie.length > 0)
serie[serie.length] = [now, serie[serie.length - 1][1]];
}
}
var enabled_series = res.filter(function(d) { return(d.disabled !== true); });
if(second_axis_series.length > 0 || enabled_series.length == 0) {
// Enable all the series
for(var i=0; i<res.length; i++)
res[i].disabled = false;
}
if(second_axis_series.length > 0) {
// Don't allow series toggle by disabling legend clicks
chart.legend.updateState(false);
}
update_chart_data(res);
first_time_loaded = false;
if(data.source_aggregation)
$("#data-aggr-dropdown > button > span:first").html(data.source_aggregation);
}).fail(function(xhr, status, error) {
if (xhr.statusText =='abort') {
return;
}
console.error("Error while retrieving the timeseries data [" + status + "]: " + error);
chart.noData(error);
update_chart_data([]);
}).always(function(data, status, xhr) {
checkQueryCompleted();
pending_chart_request = null;
});
if(first_load) {
first_load = false;
/* Wait for page load because datatable is not instantiated yet right now */
$(function() {
var flows_dt = $("#chart1-flows").data("datatable");
if(flows_dt)
pending_table_request = flows_dt.pendingRequest;
});
} else {
var flows_dt = $("#chart1-flows");
/* Reload datatable */
if(ts_table_shown) {
/* note: flows_dt.data("datatable") will change after this call */
updateGraphsTableView(null, params);
if($("#chart1-flows").css("display") !== "none")
pending_table_request = flows_dt.data("datatable").pendingRequest;
}
}
if(typeof on_load_callback === "function")
on_load_callback(chart);
return true;
}
}
var graph_old_view = null;
var graph_old_has_nindex = null;
var graph_old_nindex_query = null;
function tsQueryToTags(ts_query) {
return ts_query.split(",").
reduce(function(params, value) {
var pos = value.indexOf(":");
if(pos != -1) {
var k = value.slice(0, pos);
var v = value.slice(pos+1);
params[k] = v;
}
return params;
}, {});
}
/* Hide or show the timeseries table items based on the current time range */
function recheckGraphTableEntries() {
var table_view = graph_table_views;
var tdiff = (graph_params.epoch_end - graph_params.epoch_begin);
var reset_selection = false;
$("#chart1-flows").show();
$("#graphs-table-selector").show();
for(view_id in table_view) {
var view = table_view[view_id];
var elem = $("#" + view.html_id);
if(tdiff <= view.min_step) {
if(graph_old_view.id === view_id)
reset_selection = true;
elem.hide();
} else
elem.show();
}
/* Hide/show the headers */
var items_ul = $("#graphs-table-active-view").closest(".btn-group").find("ul:first");
items_ul.find("li.dropdown-header").each(function(idx,e) {
var next_item = $(e).nextAll("li").filter(function(idx,e) {
return(($(e).css("display") !== "none") || (!$(e).attr("data-view-id")));
}).first();
var divider = $(e).nextAll(".divider").first();
if(!next_item.attr("data-view-id")) {
$(e).hide();
divider.hide();
} else {
$(e).show();
divider.show();
}
});
if(reset_selection) {
/* Select the first available view */
var first_view = items_ul.find("li[data-view-id]").filter(function(idx,e) {
return($(e).css("display") !== "none");
}).first();
if(first_view.length)
setActiveGraphsTableView(first_view.attr("data-view-id"));
else {
$("#chart1-flows").hide();
$("#graphs-table-selector").hide();
}
return false;
}
return true;
}
function updateGraphsTableView(view, graph_params, has_nindex, nindex_query, per_page) {
if(view)
graph_old_view = view;
if(!recheckGraphTableEntries(graph_params)) {
/* handled by setActiveGraphsTableView */
return;
}
if(view) {
graph_old_has_nindex = has_nindex;
graph_old_nindex_query = nindex_query;
} else {
view = graph_old_view;
has_nindex = graph_old_has_nindex;
nindex_query = graph_old_nindex_query;
}
var graph_table = $("#chart1-flows");
nindex_query = nindex_query + "&begin_time_clause=" + graph_params.epoch_begin + "&end_time_clause=" + graph_params.epoch_end;
var nindex_buttons = "";
var params_obj = tsQueryToTags(graph_params.ts_query);
// TODO localize
/* Hide IP version selector when a host is selected */
if(!params_obj.host) {
nindex_buttons += '<div class="btn-group"><button class="btn btn-link dropdown-toggle" data-toggle="dropdown">';
nindex_buttons += "IP Version";
nindex_buttons += '<span class="caret"></span></button><ul class="dropdown-menu" role="menu">';
nindex_buttons += '<li><a class="dropdown-item" href="#" onclick="return onGraphMenuClick(null, 4)">4</a></li>';
nindex_buttons += '<li><a class="dropdown-item" href="#" onclick="return onGraphMenuClick(null, 6)">6</a></li>';
nindex_buttons += '</span></div>';
}
nindex_buttons += '<div class="btn-group pull-right"><button class="btn btn-link dropdown-toggle" data-toggle="dropdown">';
nindex_buttons += "Explorer";
nindex_buttons += '<span class="caret"></span></button><ul class="dropdown-menu" role="menu">';
nindex_buttons += '<li><a class="dropdown-item" href="'+ http_prefix +'/lua/pro/nindex_topk.lua'+ nindex_query +'">Top-K</a></li>';
nindex_buttons += '<li><a class="dropdown-item" href="'+ http_prefix +'/lua/pro/nindex.lua'+ nindex_query +'">Flows</a></li>';
nindex_buttons += '</span></div>';
if(view.columns) {
var url = http_prefix + (view.nindex_view ? "/lua/pro/get_nindex_flows.lua" : "/lua/pro/get_ts_table.lua");
var columns = view.columns.map(function(col) {
return {
title: col[1],
field: col[0],
css: {
textAlign: col[2], width: col[3],//
},
hidden: col[4] ? true : false,
};
});
columns.push({
title: i18n.actions,
field: "drilldown",
css: {width: "1%", "white-space": "nowrap", "text-align": "center"},
});
var old_dt = graph_table.data("datatable");
if(old_dt && old_dt.pendingRequest)
old_dt.pendingRequest.abort();
/* Force reinstantiation */
graph_table.removeData('datatable');
graph_table.html("");
graph_table.datatable({
title: "",
url: url,
perPage: per_page,
noResultsMessage: function() {
if(ts_chart.queryWasAborted())
return i18n.query_was_aborted;
else
return i18n.no_results_found;
},
post: function() {
var params = $.extend({}, graph_params);
delete params.ts_compare;
delete params.initial_point;
params.limit = 1; // TODO make specific query
// TODO change topk
// TODO disable statistics
params.detail_view = view.id;
return params;
},
loadingYOffset: 40,
columns: columns,
buttons: view.nindex_view ? [nindex_buttons, ] : [],
tableCallback: function() {
var data = this.resultset;
ts_chart.tableRequestCompleted();
if(!data) {
// error
return;
}
/* The user changed page */
if(data.currentPage > 1)
graph_table.data("has_interaction", true);
var stats_div = $("#chart1-flows-stats");
var has_drilldown = (data && data.data.some(function(row) { return row.drilldown; }));
/* Remove the drilldown column if no drilldown is available */
if(!has_drilldown)
$("table td:last-child, th:last-child", graph_table).remove();
if(data && data.stats && data.stats.loading_time) {
$("#flows-load-time").html(data.stats.loading_time);
$("#flows-processed-records").html(data.stats.num_records_processed);
stats_div.show();
} else
stats_div.hide();
}, rowCallback: function(row, row_data) {
if((typeof row_data.tags === "object") && (
(params_obj.category && (row_data.tags.category === params_obj.category)) ||
(params_obj.protocol && (row_data.tags.protocol === params_obj.protocol))
)) {
/* Highlight the row */
row.addClass("info");
}
return row;
}
});
}
}