Add statistics page based on dataplane.getStats, link in sidebar, udpated README

develop
Thomas Lynch 1 year ago
parent 353b183626
commit b29d90f74e
  1. 1
      README.md
  2. 5
      api.js
  3. 8
      components/MenuLinks.js
  4. 84
      controllers/account.js
  5. 130
      pages/stats.js
  6. 6
      router.js

@ -19,6 +19,7 @@ Provides a control panel interface to conveniently manage clusters (groups of id
- Protection rules, choose bot protection mode "none" (whitelist), proof-of-work or proof-of-work+captcha. Can be domain-wide or a domain+path. Path overrides domain-wide.
- Global override for protection mode, to enable for all domains in a cluster.
- Maintenance mode, disables proxying for selected domains and serves an "under maintenance" page from haproxy.
- Statistics page with server and backend-level breakdowns based on haproxy stats socket data.
##### Todo:
- Improved cert management

@ -61,6 +61,11 @@ export async function deleteFromMap(mapName, body, dispatch, errorCallback, rout
return ApiCall(`/forms/map/${mapName}/delete`, 'POST', body, dispatch, errorCallback, router, 0.5);
}
// Stats
export async function getStats(dispatch, errorCallback, router) {
return ApiCall('/stats.json', 'GET', null, dispatch, errorCallback, router);
}
// Global toggle
export async function globalToggle(body, dispatch, errorCallback, router) {
return ApiCall('/forms/global/toggle', 'POST', body, dispatch, errorCallback, router, 0.5);

@ -108,6 +108,14 @@ export default withRouter(function MenuLinks({ router }) {
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/stats">
<a className={router.pathname === "/stats" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-table pe-none me-2" width="16" height="16" />
Statistics
</a>
</Link>
</li>
</ul>
<hr />
<ul className="nav nav-pills flex-column">

@ -33,7 +33,70 @@ exports.accountData = async (req, res, next) => {
globalAcl: globalAcl === '1',
aRecords,
aaaaRecords,
}
};
};
/**
* stats data
*/
exports.statsData = async (req, res, next) => {
let serverStats = []
, frontendStats = [];
([serverStats, frontendStats] = await Promise.all([
res.locals.dataPlaneAll('getStats', { type: 'server', parent: 'servers' }, null, null, true),
res.locals.dataPlaneAll('getStats', { type: 'frontend' }, null, null, true)
]));
frontendStats.forEach(s => {
s[0].stats = s[0].stats
.filter(t => t.name === 'www-http-https')
.map(t => ({
'name': t.name,
'stats': {
'Bytes in': t.stats.bin,
'Bytes out': t.stats.bout,
'Conn rate': t.stats.conn_rate,
'Cr (max)': t.stats.conn_rate_max,
'Request rate': t.stats.req_rate,
'Rr (max)': t.stats.req_rate_max,
'1xx': t.stats.hrsp_1xx,
'2xx': t.stats.hrsp_2xx,
'3xx': t.stats.hrsp_3xx,
'4xx': t.stats.hrsp_4xx,
'5xx': t.stats.hrsp_5xx,
'Total': t.stats.req_tot,
}
}));
});
serverStats.forEach(host => {
host.forEach(server => {
server.stats = server.stats
.filter(t => t.backend_name === 'servers')
.map(t => ({
'name': t.name,
'backend_name': t.backend_name,
'stats': {
'Address': t.stats.addr,
'Bytes in': t.stats.bin,
'Bytes out': t.stats.bout,
'Sess rate': t.stats.rate,
'Sr (max)': t.stats.rate_max,
'Queue': t.stats.qcur,
'Q (max)': t.stats.qmax,
'Q (time)': t.stats.qtime,
'1xx': t.stats.hrsp_1xx,
'2xx': t.stats.hrsp_2xx,
'3xx': t.stats.hrsp_3xx,
'4xx': t.stats.hrsp_4xx,
'5xx': t.stats.hrsp_5xx,
'Total': t.stats.req_tot,
}
}));
});
});
return {
serverStats,
frontendStats,
};
};
/**
@ -63,6 +126,25 @@ exports.accountJson = async (req, res, next) => {
return res.json({ ...data, user: res.locals.user });
}
/**
* GET /stats
* stats page html
*/
exports.statsPage = async (app, req, res, next) => {
const data = await exports.statsData(req, res, next);
return app.render(req, res, '/stats', { ...data, user: res.locals.user });
}
/**
* GET /stats.json
* stats json
*/
exports.statsJson = async (req, res, next) => {
const data = await exports.statsData(req, res, next);
return res.json({ ...data, user: res.locals.user });
}
/**
* POST /forms/global/toggle
* toggle global ACL

@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import BackButton from '../components/BackButton.js';
import ErrorAlert from '../components/ErrorAlert.js';
import * as API from '../api.js'
import { useRouter } from 'next/router';
export default function Domains(props) {
const router = useRouter();
const [state, dispatch] = useState(props);
const [error, setError] = useState();
useEffect(() => {
if (!state.user) {
API.getStats(dispatch, setError, router);
}
}, [state.user, router]);
if (!state.user) {
return (
<div className="d-flex flex-column">
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
Loading...
</div>
</div>
);
}
const { user, frontendStats, serverStats } = state;
//TODO: refresh function
const clusterServers = user.clusters[user.activeCluster]
.split(',')
.map(s => {
return new URL(s).hostname;
})
return (
<>
<Head>
<title>Statistics</title>
</Head>
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
Frontend Stats:
</h5>
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<tbody>
<tr>
<th>Hostname</th>
{Object.keys(frontendStats[0][0].stats.find(s => s.name === 'www-http-https').stats)
.map(key => <th key={'fe_th_'+key}>{key}</th>)}
</tr>
{frontendStats
.map(s => {
return s[0].stats.find(k => k.name === 'www-http-https').stats;
})
.map((s, i) => {
return (<tr key={'fe_stat_'+i}>
<td>{clusterServers[i]}</td>
{Object.values(s)
.map((v, j) => <td key={'fe_stat_val'+i+'_'+j}>{v}</td>)}
</tr>);
})}
</tbody>
</table>
</div>
<h5 className="fw-bold mt-4">
Backend Stats:
</h5>
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<tbody>
<tr>
<th>Hostname</th>
<th>Server</th>
{Object.keys(serverStats[0][0].stats.find(s => s.backend_name === 'servers').stats)
.map(key => <th key={'be_th_'+key}>{key}</th>)}
</tr>
{serverStats
.map((host, hi) => {
return host.map(s => {
// return s[0].stats.find(k => k.backend_name === 'servers');
return s.stats.map((s, i) => {
return (<tr key={'be_stat_'+i}>
<td>{clusterServers[hi]}</td>
<td>{s.name}</td>
{Object.values(s.stats)
.map((v, j) => <td key={'be_stat_val'+i+'_'+j}>{v}</td>)}
</tr>);
})
})
.reduce((acc, arr) => {
return acc.concat(arr);
}, []);
})
.reduce((acc, arr) => {
return acc.concat(arr);
}, [])}
</tbody>
</table>
</div>
{/* back to account */}
<BackButton to="/account" />
</>
);
}
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
}

@ -98,14 +98,14 @@ const testRouter = (server, app) => {
apiInstance.defaults.baseURL = `${firstClusterURL.origin}/v2`;
res.locals.dataPlane = apiInstance;
res.locals.dataPlaneAll = async (operationId, parameters, data, config) => {
res.locals.dataPlaneAll = async (operationId, parameters, data, config, all=false) => {
const promiseResults = await Promise.all(clusterUrls.map(clusterUrl => {
const singleApi = new OpenAPIClientAxios({ definition, axiosConfigDefaults: { headers: { 'authorization': `Basic ${base64Auth}` } } });
const singleApiInstance = singleApi.initSync();
singleApiInstance.defaults.baseURL = `${clusterUrl.origin}/v2`;
return singleApiInstance[operationId](parameters, data, { ...config, baseUrl: `${clusterUrl.origin}/v2` });
}));
return promiseResults[0]; //TODO: better desync handling
return all ? promiseResults.map(p => p.data) : promiseResults[0]; //TODO: better desync handling
}
res.locals.fetchAll = async (path, options) => {
//used for stuff that dataplaneapi with axios seems to struggle with e.g. multipart body
@ -161,6 +161,8 @@ const testRouter = (server, app) => {
server.get('/domains.json', useSession, fetchSession, checkSession, csrfMiddleware, domainsController.domainsJson);
server.get('/certs', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, certsController.certsPage.bind(null, app));
server.get('/certs.json', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, certsController.certsJson);
server.get('/stats', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.statsPage.bind(null, app));
server.get('/stats.json', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.statsJson);
const clusterRouter = express.Router({ caseSensitive: true });
clusterRouter.post('/global/toggle', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, accountController.globalToggle);
clusterRouter.post(`/map/:name(${mapNamesOrString})/add`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.patchMapForm); //add to MAP

Loading…
Cancel
Save