summaryrefslogtreecommitdiff
path: root/sflow-rt/app
diff options
context:
space:
mode:
Diffstat (limited to 'sflow-rt/app')
-rw-r--r--sflow-rt/app/browse-metrics/LICENSE21
-rw-r--r--sflow-rt/app/browse-metrics/README.md10
-rw-r--r--sflow-rt/app/browse-metrics/html/index.html80
-rw-r--r--sflow-rt/app/browse-metrics/html/js/app.js226
-rw-r--r--sflow-rt/app/flow-trend/LICENSE21
-rw-r--r--sflow-rt/app/flow-trend/README.md15
-rw-r--r--sflow-rt/app/flow-trend/html/css/app.css66
-rw-r--r--sflow-rt/app/flow-trend/html/index.html129
-rw-r--r--sflow-rt/app/flow-trend/html/js/app.js431
-rw-r--r--sflow-rt/app/flow-trend/scripts/inc/trend.js49
-rw-r--r--sflow-rt/app/flow-trend/scripts/top.js159
11 files changed, 1207 insertions, 0 deletions
diff --git a/sflow-rt/app/browse-metrics/LICENSE b/sflow-rt/app/browse-metrics/LICENSE
new file mode 100644
index 0000000..1ee363b
--- /dev/null
+++ b/sflow-rt/app/browse-metrics/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 InMon Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sflow-rt/app/browse-metrics/README.md b/sflow-rt/app/browse-metrics/README.md
new file mode 100644
index 0000000..edd31d9
--- /dev/null
+++ b/sflow-rt/app/browse-metrics/README.md
@@ -0,0 +1,10 @@
+# Browse metrics
+
+## To install
+
+1. [Download sFlow-RT](https://sflow-rt.com/download.php)
+2. Run command: `sflow-rt/get-app.sh sflow-rt browse-metrics`
+3. Restart sFlow-RT
+
+For more information, visit:
+https://sFlow-RT.com
diff --git a/sflow-rt/app/browse-metrics/html/index.html b/sflow-rt/app/browse-metrics/html/index.html
new file mode 100644
index 0000000..bbb210a
--- /dev/null
+++ b/sflow-rt/app/browse-metrics/html/index.html
@@ -0,0 +1,80 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="icon" type="image/png" href="../../../inc/img/favicon.png">
+ <link rel="stylesheet" href="../../../inc/bootstrap.min.css">
+ <link rel="stylesheet" href="../../../inc/stripchart.css">
+ <script type="text/javascript" src="../../../inc/jquery.min.js"></script>
+ <script type="text/javascript" src="../../../inc/jquery.widget.js"></script>
+ <script type="text/javascript" src="../../../inc/jquery.stripchart.js"></script>
+ <script type="text/javascript" src="../../../inc/popper.min.js"></script>
+ <script type="text/javascript" src="../../../inc/bootstrap.min.js"></script>
+ <script type="text/javascript" src="js/app.js"></script>
+ <title>Metric Browser</title>
+ </head>
+ <body>
+ <nav class="navbar navbar-dark mb-3" style="background-color: #336;">
+ <a class="navbar-brand" href="#">
+ <img src="../../../inc/img/inmon.svg" height="30" class="d-inline-block align-top">
+ Metric Browser
+ </a>
+ <form class="form-inline">
+ <button id="clone" class="btn btn-outline-light" type="button">Clone</button>
+ </form>
+ </nav>
+ <main class="container" role="main">
+ <form>
+ <div class="form-row">
+ <div class="form-group col-md-4">
+ <label for="agent">Agent</label>
+ <select class="form-control" id="agent">
+ </select>
+ </div>
+ <div class="form-group col-md-4">
+ <label for="metric">Metric</label>
+ <select class="form-control" id="metric">
+ </select>
+ </div>
+ <div class="form-group col-md-2">
+ <label for="datasource">Datasource</label>
+ <select class="form-control" id="datasource">
+ </select>
+ </div>
+ <div class="form-group col-md-2">
+ <label for="aggregation">Statistic</label>
+ <select class="form-control" id="aggregation">
+ <option value="max">max</option>
+ <option value="min">min</option>
+ <option value="sum">sum</option>
+ <option value="avg">avg</option>
+ <option value="var">var</option>
+ <option value="sdev">sdev</option>
+ <option value="med">med</option>
+ <option value="q1">q1</option>
+ <option value="q2">q2</option>
+ <option value="q3">q3</option>
+ <option value="iqr">iqr</option>
+ <option value="any">any</option>
+ </select>
+ </div>
+ </div>
+ </form>
+ <table class="table table-striped table-bordered table-sm">
+ <tbody>
+ <tr><td class="text-right font-weight-bold" style="width: 50%">Value</td><td id="metric-value" style="width: 50%"></td></tr>
+ <tr><td class="text-right font-weight-bold" style="width: 50%">N</td><td id="metric-n" style="width: 50%"></td></tr>
+ <tr><td class="text-right font-weight-bold" style="width: 50%">Agent</td><td id="metric-agent" style="width: 50%"></td></tr>
+ <tr><td class="text-right font-weight-bold" style="wdith: 50%">Datasource</td><td id="metric-datasource" style="width: 50%"></td></tr>
+ </tbody>
+ </table>
+ <div id="chart" class="trend" style="height:300px"></div>
+ </main>
+ <footer class="footer page-footer border-top mt-3">
+ <div class="footer-copyright text-center py-2">
+ <small class="text-muted">Copyright &copy; 2015-<span class="year">2019</span> InMon Corp. ALL RIGHTS RESERVED</small>
+ </div>
+ </footer>
+ </body>
+</html>
diff --git a/sflow-rt/app/browse-metrics/html/js/app.js b/sflow-rt/app/browse-metrics/html/js/app.js
new file mode 100644
index 0000000..4847897
--- /dev/null
+++ b/sflow-rt/app/browse-metrics/html/js/app.js
@@ -0,0 +1,226 @@
+$(function() {
+ var defaults = {
+ agent: 'ALL',
+ datasource: 'ALL',
+ aggregation: 'max'
+ };
+
+ var state = {};
+ $.extend(state,defaults);
+
+ function createQuery(params) {
+ var query, key, value;
+ for(key in params) {
+ value = params[key];
+ if(value === defaults[key]) continue;
+ if(query) query += '&';
+ else query = '';
+ query += encodeURIComponent(key)+'='+encodeURIComponent(value);
+ }
+ return query;
+ }
+
+ function getState(key, defVal) {
+ return window.sessionStorage.getItem('mb_'+key) || state[key] || defVal;
+ }
+
+ function setState(key, val, showQuery) {
+ state[key] = val;
+ window.sessionStorage.setItem('mb_'+key, val);
+ if(showQuery) {
+ var query = createQuery(state);
+ window.history.replaceState({},'',query ? '?' + query : './');
+ }
+ }
+
+ function setQueryParams(query) {
+ var vars = query.split('&');
+ var params = {};
+ for(var i = 0; i < vars.length; i++) {
+ var pair = vars[i].split('=');
+ if(pair.length === 2) setState(decodeURIComponent(pair[0]), decodeURIComponent(pair[1]),false);
+ }
+ }
+
+ var search = window.location.search;
+ if(search) setQueryParams(search.substring(1));
+
+ $('#clone').click(function() {
+ window.open(window.location);
+ });
+
+ var selectedAgent = getState('agent');
+ var selectedMetric = getState('metric');
+ var selectedDatasource = getState('datasource');
+ var selectedAggregation = getState('aggregation');
+
+ var names = {};
+ var maxPoints = 5 * 60;
+ var step = 1000;
+ var chartData;
+
+ var nf = $.inmon.stripchart.prototype.valueStr;
+
+ var widget = $('#chart').stripchart();
+
+ function resetChart() {
+ chartData = {times:[], values: []};
+ var i, t = Date.now();
+ for(i = 0; i < maxPoints; i++) {
+ t = t - step;
+ chartData.times.unshift(t);
+ }
+ var series = new Array(chartData.times.length);
+ for(i = 0; i < chartData.times.length; i++) series[i] = 0;
+ chartData.values.push(series);
+ }
+
+ function updateChart(data) {
+ if(!data || data.length === 0) return;
+ if(!chartData) resetChart();
+
+ var now = Date.now();
+ chartData.times.push(now);
+ var tmin = now - (maxPoints * 1.04 * step);
+ var nshift = 0;
+ while(chartData.times.length >= maxPoints || chartData.times[0] < tmin) {
+ chartData.times.shift();
+ nshift++;
+ }
+ var series = chartData.values[0];
+ var val = data[0].metricValue;
+ series.push($.isNumeric(val) ? val : 0);
+ for(var i = 0; i < nshift; i++) {
+ series.shift();
+ }
+ widget.stripchart("draw", chartData);
+ $('#metric-agent').text(data[0].agent || '');
+ $('#metric-datasource').text(data[0].dataSource || '');
+ $('#metric-value').text($.isNumeric(val) ? nf(val) : val);
+ $('#metric-n').text(data[0].metricN || '');
+ }
+
+ $(window).resize(function() {
+ widget.stripchart("draw", chartData);
+ });
+
+ function updateMetrics() {
+ names = {};
+ if('ALL' == selectedAgent) {
+ $.get('../../../metrics/json', function(metrics) {
+ var metricSelect = $('#metric');
+ var dsSelect = $('#datasource');
+ var i, metricsList = Object.keys(metrics).sort();
+ if(!metrics[selectedMetric]) {
+ selectedMetric = metricsList[0];
+ setState('metric',selectedMetric,true);
+ }
+ metricSelect.empty();
+ for(i = 0; i < metricsList.length; i++) {
+ metricSelect.append('<option value="'+metricsList[i]+'"' + (selectedMetric == metricsList[i] ? ' selected' : '') + '>'+metricsList[i]+'</option>');
+ }
+ dsSelect.empty();
+ dsSelect.append('<option value="ALL" selected>ALL</option>');
+ selectedDatasource = 'ALL';
+ setState('datasource',selectedDatasource,true);
+ resetChart();
+ });
+ } else {
+ $.get('../../../metric/'+selectedAgent+'/json',function(metrics) {
+ var metricSelect = $('#metric');
+ var dsSelect = $('#datasource');
+ var i, idx, ds, dsName, dsList, name, nameList, metricsList = Object.keys(metrics);
+ for(i = 0; i < metricsList.length; i++) {
+ idx = metricsList[i].lastIndexOf('.');
+ ds = metricsList[i].substring(0,idx);
+ name = metricsList[i].substring(idx+1);
+ dsList = names[name];
+ if(!dsList) {
+ dsList = {};
+ names[name] = dsList;
+ };
+ dsList[ds] = metrics[ds+'.vir_host_name'] || metrics[ds+'.host_name'] || metrics[ds+'.ifname'] || ds;;
+ }
+ nameList = Object.keys(names).sort();
+ if(!names[selectedMetric]) {
+ selectedMetric = nameList[0];
+ setState('metric',selectedMetric,true);
+ }
+ metricSelect.empty();
+ for(i = 0; i < nameList.length; i++) {
+ metricSelect.append('<option value="'+nameList[i]+'"' + (selectedMetric == nameList[i] ? ' selected' : '') + '>'+nameList[i]+'</option>');
+ }
+ dsList = Object.keys(names[selectedMetric]).sort((a,b) => a - b);
+ dsSelect.empty();
+ dsSelect.append('<option value="ALL"'+('ALL' === selectedDatasource ? ' selected' : '')+'>ALL</option>');
+ for(i = 0; i < dsList.length; i++) {
+ ds = dsList[i];
+ dsName = names[selectedMetric][ds];
+ dsSelect.append('<option value="'+ds+'"'+(ds === selectedDatasource ? ' selected' : '')+'>'+dsName+'</options>');
+ }
+ resetChart();
+ });
+ }
+ }
+ $('#agent').change(function(evt) {
+ selectedAgent = $('#agent').children('option:selected').val();
+ setState('agent',selectedAgent,true);
+ updateMetrics();
+ });
+ $('#metric').change(function(evt) {
+ selectedMetric = $('#metric').children('option:selected').val();
+ setState('metric',selectedMetric,true);
+ var dsSelect = $('#datasource');
+ var i, ds, dsName, dsList = Object.keys(names[selectedMetric] || {});
+ dsSelect.empty();
+ if(dsList) {
+ dsList.sort((a,b) => a - b);
+ dsSelect.append('<option value="ALL">ALL</option>');
+ for(i = 0; i < dsList.length; i++) {
+ ds = dsList[i];
+ dsName = names[selectedMetric][ds];
+ dsSelect.append('<option value="'+ds+'">'+dsName+'</options>');
+ }
+ } else {
+ dsSelect.append('<option value="ALL" selected>ALL</option>');
+ }
+ resetChart();
+ });
+ $('#datasource').change(function(evt) {
+ selectedDatasource = $('#datasource').children('option:selected').val();
+ setState('datasource',selectedDatasource,true);
+ resetChart();
+ });
+ $('#aggregation option[value="'+selectedAggregation+'"]').prop('selected',true);
+ $('#aggregation').change(function(evt) {
+ selectedAggregation = $('#aggregation').children('option:selected').val();
+ setState('aggregation',selectedAggregation,true);
+ resetChart();
+ });
+ $.get('../../../agents/json', function(agents) {
+ var agentSelect = $('#agent');
+ var i, agentList = Object.keys(agents).sort();
+ agentSelect.append('<option value="ALL"'+('ALL' === selectedAgent ? ' selected' : '')+'>ALL</option>');
+ for(i = 0; i < agentList.length; i++) {
+ agentSelect.append('<option value="'+agentList[i]+'"'+(agentList[i] === selectedAgent ? ' selected' : '')+'>'+agentList[i]+'</option>');
+ }
+ updateMetrics();
+ resetChart();
+ });
+ (function poll() {
+ var metric = (!selectedDatasource || 'ALL' === selectedDatasource ? '' : selectedDatasource + '.') + selectedMetric;
+ var url = '../../../metric/'+selectedAgent+'/'+selectedAggregation+':'+metric+'/json';
+ $.ajax({
+ url: url,
+ success: function(data) {
+ updateChart(data);
+ setTimeout(poll, step);
+ },
+ error: function(result,status,errorThrown) {
+ setTimeout(poll, 5000);
+ },
+ dataType: "json",
+ timeout: 60000
+ });
+ })();
+});
diff --git a/sflow-rt/app/flow-trend/LICENSE b/sflow-rt/app/flow-trend/LICENSE
new file mode 100644
index 0000000..44e39f6
--- /dev/null
+++ b/sflow-rt/app/flow-trend/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 InMon Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sflow-rt/app/flow-trend/README.md b/sflow-rt/app/flow-trend/README.md
new file mode 100644
index 0000000..58beb4f
--- /dev/null
+++ b/sflow-rt/app/flow-trend/README.md
@@ -0,0 +1,15 @@
+# Real-time trend of top network flows
+
+https://blog.sflow.com/2017/09/flow-trend.html
+
+## To install
+
+1. [Download sFlow-RT](https://sflow-rt.com/download.php)
+2. Run command: `sflow-rt/get-app.sh sflow-rt flow-trend`
+3. Restart sFlow-RT
+
+Alternatively, use the Docker image:
+https://hub.docker.com/r/sflow/flow-trend/
+
+For more information, visit:
+https://sFlow-RT.com
diff --git a/sflow-rt/app/flow-trend/html/css/app.css b/sflow-rt/app/flow-trend/html/css/app.css
new file mode 100644
index 0000000..85cebc0
--- /dev/null
+++ b/sflow-rt/app/flow-trend/html/css/app.css
@@ -0,0 +1,66 @@
+body { font-family: Verdana,Arial,sans-serif; }
+#titleBar { font-weight: normal; }
+#clone { float: right; padding: .6em; position: relative; z-index: 1; }
+#clone_button { height: 18px; }
+#tabs { margin: 5px; min-height: 400px; font-size:80%;}
+.trend { width:100%; height:300px; }
+.trend td { cursor:pointer; text-decoration:underline; }
+.stripe {
+ margin: .5em 0;
+ border-collapse: collapse;
+ border: 1px solid #ddd;
+ width: 100%;
+ font-size: 90%;
+}
+.stripe th {
+ color: #666;
+ padding: .2em;
+ border-right: 1px solid #ddd;
+ border-bottom: 3px double #bbb;
+}
+.stripe td {
+ padding: .2em .4em;
+ border-right: 1px solid #ddd;
+ border-spacing: 0;
+}
+.alignr {text-align: right}
+.alignl {text-align: left}
+.alignc {text-align: center}
+.flowkey {
+ text-align: left;
+ color:#00e;
+ cursor: pointer;
+ text-decoration:underline;
+}
+.stripe .flowvalue {text-align:right;min-width:12em;}
+.stripe .even {background-color: #fff;}
+.stripe .odd {background-color: #f5f5f5;}
+.stripe .bar {float:left; height:1em;}
+
+.ui-autocomplete {
+ font-size: 80%;
+ max-height: 200px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+fieldset { border-radius: 4px; border-width:2px; border-color:#f5f5f5; margin-bottom: 10px; }
+#flowspec { text-align: center; width:100%; }
+#keys { width: 35%; }
+#value { width:10%; }
+#filter { width:35%; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { text-align: center; float: none; }
+input.good {background-color: #aaffaa; }
+input.warn {background-color: #ffffaa; }
+input.error { background-color: #ffaaaa; }
+td.good {color: #33cc33; }
+td.warn { color: #e9ab17; }
+td.error { color: red; }
+div.slider {
+ display: none;
+ padding-left:50px;
+ background-color:white;
+}
+table.dataTable tbody td.no-padding {
+ padding: 0;
+}
+
diff --git a/sflow-rt/app/flow-trend/html/index.html b/sflow-rt/app/flow-trend/html/index.html
new file mode 100644
index 0000000..38e8ffe
--- /dev/null
+++ b/sflow-rt/app/flow-trend/html/index.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Flow Trend</title>
+ <link rel="icon" type="image/png" href="../../../inc/img/favicon.png">
+ <link rel="stylesheet" href="../../../inc/inmsf/main.css" type="text/css">
+ <link rel="stylesheet" href="../../../inc/jquery-ui/jquery-ui.structure.min.css" type="text/css">
+ <link rel="stylesheet" href="../../../inc/jquery-ui/jquery-ui.theme.css" type="text/css">
+ <link rel="stylesheet" href="../../../inc/DataTables/css/dataTables.jqueryui.css" type="text/css">
+ <link rel="stylesheet" href="../../../inc/stripchart.css" type="text/css"/>
+ <link rel="stylesheet" href="css/app.css" type="text/css">
+ <script type="text/javascript" src="../../../inc/jquery.min.js"></script>
+ <script type="text/javascript" src="../../../inc/jquery-ui/jquery-ui.min.js"></script>
+ <script type="text/javascript" src="../../../inc/DataTables/js/jquery.dataTables.min.js"></script>
+ <script type="text/javascript" src="../../../inc/DataTables/js/dataTables.jqueryui.min.js"></script>
+ <script type="text/javascript" src="../../../inc/jquery.stripchart.js"></script>
+ <script type="text/javascript" src="../../../inc/stripchart.js"></script>
+ <script type="text/javascript" src="js/app.js"></script>
+</head>
+<body>
+ <div id="titleBar"><div id="product"><span id="logo"></span>Flow Trend</div></div>
+ <div id="tabs">
+ <span id="clone">
+ <button id="clone_button">New Window</button>
+ </span>
+ <ul>
+ <li><a href="#top">Flow Trend</a></li>
+ <li><a href="#settings">Settings</a></li>
+ <li><a href="#help">Help</a></li>
+ </ul>
+ <div id="top">
+ <fieldset>
+ <legend>Flow Specification</legend>
+ <div id="flowspec">
+ <input id="keys" type="text" placeholder="Keys" size="50" autocomplete="off">
+ <input id="value" type="text" placeholder="Value" size="10" autocomplete="off">
+ <input id="filter" type="text" placeholder="Filter" size="50" autocomplete="off">
+ <button id="cleardef">Clear</button>
+ <button id="submitdef">Submit</button>
+ </div>
+ </fieldset>
+ <div id="topn" class="trend"></div>
+ <table id="shortcutstable" class="display" cellspacing="0" width="100%">
+ <thead>
+ <tr>
+ <th>Category</th>
+ <th>Protocol</th>
+ <th>Description</th>
+ </tr>
+ </thead>
+ </table>
+ </div>
+ <div id="settings">
+ <fieldset>
+ <legend>Flow Shortcuts</legend>
+ <label for="numshortcuts">Shortcuts:</label>
+ <input id="numshortcuts" name="numshortcuts" size="5" readonly>
+ <button id="shortcutsrefresh">Refresh</button>
+ <input type="file" name="shortcutsfile" id="shortcutsfile" multiple="false" accept="application/json" />
+ <button id="shortcutsset">Upload</button>
+ <button id="shortcutsget">Inspect</button>
+ </fieldset>
+ </div>
+ <div id="help">
+ <div id="help-acc">
+ <div>
+ <h3>About</h3>
+ <div>
+ <p><em>Flow Trend</em> displays a chart of top network flows in real-time.</p>
+ </div>
+ </div>
+ <div>
+ <h3>Monitoring top flows</h3>
+ <div>
+ <p>The <i>Flow Trend</i> tab is used to define and monitor specific flows. Flows are defined using the <i>Flow Specification</i> form.</p>
+ <dl>
+ <dt>keys:</dt><dd>list of flowkey attributes, e.g. ipsource,ipdestination</dd>
+ <dt>value:</dt><dd>Numeric flowkey attribute, e.g. <i>bps</i>, <i>fps</i>, <i>frames</i>, <i>bytes</i></dd>
+ <dt>filter:</dt><dd>boolean expression filtering flowkeys. Filter expressions are of the form &lt;flowkey&gt;&lt;operator&gt;&lt;value&gt; The = (equals), != (not equals), ~ (match regexp), and !~ (not match regexp) operators are available to compare the flowkey with a comma separated list of values. Filter expressions can be combined using &amp;, |, (, and ), e.g. (ipsource=10.0.0.1|ipdestination=10.0.0.1)&ipprotocol=1,6,17</dd>
+ </dl>
+ <p>The shortcuts table below the form contains pre-defined flow specifications. Clicking on a row in the shortcuts table enters settings in the <i>Flow Specification</i> form and starts monitoring flows. Clicking on the clear button in the <i>Flow Specification</i> form removes definition and returns the shortcuts table.</p>
+ <p>Flow specifications can be entered directly into the form. An incremental search menu lists available options when entering text into the form. Available flow keys vary depending on the traffic being monitored, but the following are the basic layer 2, 3, and 4 protocol fields that are typically present:</p>
+ <dl>
+ <dt>Ethernet</dt><dd>macsource, macdestination, ethernetprotocol, isbroadcast, ismulticast, isunicast</dd>
+ <dt>IP</dt><dd>ipsource, ipdestination, ipprotocol, iptos, ipdscp</dd>
+ <dt>TCP</dt><dd>tcpsourceport, tcpdestinationport, tcpflags</dd>
+ <dt>UDP</dt><dd>udpsourceport, udpdestinationport</dd>
+ </dl>
+ <p>Tunneling can cause fields to appear multiple times in the packet. sFlow-RT adds a suffix to the flow key to indicate depth in the protocol stack, for example, a VxLAN encapsulated IP packet would have the following attributes, macsource, ipsource, macsource.1, ipsource.1, etc. Fields with a .1 suffix are the second occurence of the field in the packet and represent the inner (tenant) addresses.</p>
+ <p>Functions of the form &lt;funcname&gt;:&lt;arg1&gt;:&ltarg2&gt;... can be applied used to define a flowkey or in a filter:</p>
+ <dl>
+ <dt>group:&lt;flowkey&gt;:&lt;fv&gt;</dt><dd>Applies the configured <i>IP Address Groups</i> to a flow key and returns the group name, e.g. group:ipsource:fv or group:ipdestination:fv</dd>
+ <dt>oui:&lt;flowkey&gt;:&ltnumber|name&gt;</dt><dd>Extracts the three octet Organizationally Unique Identifier (OUI) prefix from a MAC address, optionally looking up the assigned vendor name, e.g. oui:macsource or oui:macsource:name</dd>
+ <dt>mask:&lt;flowkey&gt;:&lt;mask_bits&gt;</dt><dd>Applies a mask to an IP address, e.g. mask:ipsource:24</dd>
+ <dt>null:&lt;flowkey&gt;:&lt;null_value&gt;</dt><dd>Allows flows to be defined with missing fields, e.g. null:vlan:undefined</dd>
+ <dt>or:&lt;flowkey1&gt;:&ltflowkey2&gt;</dt><dd>Selects the first non-null field from a list, e.g. or:ipsource:ip6source</dd>
+ <dt>eq:&lt;flowkey1&gt;:&ltflowkey2&gt;</dt><dd>Returns true if the values referenced by the flow keys are equal, e.g. eq:ipsource:ipdestination</dd>
+ <dt>range:&lt;flowkey&gt;:&ltlower&gt;:&lt;upper&gt;</dt><dd>Return true if the value is included in the range, e.g. range:tcpsourceport:0:1023</dd>
+ </dl>
+ <p>Click on peaks in the trend chart to see values at that time. Click on items in the chart legend to drill down by adding the item to the current filter.</p>
+ </div>
+ </div>
+ <div>
+ <h3>Settings</h3>
+ <div>
+ <p>The <i>Settings</i> tab provides access to the following option:</p>
+ <h4>Flow Shortcuts</h4>
+ <p>Shortcuts are the pre-configured flow definitions in the <i>Top Flows</i> table. Shortcuts are represented as a JSON object. The following example shows the format:</p>
+ <pre>[
+ {
+ "category": "Traffic",
+ "protocol": "IP",
+ "description": "Sources",
+ "keys": "ipsource",
+ "value": "bps",
+ "filter": ""
+ },
+ ...
+]</pre>
+ <p>The <i>Shortcuts</i> count verifies that shortcuts have been installed. Button are available to refresh the counts, upload new shortcuts, and inspect installed shortcuts.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="copyright">Copyright &copy; 2017-2019 InMon Corp. ALL RIGHTS RESERVED</div>
+</body>
+</html>
diff --git a/sflow-rt/app/flow-trend/html/js/app.js b/sflow-rt/app/flow-trend/html/js/app.js
new file mode 100644
index 0000000..962bfe6
--- /dev/null
+++ b/sflow-rt/app/flow-trend/html/js/app.js
@@ -0,0 +1,431 @@
+$(function() {
+ var restPath = '../scripts/top.js/';
+ var shortcutsURL = restPath + 'shortcuts/json';
+ var keysURL = restPath + 'flowkeys/json';
+ var topURL = restPath + 'flows/json';
+
+ var SEP = '_SEP_';
+
+ var db = {};
+
+ var defaults = {
+ tab:0,
+ hlp0:'show',
+ hlp1:'hide',
+ hlp2:'hide',
+ keys:'',
+ value:'',
+ filter:'',
+ topshow:50,
+ };
+
+ var state = {};
+ $.extend(state,defaults);
+
+ function createQuery(params) {
+ var query, key, value;
+ for(key in params) {
+ value = params[key];
+ if(value === defaults[key]) continue;
+ if(query) query += '&';
+ else query = '';
+ query += encodeURIComponent(key)+'='+encodeURIComponent(value);
+ }
+ return query;
+ }
+
+ function getState(key, defVal) {
+ return window.sessionStorage.getItem('flow_trend_'+key) || state[key] || defVal;
+ }
+
+ function setState(key, val, showQuery) {
+ state[key] = val;
+ window.sessionStorage.setItem('flow_trend_'+key, val);
+ if(showQuery) {
+ var query = createQuery(state);
+ window.history.replaceState({},'',query ? '?' + query : './');
+ }
+ }
+
+ function setQueryParams(query) {
+ var vars = query.split('&');
+ var params = {};
+ for(var i = 0; i < vars.length; i++) {
+ var pair = vars[i].split('=');
+ if(pair.length === 2) setState(decodeURIComponent(pair[0]), decodeURIComponent(pair[1]),false);
+ }
+ }
+
+ var search = window.location.search;
+ if(search) setQueryParams(search.substring(1));
+
+ $('#help-acc > div').each(function(idx) {
+ $(this).accordion({
+ heightStyle:'content',
+ collapsible: true,
+ active: getState('hlp'+idx, 'hide') === 'show' ? 0 : false,
+ activate: function(event, ui) {
+ var newIndex = $(this).accordion('option','active');
+ setState('hlp'+idx, newIndex === 0 ? 'show' : 'hide', true);
+ }
+ });
+ });
+
+ $('#tabs').tabs({
+ active: getState('tab', 0),
+ activate: function(event, ui) {
+ var newIndex = ui.newTab.index();
+ setState('tab', newIndex, true);
+ $.event.trigger({type:'updateChart'});
+ },
+ create: function(event,ui) {
+ $.event.trigger({type:'updateChart'});
+ }
+ });
+
+ $('#clone_button').button({icons:{primary:'ui-icon-newwin'},text:false}).click(function() {
+ window.open(window.location);
+ });
+
+ var top_keys = getState('keys','');
+ var top_value = getState('value','');
+ var top_filter = getState('filter','');
+
+ $('#keys')
+ .val(top_keys)
+ .bind( "keydown", function( event ) {
+ if ( event.keyCode === $.ui.keyCode.TAB &&
+ $( this ).autocomplete( "instance" ).menu.active ) {
+ event.preventDefault();
+ }
+ })
+ .autocomplete({
+ minLength: 0,
+ source: function( request, response) {
+ $.getJSON(keysURL, { search: request.term.split(/,\s*/).pop() }, response)
+ },
+ focus: function() {
+ // prevent value inserted on focus
+ return false;
+ },
+ select: function( event, ui ) {
+ var terms = this.value.split(/,\s*/);
+ // remove the current input
+ terms.pop();
+ // add the selected item
+ terms.push( ui.item.value );
+ // add placeholder to get the comma-and-space at the end
+ terms.push( "" );
+ this.value = terms.join( "," );
+ return false;
+ }
+ })
+ .focus(function() { $(this).autocomplete('search'); });
+
+ $('#value')
+ .val(top_value)
+ .autocomplete({
+ minLength:0,
+ source:['bps', 'Bps', 'fps']
+ })
+ .focus(function() { $(this).autocomplete('search'); });
+
+ $('#filter')
+ .val(top_filter)
+ .bind( "keydown", function( event ) {
+ if ( event.keyCode === $.ui.keyCode.TAB &&
+ $( this ).autocomplete( "instance" ).menu.active ) {
+ event.preventDefault();
+ }
+ })
+ .autocomplete({
+ minLength: 0,
+ source: function( request, response) {
+ $.getJSON(keysURL, { search: request.term.split(/[&|(]\s*/).pop() }, response)
+ },
+ focus: function() {
+ // prevent value inserted on focus
+ return false;
+ },
+ select: function( event, ui ) {
+ var val = this.value;
+ var re = /[&|(]/g;
+ var end = 0;
+ while(re.test(val)) { end = re.lastIndex; }
+ this.value = val.substring(0,end) + ui.item.value + "=";
+ return false;
+ }
+ })
+ .focus(function() { $(this).autocomplete('search'); });
+
+ $('#cleardef').button({icons:{primary:'ui-icon-cancel'},text:false}).click(function() {
+ $('#keys').val('');
+ $('#value').val('');
+ $('#filter').val('');
+ top_keys = '';
+ top_value = '';
+ top_filter = '';
+ setState('keys',top_keys);
+ setState('value',top_value);
+ setState('filter',top_filter,true);
+ emptyTopFlows();
+ });
+ $('#submitdef').button({icons:{primary:'ui-icon-check'},text:false}).click(function() {
+ top_keys = $.trim($('#keys').val()).replace(/(,$)/g, "");
+ top_value = $.trim($('#value').val());
+ top_filter = $.trim($('#filter').val());
+ setState('keys',top_keys);
+ setState('value',top_value);
+ setState('filter',top_filter,true);
+ emptyTopFlows();
+ });
+ function valueToKey(val) {
+ var key;
+ switch(val) {
+ case 'bps':
+ key = 'bytes';
+ break;
+ case 'Bps':
+ key = 'bytes';
+ break;
+ case 'fps':
+ key = 'frames';
+ break;
+ default:
+ key = val;
+ }
+ return key;
+ }
+
+ function valueToScale(val) {
+ return 'bps' === val ? 8 : 1;
+ }
+
+ function valueToTitle(val) {
+ var title;
+ switch(val) {
+ case 'bps':
+ title = 'Bits per Second';
+ break;
+ case 'bytes':
+ case 'Bps':
+ title = 'Bytes per Second';
+ break;
+ case 'frames':
+ case 'fps':
+ title = 'Frames per Second';
+ break;
+ case 'requests':
+ title = 'Requests per Second';
+ break;
+ default:
+ title = val;
+ }
+ return title;
+ }
+
+ function addFilter(key, value, filter) {
+ var newFilter = filter;
+ if(!newFilter) newFilter = "";
+ if(newFilter.length > 0) newFilter += "&";
+ newFilter += "'" + key + "'='" + value + "'";
+ $('#filter').val(newFilter);
+ top_filter = newFilter;
+ setState('filter', top_filter, true);
+ emptyTopFlows();
+ }
+
+ var $shortcutsTable;
+ function initializeShortcutsTable() {
+ $shortcutsTable = $('#shortcutstable').DataTable({
+ ajax: {
+ url: shortcutsURL,
+ dataSrc: function(data) {
+ return data;
+ }
+ },
+ deferRenderer: true,
+ columns:[
+ {data:'category'},
+ {data:'protocol'},
+ {data:'description'}
+ ],
+ columnDefs: [ { targets: 2, orderable: false } ]
+ })
+ .page.len(getState('topshow'))
+ .on('length', function(e,settings,len) {
+ setState('topshow', len, true);
+ })
+ .on('xhr', function(e,settings,json) {
+ var len = json.length || 0;
+ $('#numshortcuts').val(len).removeClass(len ? 'error' : 'good').addClass(len ? 'good' : 'error');;
+ })
+ .on('click', 'tr', function(e) {
+ var row = $shortcutsTable.row($(this));
+ var shortcut = row.data();
+ if(!shortcut) return;
+ top_keys = shortcut.keys || '';
+ top_value = shortcut.value || '';
+ top_filter = shortcut.filter || '';
+ $('#keys').val(top_keys);
+ $('#value').val(top_value);
+ $('#filter').val(top_filter);
+ setState('keys', top_keys, false);
+ setState('value', top_value, false);
+ setState('filter', top_filter, true);
+ emptyTopFlows();
+ });
+ }
+
+ function updateData(data,scale) {
+ if(!data
+ || !data.trend
+ || !data.trend.times
+ || data.trend.times.length == 0) return;
+
+ if(scale !== 1) {
+ var topn = data.trend.trends.topn;
+ for(var i = 0; i < topn.length; i++) {
+ var entry = topn[i];
+ for(var flow in entry) {
+ entry[flow]*=scale;
+ }
+ }
+ }
+
+ if(db.trend) {
+ // merge in new data
+ var maxPoints = db.trend.maxPoints;
+ var remove = db.trend.times.length > maxPoints ? db.trend.times.length - maxPoints : 0;
+ db.trend.times = db.trend.times.concat(data.trend.times);
+ if(remove) db.trend.times = db.trend.times.slice(remove);
+ for(var name in db.trend.trends) {
+ db.trend.trends[name] = db.trend.trends[name].concat(data.trend.trends[name]);
+ if(remove) db.trend.trends[name] = db.trend.trends[name].slice(remove);
+ }
+ } else db.trend = data.trend;
+
+ db.trend.start = new Date(db.trend.times[0]);
+ db.trend.end = new Date(db.trend.times[db.trend.times.length - 1]);
+
+ $.event.trigger({type:'updateChart'});
+ }
+
+
+ var running_topflows;
+ var timeout_topflows;
+ function pollTopFlows() {
+ running_topflows = true;
+ var query = {keys:top_keys,value:valueToKey(top_value),filter:top_filter};
+ if(db.trend && db.trend.end) query.after=db.trend.end.getTime();
+ var scale = valueToScale(top_value);
+ $.ajax({
+ url: topURL,
+ data: query,
+ success: function(data) {
+ if(running_topflows) {
+ updateData(data,scale);
+ timeout_topflows = setTimeout(pollTopFlows, 1000);
+ }
+ },
+ error: function(result,status,errorThrown) {
+ if(running_topflows) timeout_topflows = setTimeout(pollTopFlows, 5000);
+ }
+ });
+ }
+
+ function stopPollTopFlows() {
+ running_topflows = false;
+ if(timeout_topflows) clearTimeout(timeout_topflows);
+ }
+
+ function emptyTopFlows() {
+ stopPollTopFlows();
+ if(db.trend) {
+ $(document).off('updateChart');
+ $('#topn').stripchart('destroy');
+ $('#topn').empty();
+ delete db.trend;
+ }
+ if(!top_keys || !top_value) {
+ $('#shortcutstable_wrapper').show();
+ $('#topn').hide();
+ return;
+ }
+ $('#shortcutstable_wrapper').hide();
+ $('#topn').show();
+
+ if(!db.trend) {
+ $('#topn').chart({
+ type: 'topn',
+ legendHeadings: top_keys.match(/(\\.|[^,])+/g),
+ units:valueToTitle(top_value),
+ stack: true,
+ sep: SEP,
+ metric: 'topn'
+ },db);
+ }
+
+ var query = {keys:top_keys,value:valueToKey(top_value),filter:top_filter};
+ pollTopFlows();
+ }
+
+ $('#topn').click(function(e) {
+ var idx,key,val,tgt = $(e.target);
+ if(tgt.is('td')) {
+ idx = tgt.index() - 1;
+ key = top_keys.match(/(\\.|[^,])+/g)[idx];
+ val = tgt.text();
+ addFilter(key,val,top_filter);
+ }
+ else if(tgt.is('div') && tgt.parent().is('td')) {
+ var row = tgt.parent().parent();
+ row.children().each(function(i,td) {
+ if(i>0) {
+ idx = i - 1;
+ key = top_keys.match(/(\\.|[^,])+/g)[idx];
+ val = $(td).text();
+ addFilter(key,val,top_filter);
+ }
+ });
+ }
+ });
+
+ function refreshShortcuts() {
+ $shortcutsTable.ajax.reload();
+ }
+
+ function getShortcuts() {
+ location.href = shortcutsURL;
+ }
+
+ function warningDialog(message) {
+ $('<div>' + message + '</div>').dialog({dialogClass:'alert', modal:true, buttons:{'Close': function() { $(this).dialog('close'); }}})
+ }
+
+ $('#shortcutsrefresh').button({icons:{primary:'ui-icon-arrowrefresh-1-e'},text:false}).click(refreshShortcuts);
+ $('#shortcutsget').button({icons:{primary:'ui-icon-search'},text:false}).click(getShortcuts);
+ $('#shortcutsfile').hide().change(function(event) {
+ var input = event.target;
+ var reader = new FileReader();
+ var $this = $(this);
+ reader.onload = function(){
+ var text = reader.result;
+ $this.wrap('<form>').closest('form').get(0).reset();
+ $this.unwrap();
+ $.ajax({
+ url:shortcutsURL,
+ type: 'POST',
+ contentType:'application/json',
+ data:text,
+ success:refreshShortcuts,
+ error: function() { warningDialog('Badly formatted shortcuts'); }
+ });
+ };
+ reader.readAsText(input.files[0]);
+ });
+ $('#shortcutsset').button({icons:{primary:'ui-icon-arrowstop-1-n'},text:false}).click(function() {$('#shortcutsfile').click();});
+
+ initializeShortcutsTable();
+ emptyTopFlows();
+});
diff --git a/sflow-rt/app/flow-trend/scripts/inc/trend.js b/sflow-rt/app/flow-trend/scripts/inc/trend.js
new file mode 100644
index 0000000..e550352
--- /dev/null
+++ b/sflow-rt/app/flow-trend/scripts/inc/trend.js
@@ -0,0 +1,49 @@
+function Trend(maxPoints, stepSize) {
+ this.maxPoints = maxPoints;
+ this.trends = {};
+ this.times = new Array(maxPoints);
+ var i, t = (new Date()).getTime(), stepMs = stepSize * 1000;
+ for(i = maxPoints - 1; i >= 0; i--) { t -= stepMs; this.times[i] = t; }
+}
+
+Trend.prototype.addPoints = function(now,values) {
+ this.times.push(now);
+
+ var name, i;
+ for (name in values) {
+ var points = this.trends[name];
+ if(!points) {
+ points = new Array(this.maxPoints);
+ for(i = 0; i < this.maxPoints; i++) points[i] = 0;
+ this.trends[name] = points;
+ }
+ points.push(values[name]);
+ points.shift();
+ }
+ this.times.shift();
+}
+
+Trend.prototype.after = function(tval) {
+ var res = new Trend(0,0);
+ res.maxPoints = this.maxPoints;
+ for(var i = 0; i < this.times.length; i++) {
+ var t = this.times[i];
+ if(tval < t) {
+ res.times.push(t);
+ for (var name in this.trends) {
+ var val = this.trends[name][i];
+ var trend = res.trends[name];
+ if(!trend) {
+ trend = [];
+ res.trends[name] = trend;
+ }
+ trend.push(val);
+ }
+ }
+ }
+ return res;
+}
+
+Trend.prototype.remove = function(name) {
+ delete this.trends[name];
+}
diff --git a/sflow-rt/app/flow-trend/scripts/top.js b/sflow-rt/app/flow-trend/scripts/top.js
new file mode 100644
index 0000000..a9c2983
--- /dev/null
+++ b/sflow-rt/app/flow-trend/scripts/top.js
@@ -0,0 +1,159 @@
+// author: InMon Corp.
+// version: 1.0
+// date: 3/2/2017
+// description: Flow Trend
+// copyright: Copyright (c) 2017 InMon Corp. ALL RIGHTS RESERVED
+
+include(scriptdir()+'/inc/trend.js');
+
+var SEP = '_SEP_';
+
+var defaultShortcuts = [
+{category:'Traffic', protocol:'IP', description:'Sources',keys:'ipsource',value:'bps',filter:''},
+{category:'Traffic', protocol:'IP', description:'Destinations',keys:'ipdestination',value:'bps',filter:''},
+{category:'Traffic', protocol:'IP', description:'Source-Destination pairs',keys:'ipsource,ipdestination',value:'bps',filter:''},
+{category:'Traffic', protocol:'IPv6', description:'Sources',keys:'ip6source',value:'bps',filter:''},
+{category:'Traffic', protocol:'IPv6', description:'Destinations',keys:'ip6destination',value:'bps',filter:''},
+{category:'Traffic', protocol:'IPv6', description:'Source-Destination pairs',keys:'ip6source,ip6destination',value:'bps',filter:''},
+{category:'Traffic', protocol:'Ethernet', description:'Sources',keys:'macsource,oui:macsource:name',value:'frames',filter:''},
+{category:'Traffic', protocol:'Ethernet', description:'Sources of broadcasts',keys:'macsource,oui:macsource:name',value:'frames',filter:'macdestination=FFFFFFFFFFFF'},
+{category:'Security', protocol:'TCP', description:'Connection attempts',keys:'ipsource,ipdestination,tcpdestinationport',value:'frames',filter:'tcpflags~.......1.'},
+{category:'Security', protocol:'ARP', description:'Source of requests',keys:'arpipsender',value:'requests',filter:'arpoperation=1'},
+{category:'Security', protocol:'DNS', description:'Requested domains',keys:'dnsqname',value:'requests',filter:''},
+{category:'Security', protocol:'DNS', description:'Clients',keys:'or:ipsource:ip6source',value:'requests',filter:'dnsqr=false'},
+{category:'Security', protocol:'DNS', description:'Servers',keys:'or:ipsource:ip6source',value:'requests',filter:'dnsqr=true'},
+{category:'Security', protocol:'ICMP', description:'Unreachable ports', keys:'ipdestination,icmpunreachableport', value:'fps', filter:''},
+{category:'Security', protocol:'ICMP', description:'Unreachable protocols', keys:'ipdestination,icmpunreachableprotocol', value:'fps', filter:''},
+{category:'Security', protocol:'ICMP', description:'Unreachable hosts', keys:'ipdestination,icmpunreachablehost', value:'fps', filter:''},
+{category:'Security', protocol:'ICMP', description:'Unreachable networks', keys:'ipdestination,icmpunreachablenet', value:'fps', filter:''},
+{category:'Virtualization', protocol:'VxLAN', description:'Tenant VNI', keys:'vxlanvni',value:'bps',filter:''},
+{category:'Virtualization', protocol:'NVGRE', description:'Tenant VSID', keys:'grevsid', value:'bps',filter:''},
+{category:'Virtualization', protocol:'Geneve', description:'Tenant VNI', keys:'genevevni',value:'bps',filter:''}
+];
+
+var aggMode = getSystemProperty('flow-trend.aggMode') || 'max';
+var maxFlows = getSystemProperty('flow-trend.maxFlows') || 10;
+var minValue = getSystemProperty('flow-trend.minValue') || 0.01;
+var agents = getSystemProperty('flow-trend.agents') || 'ALL';
+var t = getSystemProperty('flow-trend.t') || 2;
+
+var shortcuts = storeGet('shortcuts') || defaultShortcuts;
+
+var userFlows = {};
+
+function escapeRegExp(str) {
+ // seems like a bug - Rhino doesn't convert Java strings into native JavaScript strings
+ str = new String(str);
+ return str ? str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") : null;
+}
+
+var specID = 0;
+function flowSpec(keys,value,filter) {
+ var keysStr = keys ? keys.join(',') : '';
+ var valueStr = value ? value.join(',') : '';
+ var filterStr = filter ? filter.join('&') : '';
+
+ if(keysStr.length === 0 || valueStr.length === 0) return null;
+
+ var key = keysStr || '';
+ if(valueStr) key += '#' + valueStr;
+ if(filterStr) key += '#' + filterStr;
+ var entry = userFlows[key];
+ if(!entry) {
+ // try to create flow
+ var name = 'flow_trend_' + specID;
+ try {
+ setFlow(name,{keys:keysStr, value:valueStr, filter: filterStr.length > 0 ? filterStr : null, t:t, n:maxFlows, fs:SEP});
+ entry = {name:name, trend: new Trend(300,1)};
+ entry.trend.addPoints(Date.now(), {topn:{}});
+ userFlows[key] = entry;
+ specID++;
+ } catch(e) {
+ entry = null;
+ }
+ }
+ if(!entry) return null;
+ entry.lastQuery = (new Date()).getTime();
+
+ return entry;
+}
+
+setIntervalHandler(function(now) {
+ var key, entry, top, topN, i;
+ for(key in userFlows) {
+ entry = userFlows[key];
+ if(now - entry.lastQuery > 10000) {
+ clearFlow(entry.name);
+ delete userFlows[key];
+ } else {
+ topN = {};
+ top = activeFlows(agents,entry.name,maxFlows,minValue,aggMode);
+ if(top) {
+ for(i = 0; i < top.length; i++) {
+ topN[top[i].key] = top[i].value;
+ }
+ }
+ entry.trend.addPoints(now,{topn:topN});
+ }
+ }
+},1);
+
+function validShortcuts(obj) {
+ if(!Array.isArray(obj)) return false;
+ var attrs = ['category','protocol','description','keys','value','filter'];
+ for(var i = 0; i < obj.length; i++) {
+ let shortcut = obj[i];
+ for(var j = 0; j < attrs.length; j++) {
+ let attr = attrs[j];
+ if(!shortcut.hasOwnProperty(attr)) return false;
+ if(typeof shortcut[attr] !== 'string') return false;
+ }
+ }
+ return true;
+}
+
+setHttpHandler(function(req) {
+ var result, trend, key, entry, path = req.path;
+ if(!path || path.length === 0) throw "not_found";
+
+ switch(path[0]) {
+ case 'shortcuts':
+ if(path.length > 1) throw "not_found";
+ switch(req.method) {
+ case 'POST':
+ case 'PUT':
+ if(req.error) throw "bad_request";
+ if(!validShortcuts(req.body)) throw "bad_request";
+ shortcuts = req.body;
+ storeSet('shortcuts', shortcuts);
+ break;
+ default: return shortcuts;
+ }
+ break;
+ case 'flowkeys':
+ if(path.length > 1) throw "not_found";
+ result = [];
+ var search = req.query['search'];
+ if(search) {
+ var matcher = new RegExp('^' + escapeRegExp(search), 'i');
+ for(key in flowKeys()) {
+ if(matcher.test(key)) result.push(key);
+ }
+ } else {
+ for(key in flowKeys()) result.push(key);
+ }
+ result.sort();
+ break;
+ case 'flows':
+ if(path.length > 1) throw "not_found";
+ entry = flowSpec(req.query['keys'],req.query['value'],req.query['filter']);
+ if(!entry) throw 'bad_request';
+ trend = entry.trend;
+ result = {};
+ result.trend = req.query.after ? trend.after(parseInt(req.query.after)) : trend;
+ break;
+ default: throw 'not_found';
+ }
+ return result;
+});
+