From 764905b04187a5bdb31ac896996d304f31f929c7 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sat, 7 Dec 2024 21:25:10 -0800 Subject: [PATCH] fix(usage): Adding endpoints to analyize 24 hour usage of the REST API --- src/controllers/rest-api/usage/controller.js | 50 +++++++++++++- src/controllers/rest-api/usage/index.js | 2 + src/controllers/timer-controllers.js | 17 ++++- src/use-cases/usage-use-cases.js | 69 +++++++++++++++++++- 4 files changed, 134 insertions(+), 4 deletions(-) diff --git a/src/controllers/rest-api/usage/controller.js b/src/controllers/rest-api/usage/controller.js index 4c05da7..d78db1e 100644 --- a/src/controllers/rest-api/usage/controller.js +++ b/src/controllers/rest-api/usage/controller.js @@ -29,6 +29,8 @@ class UsageRESTControllerLib { // Bind 'this' object to all subfunctions this.getStatus = this.getStatus.bind(this) + this.getTopIps = this.getTopIps.bind(this) + this.getTopEndpoints = this.getTopEndpoints.bind(this) this.handleError = this.handleError.bind(this) } @@ -42,10 +44,10 @@ class UsageRESTControllerLib { * curl -H "Content-Type: application/json" -X GET localhost:5020/usage * */ - async getStatus (ctx) { + getStatus (ctx) { try { // const status = await this.adapters.ipfs.getStatus() - const status = await this.useCases.usage.getRestSummary() + const status = this.useCases.usage.getRestSummary() ctx.body = { status } } catch (err) { @@ -55,6 +57,50 @@ class UsageRESTControllerLib { } } + /** + * @api {get} /usage/ips Get top IP addresses consuming the REST API + * @apiPermission public + * @apiName GetUsageIPs + * @apiGroup REST Usage + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5020/usage/ips + * + */ + getTopIps (ctx) { + try { + const ips = this.useCases.usage.getTopIps() + + ctx.body = { ips } + } catch (err) { + wlogger.error('Error in usage/controller.js/getTopIps(): ') + // ctx.throw(422, err.message) + this.handleError(ctx, err) + } + } + + /** + * @api {get} /usage/endpoints Get top endpoints consumed from the REST API + * @apiPermission public + * @apiName GetUsageEndpoints + * @apiGroup REST Usage + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5020/usage/endpoints + * + */ + getTopEndpoints (ctx) { + try { + const endpoints = this.useCases.usage.getTopEndpoints() + + ctx.body = { endpoints } + } catch (err) { + wlogger.error('Error in usage/controller.js/getTopEndpoints(): ') + // ctx.throw(422, err.message) + this.handleError(ctx, err) + } + } + // DRY error handler handleError (ctx, err) { // If an HTTP status is specified by the buisiness logic, use that. diff --git a/src/controllers/rest-api/usage/index.js b/src/controllers/rest-api/usage/index.js index 64733b2..f0b1004 100644 --- a/src/controllers/rest-api/usage/index.js +++ b/src/controllers/rest-api/usage/index.js @@ -52,6 +52,8 @@ class UsageRouter { // Define the routes and attach the controller. this.router.get('/', this.usageRESTController.getStatus) + this.router.get('/ips', this.usageRESTController.getTopIps) + this.router.get('/endpoints', this.usageRESTController.getTopEndpoints) // Attach the Controller routes to the Koa app. app.use(this.router.routes()) diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js index daa0c64..b96b224 100644 --- a/src/controllers/timer-controllers.js +++ b/src/controllers/timer-controllers.js @@ -28,6 +28,7 @@ class TimerControllers { // Bind 'this' object to all subfunctions. this.exampleTimerFunc = this.exampleTimerFunc.bind(this) + this.cleanUsage = this.cleanUsage.bind(this) // this.startTimers() } @@ -36,13 +37,15 @@ class TimerControllers { startTimers () { // Any new timer control functions can be added here. They will be started // when the server starts. - this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 10) + this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 60) + this.cleanUsageHandle = setInterval(this.cleanUsage, 60000 * 1) // 1 hour return true } stopTimers () { clearInterval(this.optimizeWalletHandle) + clearInterval(this.cleanusageHandle) } // Replace this example function with your own timer handler. @@ -60,6 +63,18 @@ class TimerControllers { return false } } + + // Clean the usage state so that stats reflect the last 24 hours. + cleanUsage () { + try { + this.useCases.usage.cleanUsage() + } catch (err) { + console.error('Error in time-controller.js/cleanUsage(): ', err) + + // Note: Do not throw an error. This is a top-level function. + return false + } + } } export default TimerControllers diff --git a/src/use-cases/usage-use-cases.js b/src/use-cases/usage-use-cases.js index 0920031..356485a 100644 --- a/src/use-cases/usage-use-cases.js +++ b/src/use-cases/usage-use-cases.js @@ -7,7 +7,7 @@ // This global variable is used to share data between the REST middleware and // the Usage Use Case class instance. -const restCalls = [] +let restCalls = [] class UsageUseCases { constructor (localConfig = {}) { @@ -21,10 +21,27 @@ class UsageUseCases { // Bind 'this' object to all subfunctions this.getRestSummary = this.getRestSummary.bind(this) + this.getTopIps = this.getTopIps.bind(this) + this.getTopEndpoints = this.getTopEndpoints.bind(this) // State } + // Clean up the state by removing entries that are older than 24 hours. This + // ensures stats reflect only the last 24 hours. + // This function is called by a Timer Controller. + cleanUsage () { + try { + const now = new Date() + const twentyFourHoursAgo = now.getTime() - (60000 * 60 * 24) + + restCalls = restCalls.filter(x => x.timestamp > twentyFourHoursAgo) + } catch (err) { + console.error('Error in usage-use-cases.js/cleanUsage()') + throw err + } + } + // Track the calls to a REST API getRestSummary (inObj = {}) { try { @@ -36,6 +53,56 @@ class UsageUseCases { throw err } } + + // Get the top 20 IP addresses from the stats. + getTopIps () { + try { + const ips = restCalls.map(x => x.ip) + + // Create a Map to count occurrences of each IP address string + const countMap = new Map() + ips.forEach(ip => { + countMap.set(ip, (countMap.get(ip) || 0) + 1) + }) + + // Convert the Map into an array of objects with `str` and `cnt` properties + const result = Array.from(countMap, ([ip, cnt]) => ({ ip, cnt })) + + // Sort the results by the `cnt` property in descending order + result.sort((a, b) => b.cnt - a.cnt) + + // Ensure the result has at most 20 elements + return result.slice(0, 20) + } catch (err) { + console.error('Error in usage-use-cases.js/getTopIps()') + throw err + } + } + + // Get the top 20 most consumed endpoints. + getTopEndpoints () { + try { + const endpoints = restCalls.map(x => `${x.method} ${x.url}`) + + // Create a Map to count occurrences of each IP address string + const countMap = new Map() + endpoints.forEach(endpoint => { + countMap.set(endpoint, (countMap.get(endpoint) || 0) + 1) + }) + + // Convert the Map into an array of objects with `str` and `cnt` properties + const result = Array.from(countMap, ([endpoint, cnt]) => ({ endpoint, cnt })) + + // Sort the results by the `cnt` property in descending order + result.sort((a, b) => b.cnt - a.cnt) + + // Ensure the result has at most 20 elements + return result.slice(0, 20) + } catch (err) { + console.error('Error in usage-use-cases.js/getTopEndpoints()') + throw err + } + } } // This Koa middleware is called any time there is a REST API. It logs the