From 974dab5e53100b96a314e809f9614c9db13ad021 Mon Sep 17 00:00:00 2001 From: zlatko-ms Date: Thu, 1 Sep 2022 14:23:47 +0200 Subject: [PATCH 1/4] - added performance tracker features - wired connectivity servics to perfromance tracker - exposed performance counters via dedicated url ( /perfs ) --- app/app.js | 24 +++-- app/handlers/githubForwarder.js | 6 +- app/handlers/health.js | 1 + app/handlers/hello.js | 6 +- app/handlers/perfs.js | 18 ++++ app/handlers/privateForwarders.js | 10 +- app/util/perf.js | 139 +++++++++++++++++++++++++++ package.json | 1 + test/perfs.js | 151 ++++++++++++++++++++++++++++++ test/response.js | 6 +- 10 files changed, 344 insertions(+), 18 deletions(-) create mode 100644 app/handlers/perfs.js create mode 100644 app/util/perf.js create mode 100644 test/perfs.js diff --git a/app/app.js b/app/app.js index 666185d..aa03c13 100644 --- a/app/app.js +++ b/app/app.js @@ -6,20 +6,30 @@ var express = require('express'); const { Logger } = require('@util/logger.js'); var { healthProbeHandler } = require('@handlers/health.js'); var { helloHandler } = require('@handlers//hello.js'); -var { spokeForwarder, onpremForwarder } = require('@handlers/privateForwarders.js'); +var { forwardCallHandler } = require('@handlers/privateForwarders.js'); var { githubForwarder } = require('@handlers/githubForwarder.js'); -//var { onpremForwarder } = require('@handlers/onpremForwarder.js'); +var { perfHandler } = require('@handlers/perfs.js') // configuration -var { appPort } = require('@util/configuration.js'); +var { appPort , forwardSpokeUrl, forwardOnPremUrl } = require('@util/configuration.js'); + +// service handler functions +var handlerConnectivityLocal = function(req,res) { helloHandler(req,res,"connectivity/local") } +var handlerConnectivitySpoke = function(req,res) { forwardCallHandler(req,res,"connectivity/spoke",forwardSpokeUrl,"spoke")} +var handlerConnectivityOnPrem = function(req,res) { forwardCallHandler(req,res,"connectivity/onprem",forwardOnPremUrl,"onprem")} +var handlerConnectivityPublic = function(req,res) { githubForwarder(req,res,"connectivity/public") } // app Logger.info("starting backend"); var app = express(); + +// url routing app.get('/health', healthProbeHandler) -app.get('/connectivity/local', helloHandler); -app.get('/connectivity/spoke', spokeForwarder ); -app.get('/connectivity/public', githubForwarder); -app.get('/connectivity/onprem', onpremForwarder); +app.get('/perfs',perfHandler) +app.get('/connectivity/local', handlerConnectivityLocal); +app.get('/connectivity/spoke', handlerConnectivitySpoke ); +app.get('/connectivity/onprem', handlerConnectivityOnPrem); +app.get('/connectivity/public', handlerConnectivityPublic); + app.listen(appPort); Logger.info("backend started, listening on port="+appPort); diff --git a/app/handlers/githubForwarder.js b/app/handlers/githubForwarder.js index 1910296..5206198 100644 --- a/app/handlers/githubForwarder.js +++ b/app/handlers/githubForwarder.js @@ -3,15 +3,17 @@ const { Logger } = require('@util/logger.js'); var { responseBuilder } = require('@util/response.js'); const url= require('url'); const axios = require('axios'); - +const { BackendPerformanceTracker } = require('@util/perf.js'); const githubUsername = process.env.HELLOER_FORWARDER_GITHUB_USERNAME || "funkomatic"; -module.exports.githubForwarder = function (req,res) { +module.exports.githubForwarder = function (req,res,serviceName) { Logger.info("recv public forwarding request, retrieving "+githubUsername+" repos from github"); var callUrl = "https://api.github.com/users/"+githubUsername+"/repos"; var responsePayload=responseBuilder(req); + + BackendPerformanceTracker.addHit(serviceName) axios.get(callUrl).then(resAxios => { var repoCount = resAxios.data.length; diff --git a/app/handlers/health.js b/app/handlers/health.js index f7e5a06..dabec72 100644 --- a/app/handlers/health.js +++ b/app/handlers/health.js @@ -3,6 +3,7 @@ const { Logger } = require('@util/logger.js'); var { responseBuilder } = require('@util/response.js'); const { logProbeCalls } = require('@util/configuration.js') +const { BackendPerformanceTracker } = require('@util/perf.js'); module.exports.healthProbeHandler = function(req,res) { if (logProbeCalls) { diff --git a/app/handlers/hello.js b/app/handlers/hello.js index fbfcda2..cd3c61e 100644 --- a/app/handlers/hello.js +++ b/app/handlers/hello.js @@ -1,12 +1,14 @@ const { Logger } = require('@util/logger.js'); var { responseBuilder } = require('@util/response.js'); +const { BackendPerformanceTracker } = require('@util/perf.js'); -module.exports.helloHandler = function(req,res) { +module.exports.helloHandler = function(req,res,serviceName) { Logger.info("recv hello request"); + BackendPerformanceTracker.addHit(serviceName) var responsePayload = responseBuilder(req); responsePayload['message']="hello"; res.json(responsePayload); - Logger.info("sent hello response to client from "+responsePayload.request_source_ip); + Logger.info("sent "+serviceName+" response to client from "+responsePayload.request_source_ip); } diff --git a/app/handlers/perfs.js b/app/handlers/perfs.js new file mode 100644 index 0000000..3d039d3 --- /dev/null +++ b/app/handlers/perfs.js @@ -0,0 +1,18 @@ +const { Logger } = require('@util/logger.js'); +const { forwardSpokeUrl, forwardOnPremUrl} = require('@util/configuration.js') +var { responseBuilder } = require('@util/response.js'); +const { BackendPerformanceTracker } = require('@util/perf.js'); +const axios = require('axios'); + +module.exports.perfHandler= function(req,res) { + Logger.info("recv hello request"); + var responsePayload = responseBuilder(req); + + + responsePayload['perfs']= { + 'aggRps' : BackendPerformanceTracker.getAggRps(), + 'services' : BackendPerformanceTracker.getServiceBreakdown() + } + res.json(responsePayload); + Logger.info("sent perf stats to client from "+responsePayload.request_source_ip); +} diff --git a/app/handlers/privateForwarders.js b/app/handlers/privateForwarders.js index 98dac11..b796cc4 100644 --- a/app/handlers/privateForwarders.js +++ b/app/handlers/privateForwarders.js @@ -1,12 +1,14 @@ const { Logger } = require('@util/logger.js'); -const { forwardSpokeUrl, forwardOnPremUrl} = require('@util/configuration.js') var { responseBuilder } = require('@util/response.js'); const axios = require('axios'); +const { BackendPerformanceTracker } = require('@util/perf.js'); -function forwardCall(req,res,forwardUrl,label) { +module.exports.forwardCallHandler = function(req,res,serviceName,forwardUrl,label) { Logger.info("recv private forwarding request, using private endpoint "+forwardUrl); + var responsePayload=responseBuilder(req); + BackendPerformanceTracker.addHit(serviceName); axios.get(forwardUrl).then(resAxios => { responsePayload[label+"_status"]="success"; @@ -20,7 +22,7 @@ function forwardCall(req,res,forwardUrl,label) { responsePayload[label+"_response"]=err; res.status(500).json(responsePayload); }); + }; -module.exports.onpremForwarder = function (req,res) { return forwardCall(req,res,forwardOnPremUrl,"onprem") } -module.exports.spokeForwarder = function (req,res) { return forwardCall(req,res,forwardSpokeUrl,"spoke") } \ No newline at end of file + diff --git a/app/util/perf.js b/app/util/perf.js new file mode 100644 index 0000000..9a2f3b7 --- /dev/null +++ b/app/util/perf.js @@ -0,0 +1,139 @@ + + +const { Logger } = require('@util/logger.js'); +var format = require('date-format'); +const { loggers } = require('winston'); + +const dateFormat = 'yyyyMMddhhmm' + +let ServicePerfRecord = class { + + constructor(start, end, count) { + this.startDate = start + this.endDate = end + this.avgRps = count + } + + set startDate(date) { this._startDate = date } + get startDate() { return this._startDate } + + get endDate() { return this._endDate } + set endDate(date) { this._endDate = date } + + get avgRps() { return this._avgHitsPerSecond } + set avgRps(val) { this._avgHitsPerSecond = val } +} + + + +let ServicePerfTracker = class { + + constructor(serviceName) { + this._serviceName = serviceName + this._records = new Map() + } + + addHit(date = new Date()) { + let key = format.asString(dateFormat, date); + var hits = this._records.has(key) ? this._records.get(key) : 0 + hits++ + this._records.set(key, hits) + this._purgeOldRecords() + } + + _purgeOldRecords(hours = 2) { + var keysToRemove = new Set() + var lastDateToKeep = new Date() + lastDateToKeep.setHours(lastDateToKeep.getHours() - hours) + let lastDateToKeepString = format.asString(dateFormat, lastDateToKeep); + let lastDateToKeepAsInt = parseInt(lastDateToKeepString) + this._records.forEach((value, key) => { + if (parseInt(key) <= lastDateToKeepAsInt) { + keysToRemove.add(key) + } + }) + keysToRemove.forEach(k => { + this._records.delete(k) + }); + } + + getServicePerfRecord() { + + var datesArray = [] + var hits = 0; + this._records.forEach((v, k) => { + var keyDate = format.parse(dateFormat, k) + datesArray.push(keyDate) + hits += v + }) + + let minutesCount = datesArray.length + if (minutesCount>0) { + + datesArray.sort((a, b) => {return a - b}) + var avgHits = hits / (minutesCount * 60) + + var startDate = datesArray[0] + startDate.setSeconds(0) + startDate.setMilliseconds(0) + + var endDate = datesArray.pop() + endDate.setSeconds(0) + endDate.setMilliseconds(0) + + return new ServicePerfRecord(startDate, endDate, avgHits) + } + + return new ServicePerfRecord(new Date, new Date, Number.NaN); + } + + getIndicatorHistory() { + return new Map([...this._records].sort((a, b) => String(a[0]).localeCompare(b[0]))) + } + + } + + let PerformanceTracker = class { + + constructor() { + this._serviceTrackers = new Map() + } + + addHit(serviceName, date = new Date) { + var svcTracker = this._serviceTrackers.has(serviceName) ? this._serviceTrackers.get(serviceName) : new ServicePerfTracker(serviceName) + svcTracker.addHit(date) + this._serviceTrackers.set(serviceName, svcTracker) + } + + getServicePerfRecord(serviceName) { + var svcTracker = this._serviceTrackers.has(serviceName) ? this._serviceTrackers.get(serviceName) : new ServicePerfTracker(serviceName) + return svcTracker.getServicePerfRecord() + } + + getTrackedServices() { + return new Set(this._serviceTrackers.keys()) + } + + getAggRps() { + + var totalHits = 0 + this._serviceTrackers.forEach( (v,k) => { + let serviceRecord = v.getServicePerfRecord() + totalHits+=serviceRecord.avgRps + }) + return totalHits; + } + + getServiceBreakdown() { + var ret = new Map() + this._serviceTrackers.forEach( (v,k) => { ret[k]=v.getServicePerfRecord() }); + return ret; + } + + } + + + +var BackendPerformanceTracker = new PerformanceTracker() + +module.exports = { ServicePerfRecord , ServicePerfTracker , PerformanceTracker, BackendPerformanceTracker } diff --git a/package.json b/package.json index 24b7202..2792708 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "app:test": "mocha --reporter spec" }, "dependencies": { + "date-format" : "^4.0.13", "axios": "^0.27.2", "cookie-parser": "~1.4.4", "debug": "~4.3.2", diff --git a/test/perfs.js b/test/perfs.js new file mode 100644 index 0000000..6cad170 --- /dev/null +++ b/test/perfs.js @@ -0,0 +1,151 @@ +require('module-alias/register') + +var assert = require("chai").assert +var { ServicePerfRecord } = require('@util/perf.js'); +const { Logger } = require('@util/logger.js'); +const { ServicePerfTracker, PerformanceTracker } = require('@util/perf.js'); + +function assertDatesMatchToMinutes(date1,date2) { + assert.equal(date1.getFullYear(),date2.getFullYear(),"years match") + assert.equal(date1.getMonth(),date2.getMonth(),"months match") + assert.equal(date1.getDay(),date2.getDay(),"days match") + assert.equal(date1.getHours(),date2.getHours(),"hours match") + assert.equal(date1.getMinutes(),date2.getMinutes(),"minutes match") +} + +describe("Performance Tracking", function() { + + describe("Single Service Performance Tracker", () => { + + it("service hits are correctly aggregated and summed on the time dimension", () => { + + let now = new Date(); + const numberOfHits = 60 + + let spt = new ServicePerfTracker("svc/a") + for (let i = 0; i < numberOfHits; i++) { spt.addHit(now) } + + let perfRecord = spt.getServicePerfRecord() + assert.equal(perfRecord.avgRps,1,"average is correctly computed") + assertDatesMatchToMinutes(perfRecord.startDate,now) + assertDatesMatchToMinutes(perfRecord.endDate,now) + + }); + + it("non tracked services provide a NaN as metric", () => { + let spt = new ServicePerfTracker("whatever") + assert.equal(isNaN(spt.getServicePerfRecord().avgRps),true,"non tracked returned NaN as average") + }); + + + it("old service hits are correctly purged", () => { + + var then = new Date(); + then.setHours(then.getHours() - 5) + + let spt = new ServicePerfTracker("whatever") + for (let i = 0; i < 60; i++) { spt.addHit(then) } + + // then check that the data exceending the purge treshold are removed + let avgHits = spt.getServicePerfRecord() + assert.isTrue(isNaN(avgHits.avgRps),"old entries are correctly purged") + + var now = new Date() + assertDatesMatchToMinutes(avgHits.startDate,now) + assertDatesMatchToMinutes(avgHits.endDate,now) + + }); + + it("history of hits is correctly exposed", () => { + + let now = new Date(); + + let aMinuteAgo = new Date() + aMinuteAgo.setMinutes( aMinuteAgo.getMinutes - 1) + + let spt = new ServicePerfTracker("whatever") + spt.addHit(aMinuteAgo) + spt.addHit(now) + assert.equal(spt.getIndicatorHistory().size,2,"two hits in two distinct minutes produce two history perf records") + + }); + + + }); + + describe("Global Services Performance Tracker", () => { + + it("multiple service hits are tracked", () => { + let gt = new PerformanceTracker() + gt.addHit("svc/a") + gt.addHit("svc/b") + let services = new Set(gt.getTrackedServices()) + assert.isTrue(services.has("svc/a"),"service a has a hit") + assert.isTrue(services.has("svc/b"),"service b has a hit") + }); + + it("multiple service hits are correctly computed on RPS level", () => { + + let gt = new PerformanceTracker() + let svcA = 60 + let svcB = 120 + + for (let i=0;i { + let gt = new PerformanceTracker() + let svcA = 60 + let svcB = 120 + + for (let i=0;i { + + let now = new Date() + let gt = new PerformanceTracker() + gt.addHit("svc/a",now) + gt.addHit("svc/b",now) + + let serviceBreakdown = gt.getServiceBreakdown() + + assert.isDefined(serviceBreakdown['svc/a'],"svc/a has perf tracker entry") + assert.isDefined(serviceBreakdown['svc/b'],"svc/b has perf tracker entry ") + + let svcARecord = serviceBreakdown['svc/a'] + let svcBRecord = serviceBreakdown['svc/a'] + + assert.isTrue(ServicePerfRecord.prototype.isPrototypeOf(svcARecord),"svc/a perf tracker is of the correct class") + assert.isTrue(ServicePerfRecord.prototype.isPrototypeOf(svcBRecord),"svc/b perf tracker is of the correct class") + + assert.isDefined(svcARecord.startDate,"svc/a perf record has start date") + assert.isDefined(svcARecord.endDate,"svc/a perf record has end date") + assert.isDefined(svcBRecord.startDate,"svc/b perf record has start date") + assert.isDefined(svcBRecord.endDate,"svc/b perf record has ebd date") + + assertDatesMatchToMinutes(svcARecord.startDate,now) + + assert.isDefined(serviceBreakdown['svc/a'],"service a breakdown is available") + assert.isDefined(serviceBreakdown['svc/a'].startDate,"service a startdate is available") + assertDatesMatchToMinutes(serviceBreakdown['svc/a'].startDate,now,"service a startdate is correct") + + assert.isDefined(serviceBreakdown['svc/b'],"service b breakdown is available") + assert.isDefined(serviceBreakdown['svc/b'].startDate,"service b startdate is available") + assertDatesMatchToMinutes(serviceBreakdown['svc/b'].startDate,now,"service b startdate is correct") + + }); + + + + + }); + }); diff --git a/test/response.js b/test/response.js index f62b18a..ad1b630 100644 --- a/test/response.js +++ b/test/response.js @@ -10,12 +10,12 @@ const unknown = "unknown" describe("Response Builder", function() { describe("Source Address Fetching", function() { - it("returns 'unknown' if source address cannot be fetched", function() { + it("returns 'unknown' if source address cannot be fetched", () => { var request = httpMocks.createRequest({}); var responsePayload = responseBuilder(request); expect(responsePayload.request_source_ip).to.equal(unknown) }); - it("fetches the source address from request header map", function() { + it("fetches the source address from request header map", () => { var request = httpMocks.createRequest({ headers : { 'x-forwarded-for' : localhost @@ -25,7 +25,7 @@ describe("Response Builder", function() { expect(responsePayload.request_source_ip).to.equal(localhost) }); - it("feches the source address from the request socket map", function() { + it("feches the source address from the request socket map", () => { var request = httpMocks.createRequest({ socket : { From 52de510761b3d811a07479bfd476fa9bd441103a Mon Sep 17 00:00:00 2001 From: zlatko-ms Date: Thu, 1 Sep 2022 14:40:08 +0200 Subject: [PATCH 2/4] update readme, small refac on the app exposition --- README.md | 8 +++++++- app/app.js | 16 +++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0f3a52c..f045b74 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ During those projects you will be required to validate all of your LZ design, mo In order to test your connectivity and eventually deliver a POC/Study you'll need a tiny workload to deploy on different places of the cloud and hybrid infrastructure. -This is exactly the purpose of this workload, that can be deployed on most of the hosting infrastructures (VM, Functions, Kubernetes, Application services) and can be used to test direct or transitive connectivity to your Cloud and hybrid architecture. +This is exactly the purpose of the Helloer that can be deployed on most of the hosting infrastructures (VM, Functions, Kubernetes, Application services) and used to test direct or transitive connectivity to your Cloud and hybrid architecture. + +Might you need to generate some traffic on the Helloer, you can eventually use the companion [Greeter](https://github.com/zlatko-ms/pgreeter) app that will act as a client of the helloer in otder to demonstrate/assess the connectivity constraints. ## Features @@ -48,6 +50,10 @@ Most of the available infrastructures will require healthchecks, being it on the In order to integrate with those infrastructures you can use the **'/health'** endpoint of the workload that will return a small payload with a 200 HTTP status, which is more then sufficient to deploy the workload in those conditions +### Performance Tracking + +The application integrates very basic perfromance tracking that simply exposes the aggregated RPS (request per second) metric. It can come handy in order to check quota limits or basic network connectivity performance, usefull when benching different Load Balancer solutions. + ## Code The code is pretty staightfoward and easy to update : diff --git a/app/app.js b/app/app.js index aa03c13..eb885f2 100644 --- a/app/app.js +++ b/app/app.js @@ -13,23 +13,17 @@ var { perfHandler } = require('@handlers/perfs.js') // configuration var { appPort , forwardSpokeUrl, forwardOnPremUrl } = require('@util/configuration.js'); -// service handler functions -var handlerConnectivityLocal = function(req,res) { helloHandler(req,res,"connectivity/local") } -var handlerConnectivitySpoke = function(req,res) { forwardCallHandler(req,res,"connectivity/spoke",forwardSpokeUrl,"spoke")} -var handlerConnectivityOnPrem = function(req,res) { forwardCallHandler(req,res,"connectivity/onprem",forwardOnPremUrl,"onprem")} -var handlerConnectivityPublic = function(req,res) { githubForwarder(req,res,"connectivity/public") } - // app Logger.info("starting backend"); var app = express(); -// url routing +// exposed services app.get('/health', healthProbeHandler) app.get('/perfs',perfHandler) -app.get('/connectivity/local', handlerConnectivityLocal); -app.get('/connectivity/spoke', handlerConnectivitySpoke ); -app.get('/connectivity/onprem', handlerConnectivityOnPrem); -app.get('/connectivity/public', handlerConnectivityPublic); +app.get('/connectivity/local', (req,res) => helloHandler(req,res,"connectivity/local") ); +app.get('/connectivity/spoke', (req,res) => forwardCallHandler(req,res,"connectivity/spoke",forwardSpokeUrl,"spoke")); +app.get('/connectivity/onprem', (req,res) => forwardCallHandler(req,res,"connectivity/onprem",forwardOnPremUrl,"onprem")); +app.get('/connectivity/public', (req,res) => githubForwarder(req,res,"connectivity/public")); app.listen(appPort); Logger.info("backend started, listening on port="+appPort); From 03b2906ac87bfc6449f7eb58e09b451fc137c58b Mon Sep 17 00:00:00 2001 From: zlatko-ms Date: Thu, 1 Sep 2022 14:40:45 +0200 Subject: [PATCH 3/4] updated version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2792708..cd98e55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "helloer", - "version": "1.0.4", + "version": "1.0.5", "private": true, "scripts": { "app:clean": "rm -rf node_modules/* && rm -f *.log && rm -f helloer-dist.*.gz", From fa3d66fb1d4c4dca121aef5d8bb1e65d2db80d63 Mon Sep 17 00:00:00 2001 From: zlatko-ms Date: Thu, 1 Sep 2022 14:42:06 +0200 Subject: [PATCH 4/4] Updated doc, added perf url --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f045b74..3e144ad 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ The following table lists the used environnement variables : | /connectivity/public | translates calls to github api | | /connectivity/spoke | translates calls to other spoke http server (you can use a dedicated helloer as well) | | /connectivity/onprem | translates calls to on prem http server (uou can use a dedicated helloer as well) | +| /perfs | exposition of basic aggregated Request Per Second metric as well as breakdown per service | ### HTTP Responses