diff --git a/src/adapters/localdb/index.js b/src/adapters/localdb/index.js index be6b2cf..7fd6ead 100644 --- a/src/adapters/localdb/index.js +++ b/src/adapters/localdb/index.js @@ -4,11 +4,13 @@ // Load Mongoose models. import Users from './models/users.js' +import Usage from './models/usage.js' class LocalDB { constructor () { // Encapsulate dependencies this.Users = Users + this.Usage = Usage } } diff --git a/src/adapters/localdb/models/usage.js b/src/adapters/localdb/models/usage.js new file mode 100644 index 0000000..9dec059 --- /dev/null +++ b/src/adapters/localdb/models/usage.js @@ -0,0 +1,15 @@ +/* + Usage data model +*/ + +// Global npm libraries +import mongoose from 'mongoose' + +const Usage = new mongoose.Schema({ + ip: { type: String }, + url: { type: String }, + method: { type: String }, + timestamp: { type: Date } +}) + +export default mongoose.model('usage', Usage) diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js index 385b918..320292f 100644 --- a/src/controllers/timer-controllers.js +++ b/src/controllers/timer-controllers.js @@ -23,58 +23,76 @@ class TimerControllers { this.debugLevel = localConfig.debugLevel + // Constants + this.cleanUsageInterval = 60000 * 60 // 1 hour + this.backupUsageInterval = 60000 * 10 // 10 minutes + // Encapsulate dependencies this.config = config // Bind 'this' object to all subfunctions. - this.exampleTimerFunc = this.exampleTimerFunc.bind(this) this.cleanUsage = this.cleanUsage.bind(this) - - // this.startTimers() + this.backupUsage = this.backupUsage.bind(this) } // Start all the time-based controllers. startTimers () { // Any new timer control functions can be added here. They will be started // when the server starts. - this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 60) - this.cleanUsageHandle = setInterval(this.cleanUsage, 60000 * 60) // 1 hour + this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval) + this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval) return true } stopTimers () { - clearInterval(this.optimizeWalletHandle) clearInterval(this.cleanUsageHandle) + clearInterval(this.backupUsageHandle) } - // Replace this example function with your own timer handler. - exampleTimerFunc (negativeTest) { + // Clean the usage state so that stats reflect the last 24 hours. + cleanUsage () { try { - console.log('Example timer controller executed.') + clearInterval(this.cleanUsageHandle) - if (negativeTest) throw new Error('test error') + const now = new Date() + console.log(`cleanUsage() Timer Controller executing at ${now.toLocaleString()}`) + + this.useCases.usage.cleanUsage() + + this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval) return true } catch (err) { - console.error('Error in exampleTimerFunc(): ', err) + console.error('Error in time-controller.js/cleanUsage(): ', err) + + this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval) // Note: Do not throw an error. This is a top-level function. return false } } - // Clean the usage state so that stats reflect the last 24 hours. - cleanUsage () { + // Backup the usage stats to the database + async backupUsage () { try { - const now = new Date() - console.log(`cleanUsage() Timer Controller executing at ${now.toLocaleString()}`) + clearInterval(this.backupUsageHandle) - this.useCases.usage.cleanUsage() + console.log('backupUsage() Timer Controller executing at ', new Date().toLocaleString()) + + // Clear the database of old usage data. + await this.useCases.usage.clearUsage() + + // Save the current usage snapshot to the database. + await this.useCases.usage.saveUsage() + + this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval) return true } catch (err) { - console.error('Error in time-controller.js/cleanUsage(): ', err) + console.error('Error in time-controller.js/backupUsage(): ', err) + + this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval) // Note: Do not throw an error. This is a top-level function. return false diff --git a/src/use-cases/index.js b/src/use-cases/index.js index 8ce82b3..dcbe301 100644 --- a/src/use-cases/index.js +++ b/src/use-cases/index.js @@ -24,6 +24,9 @@ class UseCases { // Run any startup Use Cases at the start of the app. async start () { + // Load the usage stats from the database + await this.usage.loadUsage() + console.log('Async Use Cases have been started.') return true diff --git a/src/use-cases/usage-use-cases.js b/src/use-cases/usage-use-cases.js index ce32f24..c6886c2 100644 --- a/src/use-cases/usage-use-cases.js +++ b/src/use-cases/usage-use-cases.js @@ -3,6 +3,10 @@ for tracking the usage of REST API and JSON RPC calls. This library is used by admins to keep an eye on how many API calls were made in a 24-hour and 1-hour time period. + + Usage stats are held in memory. But they are periodically backed up to the + Mongo database. On startup, the usage stats are loaded from the database. + This allows the usage stats to be persisted across restarts. */ // This global variable is used to share data between the REST middleware and @@ -19,11 +23,17 @@ class UsageUseCases { ) } + // Encapsulate dependencies + this.UsageModel = this.adapters.localdb.Usage + // Bind 'this' object to all subfunctions this.cleanUsage = this.cleanUsage.bind(this) this.getRestSummary = this.getRestSummary.bind(this) this.getTopIps = this.getTopIps.bind(this) this.getTopEndpoints = this.getTopEndpoints.bind(this) + this.clearUsage = this.clearUsage.bind(this) + this.saveUsage = this.saveUsage.bind(this) + this.loadUsage = this.loadUsage.bind(this) // State } @@ -108,6 +118,65 @@ class UsageUseCases { throw err } } + + // Clear the usage database data + async clearUsage () { + try { + await this.UsageModel.deleteMany({}) + + // Debugging: verify the database is empty + // Delete this code after debugging + const usage = await this.UsageModel.find({}) + console.log('clearUsage() usage: ', usage) + } catch (err) { + console.error('Error in usage-use-cases.js/clearUsage()') + throw err + } + } + + // Save the usage data to the database + async saveUsage (inObj = {}) { + try { + for (let i = 0; i < restCalls.length; i++) { + const thisRestCall = restCalls[i] + + // Debugging: delete this code after debugging + if (i === 5) { + console.log('saveUsage() thisRestCall: ', thisRestCall) + } + + const usageData = { + ip: thisRestCall.ip, + url: thisRestCall.url, + method: thisRestCall.method, + timestamp: thisRestCall.timestamp + } + + const usage = new this.UsageModel(usageData) + await usage.save() + } + } catch (err) { + console.error('Error in usage-use-cases.js/saveUsage()') + throw err + } + } + + // Load usage data from the database + async loadUsage () { + try { + const usage = await this.UsageModel.find({}) + // console.log('usage: ', usage) + + if (usage[5]) { + console.log('loadUsage() usage[5]: ', usage[5]) + } + + restCalls = usage + } catch (err) { + console.error('Error in usage-use-cases.js/loadUsage(): ', err) + // throw err + } + } } // This Koa middleware is called any time there is a REST API. It logs the diff --git a/test/unit/controllers/timer-controllers.unit.js b/test/unit/controllers/timer-controllers.unit.js index fda1b72..3daacdc 100644 --- a/test/unit/controllers/timer-controllers.unit.js +++ b/test/unit/controllers/timer-controllers.unit.js @@ -66,19 +66,19 @@ describe('#Timer-Controllers', () => { }) }) - describe('#exampleTimerFunc', () => { - it('should kick off the Use Case', async () => { - const result = await uut.exampleTimerFunc() + // describe('#exampleTimerFunc', () => { + // it('should kick off the Use Case', async () => { + // const result = await uut.exampleTimerFunc() - assert.equal(result, true) - }) + // assert.equal(result, true) + // }) - it('should return false on error', async () => { - const result = await uut.exampleTimerFunc(true) + // it('should return false on error', async () => { + // const result = await uut.exampleTimerFunc(true) - assert.equal(result, false) - }) - }) + // assert.equal(result, false) + // }) + // }) describe('#cleanUsage', () => { it('should kick off the Use Case', async () => { @@ -94,4 +94,21 @@ describe('#Timer-Controllers', () => { assert.equal(result, false) }) }) + + describe('#backupUsage', () => { + it('should kick off the Use Case', async () => { + const result = await uut.backupUsage() + + assert.equal(result, true) + }) + + it('should return false on error', async () => { + sandbox.stub(uut.useCases.usage, 'clearUsage').throws(new Error('test error')) + // sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error')) + + const result = await uut.backupUsage() + + assert.equal(result, false) + }) + }) }) diff --git a/test/unit/mocks/use-cases/index.js b/test/unit/mocks/use-cases/index.js index 73979c3..a915fa5 100644 --- a/test/unit/mocks/use-cases/index.js +++ b/test/unit/mocks/use-cases/index.js @@ -47,6 +47,14 @@ class UsageUseCaseMock { async getTopEndpoints(existingUser, newData) { return true } + + async clearUsage() { + return true + } + + async saveUsage() { + return true + } } class UseCasesMock {