mirror of
https://github.com/ntop/ntopng.git
synced 2026-05-06 03:45:26 +00:00
468 lines
14 KiB
JavaScript
468 lines
14 KiB
JavaScript
// 2018 - ntop.org
|
|
|
|
var schema_2_label = {};
|
|
var data_2_label = {};
|
|
|
|
function initLabelMaps(_schema_2_label, _data_2_label) {
|
|
schema_2_label = _schema_2_label;
|
|
data_2_label = _data_2_label;
|
|
};
|
|
|
|
function getSerieLabel(schema, serie) {
|
|
var data_label = serie.label;
|
|
var new_label = data_2_label[data_label];
|
|
|
|
if((schema == "top:local_senders") || (schema == "top:local_receivers")) {
|
|
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.device && serie.tags.if_index) { // SNMP interface
|
|
if(serie.tags.if_index != serie.ext_label)
|
|
return serie.ext_label + " (" + serie.tags.if_index + ")";
|
|
else
|
|
return serie.ext_label;
|
|
} else if(serie.tags.device && serie.tags.port) // Flow device
|
|
return serie.tags.port;
|
|
else if(serie.tags.profile)
|
|
return serie.tags.profile;
|
|
} 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;
|
|
}
|
|
|
|
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, series) {
|
|
if(series && series.length && series[0].label) {
|
|
var label = series[0].label;
|
|
|
|
if(label.contains("bytes"))
|
|
return [fbits_from_bytes, bytesToSize];
|
|
else if(label.contains("packets"))
|
|
return [fpackets, formatPackets];
|
|
else if(label.contains("flows"))
|
|
return [formatFlows, formatFlows];
|
|
else if(label.contains("millis"))
|
|
return [fmillis, fmillis];
|
|
}
|
|
|
|
// fallback
|
|
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;
|
|
}
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
function interpolateSerie(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) {
|
|
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 ~60
|
|
var range_params = [
|
|
// max_diff, resolution, x_format, alignment, tick_step
|
|
[15, 1, "%H:%M:%S", 1, 1], // <= 1 min
|
|
[60, 1, "%H:%M:%S", 1, 5], // <= 1 min
|
|
[120, 5, "%H:%M:%S", 10, 10], // <= 5 min
|
|
[300, 5, "%H:%M:%S", 10, 30], // <= 5 min
|
|
[600, 10, "%H:%M:%S", 30, 60], // <= 10 min
|
|
[1200, 30, "%H:%M:%S", 60, 120], // <= 20 min
|
|
[3600, 60, "%H:%M:%S", 60, 300], // <= 1 h
|
|
[5400, 120, "%H:%M:%S", 300, 900], // <= 1.5 h
|
|
[10800, 300, "%H:%M:%S", 300, 900], // <= 3 h
|
|
[21600, 300, "%H:%M:%S", 3600, 1800], // <= 6 h
|
|
[43200, 600, "%H:%M:%S", 3600, 3600], // <= 12 h
|
|
[86400, 600, "%H:%M:%S", 3600, 7200], // <= 1 d
|
|
[604800, 3600, "%Y-%m-%d", 86400, 86400], // <= 7 d
|
|
[1209600, 7200, "%Y-%m-%d", 86400, 172800], // <= 14 d
|
|
[2678400, 21600, "%Y-%m-%d", 86400, 259200], // <= 1 m
|
|
[15768000, 175200, "%Y-%m-%d", 2678400, 1314000], // <= 6 m
|
|
[31622400, 175200, "%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;
|
|
}
|
|
}
|
|
|
|
if(align) {
|
|
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.round(diff_epoch / resolution);
|
|
|
|
// align epoch end wrt params.limit
|
|
params.epoch_end += Math.ceil(diff_epoch / params.limit) * params.limit - diff_epoch;
|
|
|
|
chart.xAxis.tickValues(buildTimeArray(params.epoch_begin, params.epoch_end, tick_step));
|
|
}
|
|
|
|
chart.xAxis.tickFormat(function(d) { return d3.time.format(fmt)(new Date(d*1000)) });
|
|
}
|
|
|
|
// add a new updateStackedChart function
|
|
function attachStackedChartCallback(chart, schema_name, url, chart_id, params) {
|
|
var pending_request = null;
|
|
var d3_sel = d3.select(chart_id);
|
|
var $chart = $(chart_id);
|
|
|
|
//var spinner = $("<img class='chart-loading-spinner' src='" + spinner_url + "'/>");
|
|
var spinner = $('<i class="chart-loading-spinner fa fa-spinner fa-lg fa-spin"></i>');
|
|
$chart.parent().css("position", "relative");
|
|
|
|
var chart_colors = [
|
|
"#69B87F",
|
|
"#94CFA4",
|
|
"#B3DEB6",
|
|
"#E5F1A6",
|
|
"#FFFCC6",
|
|
"#FEDEA5",
|
|
"#FFB97B",
|
|
"#FF8D6D",
|
|
"#E27B85"
|
|
];
|
|
|
|
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).transition().call(chart);
|
|
nv.utils.windowResize(chart.update);
|
|
pending_request = null;
|
|
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) {
|
|
if(typeof localStorage !== "undefined")
|
|
localStorage.setItem("chart_series.disabled." + d.legend_key, (!d.disabled) ? true : false);
|
|
});
|
|
|
|
chart.updateStackedChart = function (tstart, tend, no_spinner) {
|
|
if(pending_request)
|
|
pending_request.abort();
|
|
else if(!no_spinner)
|
|
spinner.appendTo($chart.parent());
|
|
|
|
if(tstart) params.epoch_begin = tstart;
|
|
if(tend) params.epoch_end = tend;
|
|
|
|
fixTimeRange(chart, params);
|
|
|
|
// Load data via ajax
|
|
pending_request = $.get(url, params, function(data) {
|
|
if(!data || !data.series || !checkSeriesConsinstency(schema_name, data.count, data.series)) {
|
|
update_chart_data([]);
|
|
return;
|
|
}
|
|
|
|
// Adapt data
|
|
var res = [];
|
|
var series = data.series;
|
|
var total_serie;
|
|
var color_i = 0;
|
|
|
|
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;
|
|
}
|
|
|
|
res.push({
|
|
key: getSerieLabel(schema_name, series[j]),
|
|
yAxis: 1,
|
|
values: values,
|
|
type: "area",
|
|
color: chart_colors[color_i++],
|
|
legend_key: series[j].label,
|
|
disabled: isLegendDisabled(series[j].label, false),
|
|
});
|
|
}
|
|
|
|
var visual_total = buildTotalSerie(series);
|
|
|
|
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: "Other", // TODO localize
|
|
yAxis: 1,
|
|
values: arrayToNvSerie(other_serie, data.start, data.step),
|
|
type: "area",
|
|
color: chart_colors[color_i++],
|
|
legend_key: "other",
|
|
disabled: isLegendDisabled("other", false),
|
|
});
|
|
}
|
|
} else
|
|
total_serie = visual_total;
|
|
|
|
if(data.additional_series) {
|
|
for(var key in data.additional_series) {
|
|
if(key == "total") {
|
|
// handle manually as "other" above
|
|
continue;
|
|
}
|
|
|
|
var serie_data = interpolateSerie(data.additional_series[key], data.count);
|
|
var values = arrayToNvSerie(serie_data, data.start, data.step);
|
|
|
|
res.push({
|
|
key: capitaliseFirstLetter(key),
|
|
yAxis: 1,
|
|
values: values,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#7E91A0",
|
|
legend_key: key,
|
|
disabled: isLegendDisabled(key, false),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Smoothed serie
|
|
var num_smoothed_points = Math.max(Math.floor(total_serie.length / 5), 3);
|
|
|
|
var smoothed = smooth(total_serie, num_smoothed_points);
|
|
var scale = d3.max(total_serie) / d3.max(smoothed);
|
|
var scaled = $.map(smoothed, function(x) { return x * scale; });
|
|
var aligned = interpolateSerie(scaled, data.count);
|
|
|
|
res.push({
|
|
key: "Trend", // TODO localize
|
|
yAxis: 1,
|
|
values: arrayToNvSerie(aligned, data.start, data.step),
|
|
type: "line",
|
|
classed: "line-animated",
|
|
color: "#62ADF6",
|
|
legend_key: "trend",
|
|
disabled: isLegendDisabled("trend", false),
|
|
});
|
|
|
|
// get the value formatter
|
|
var formatter = getValueFormatter(schema_name, series);
|
|
var value_formatter = formatter[0];
|
|
var tot_formatter = formatter[1];
|
|
chart.yAxis1.tickFormat(value_formatter);
|
|
chart.interactiveLayer.tooltip.valueFormatter(value_formatter);
|
|
|
|
var stats_table = $chart.closest("table").find(".graph-statistics");
|
|
var stats = data.statistics;
|
|
|
|
if(stats) {
|
|
if(stats.average) {
|
|
var values = makeFlatLineValues(data.start, data.step, data.count, stats.average);
|
|
|
|
res.push({
|
|
key: "Avg", // TODO localize
|
|
yAxis: 1,
|
|
values: values,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#AC9DDF",
|
|
legend_key: "avg",
|
|
disabled: isLegendDisabled("avg", true),
|
|
});
|
|
}
|
|
|
|
// fill the stats
|
|
if(stats.total)
|
|
stats_table.find(".graph-val-total").show().find("span").html(tot_formatter(stats.total));
|
|
if(stats.average)
|
|
stats_table.find(".graph-val-average").show().find("span").html(value_formatter(stats.average));
|
|
if(stats.min_val)
|
|
stats_table.find(".graph-val-min").show().find("span").html(value_formatter(stats.min_val) + "@" + (new Date(res[0].values[stats.min_val_idx][0] * 1000)).format("dd/MM/yyyy hh:mm:ss"));
|
|
if(stats.max_val)
|
|
stats_table.find(".graph-val-max").show().find("span").html(value_formatter(stats.max_val) + "@" + (new Date(res[0].values[stats.max_val_idx][0] * 1000)).format("dd/MM/yyyy hh:mm:ss"));
|
|
if(stats["95th_percentile"]) {
|
|
stats_table.find(".graph-val-95percentile").show().find("span").html(value_formatter(stats["95th_percentile"]));
|
|
|
|
var values = makeFlatLineValues(data.start, data.step, data.count, stats["95th_percentile"]);
|
|
|
|
res.push({
|
|
key: "95th Perc", // TODO localize
|
|
yAxis: 1,
|
|
values: values,
|
|
type: "line",
|
|
classed: "line-dashed line-animated",
|
|
color: "#476DFF",
|
|
legend_key: "95perc",
|
|
disabled: isLegendDisabled("95perc", true),
|
|
});
|
|
}
|
|
|
|
// only show if there are visible elements
|
|
if(stats_table.find("td").filter(function(){ return $(this).css("display") != "none"; }).length > 0)
|
|
stats_table.show();
|
|
else
|
|
stats_table.hide();
|
|
} else {
|
|
stats_table.hide();
|
|
}
|
|
|
|
update_chart_data(res);
|
|
}).fail(function(xhr, status, error) {
|
|
console.error("Error while retrieving the timeseries data [" + status + "]: " + error);
|
|
update_chart_data([]);
|
|
});
|
|
}
|
|
}
|