Skip to content

Commit

Permalink
Merge pull request #481 from highcharts/health/moving-average-indicat…
Browse files Browse the repository at this point in the history
…or-last-30-mins

health/moving-average-indicator
  • Loading branch information
PaulDalek authored Apr 4, 2024
2 parents 34ae5cc + 091b042 commit 56908ad
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 38 deletions.
10 changes: 8 additions & 2 deletions lib/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { readFileSync, writeFileSync } from 'fs';

import { getOptions, initExportSettings } from './config.js';
import { log, logWithStack } from './logger.js';
import { killPool, postWork } from './pool.js';
import { killPool, postWork, stats } from './pool.js';
import {
fixType,
handleResources,
Expand Down Expand Up @@ -59,7 +59,13 @@ export const startExport = async (settings, endCallback) => {
if (options.payload?.svg && options.payload.svg !== '') {
try {
log(4, '[chart] Attempting to export from a SVG input.');
return exportAsString(options.payload.svg.trim(), options, endCallback);
const result = exportAsString(
options.payload.svg.trim(),
options,
endCallback
);
++stats.exportFromSvgAttempts;
return result;
} catch (error) {
return endCallback(
new ExportError('[chart] Error loading SVG input.').setError(error)
Expand Down
28 changes: 15 additions & 13 deletions lib/pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@ import { measureTime } from './utils.js';

import ExportError from './errors/ExportError.js';

let performedExports = 0;
let exportAttempts = 0;
let timeSpent = 0;
let droppedExports = 0;
let spentAverage = 0;
// Pool statistics
export const stats = {
performedExports: 0,
exportAttempts: 0,
exportFromSvgAttempts: 0,
timeSpent: 0,
droppedExports: 0,
spentAverage: 0
};

let poolConfig = {};

// The pool instance
Expand Down Expand Up @@ -299,7 +304,7 @@ export const postWork = async (chart, options) => {
try {
log(4, '[pool] Work received, starting to process.');

++exportAttempts;
++stats.exportAttempts;
if (poolConfig.benchmarking) {
getPoolInfo();
}
Expand Down Expand Up @@ -377,8 +382,8 @@ export const postWork = async (chart, options) => {
// in turn is used by the /health route.
const workEnd = new Date().getTime();
const exportTime = workEnd - workStart;
timeSpent += exportTime;
spentAverage = timeSpent / ++performedExports;
stats.timeSpent += exportTime;
stats.spentAverage = stats.timeSpent / ++stats.performedExports;

log(4, `[pool] Work completed in ${exportTime} ms.`);

Expand All @@ -388,7 +393,7 @@ export const postWork = async (chart, options) => {
options
};
} catch (error) {
++droppedExports;
++stats.droppedExports;

if (workerHandle) {
pool.release(workerHandle);
Expand Down Expand Up @@ -455,8 +460,5 @@ export default {
getPool,
getPoolInfo,
getPoolInfoJSON,
workAttempts: () => exportAttempts,
droppedWork: () => droppedExports,
averageTime: () => spentAverage,
processedWorkCount: () => performedExports
getStats: () => stats
};
90 changes: 67 additions & 23 deletions lib/server/routes/health.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ See LICENSE file in root for details.

import { readFileSync } from 'fs';
import { join as pather } from 'path';
import { log } from '../../logger.js';

import cache from '../../cache.js';
import pool from '../../pool.js';
Expand All @@ -23,28 +24,71 @@ const pkgFile = JSON.parse(readFileSync(pather(__dirname, 'package.json')));

const serverStartTime = new Date();

const successRates = [];
const recordInterval = 60 * 1000; // record every minute
const windowSize = 30; // 30 minutes

function recordSuccessRate() {
const stats = pool.getStats();
const successRatio =
stats.exportAttempts === 0
? 1
: (stats.performedExports / stats.exportAttempts) * 100;

successRates.push(successRatio);
if (successRates.length > windowSize) {
successRates.shift();
}
}

function calculateMovingAverage() {
const sum = successRates.reduce((a, b) => a + b, 0);
return sum / successRates.length;
}

setInterval(recordSuccessRate, recordInterval);

/**
* Adds the GET /health route, which outputs basic stats for the server.
* Adds the /health and /success-moving-average routes
* which output basic stats for the server.
*/
export default (app) =>
!app
? false
: app.get('/health', (request, response) => {
response.send({
status: 'OK',
bootTime: serverStartTime,
uptime:
Math.floor(
(new Date().getTime() - serverStartTime.getTime()) / 1000 / 60
) + ' minutes',
version: pkgFile.version,
highchartsVersion: cache.version(),
averageProcessingTime: pool.averageTime(),
performedExports: pool.processedWorkCount(),
failedExports: pool.droppedWork(),
exportAttempts: pool.workAttempts(),
sucessRatio: (pool.processedWorkCount() / pool.workAttempts()) * 100,
// eslint-disable-next-line import/no-named-as-default-member
pool: pool.getPoolInfoJSON()
});
});
export default function addHealthRoutes(app) {
if (!app) {
return false;
}

app.get('/health', (_, res) => {
const stats = pool.getStats();
const period = successRates.length;
const movingAverage = calculateMovingAverage();

log(4, '[health.js] GET /health [200] - returning server health.');

res.send({
status: 'OK',
bootTime: serverStartTime,
uptime:
Math.floor(
(new Date().getTime() - serverStartTime.getTime()) / 1000 / 60
) + ' minutes',
version: pkgFile.version,
highchartsVersion: cache.version(),
averageProcessingTime: stats.spentAverage,
performedExports: stats.performedExports,
failedExports: stats.droppedExports,
exportAttempts: stats.exportAttempts,
sucessRatio: (stats.performedExports / stats.exportAttempts) * 100,
// eslint-disable-next-line import/no-named-as-default-member
pool: pool.getPoolInfoJSON(),

// Moving average
period,
movingAverage,
message: `Last ${period} minutes had a success rate of ${movingAverage.toFixed(2)}%.`,

// SVG/JSON attempts
svgExportAttempts: stats.exportFromSvgAttempts,
jsonExportAttempts: stats.performedExports - stats.exportFromSvgAttempts
});
});
}

0 comments on commit 56908ad

Please sign in to comment.