diff --git a/src/device-registry/bin/jobs/check-active-statuses.js b/src/device-registry/bin/jobs/check-active-statuses.js new file mode 100644 index 0000000000..7641913214 --- /dev/null +++ b/src/device-registry/bin/jobs/check-active-statuses.js @@ -0,0 +1,116 @@ +const constants = require("@config/constants"); +const log4js = require("log4js"); +const logger = log4js.getLogger( + `${constants.ENVIRONMENT} -- /bin/jobs/check-active-statuses-job` +); +const DeviceModel = require("@models/Device"); +const cron = require("node-cron"); +const ACTIVE_STATUS_THRESHOLD = 0; +const { logText, logObject } = require("@utils/log"); + +const checkActiveStatuses = async () => { + try { + // Check for Deployed devices with incorrect statuses + const activeIncorrectStatusCount = await DeviceModel( + "airqo" + ).countDocuments({ + isActive: true, + status: { $ne: "deployed" }, + }); + + const activeMissingStatusCount = await DeviceModel("airqo").countDocuments({ + isActive: true, + status: { $exists: false } || { $eq: null } || { $eq: "" }, + }); + + const activeIncorrectStatusResult = await DeviceModel("airqo").aggregate([ + { + $match: { + isActive: true, + status: { $ne: "deployed" }, + }, + }, + { + $group: { + _id: "$name", + }, + }, + ]); + + const activeMissingStatusResult = await DeviceModel("airqo").aggregate([ + { + $match: { + isActive: true, + status: { $exists: false } || { $eq: null } || { $eq: "" }, + }, + }, + { + $group: { + _id: "$name", + }, + }, + ]); + + const activeIncorrectStatusUniqueNames = activeIncorrectStatusResult.map( + (doc) => doc._id + ); + const activeMissingStatusUniqueNames = activeMissingStatusResult.map( + (doc) => doc._id + ); + + logObject("activeIncorrectStatusCount", activeIncorrectStatusCount); + logObject("activeMissingStatusCount", activeMissingStatusCount); + + const percentageActiveIncorrectStatus = + (activeIncorrectStatusCount / + (await DeviceModel("airqo").countDocuments({ isActive: true }))) * + 100; + const percentageActiveMissingStatus = + (activeMissingStatusCount / + (await DeviceModel("airqo").countDocuments({ isActive: true }))) * + 100; + + logObject( + "percentageActiveIncorrectStatus", + percentageActiveIncorrectStatus + ); + logObject("percentageActiveMissingStatus", percentageActiveMissingStatus); + + if ( + percentageActiveIncorrectStatus > ACTIVE_STATUS_THRESHOLD || + percentageActiveMissingStatus > ACTIVE_STATUS_THRESHOLD + ) { + logText( + `⁉️ Deployed devices with incorrect statuses (${activeIncorrectStatusUniqueNames.join( + ", " + )}) - ${percentageActiveIncorrectStatus.toFixed(2)}%` + ); + logger.info( + `⁉️ Deployed devices with incorrect statuses (${activeIncorrectStatusUniqueNames.join( + ", " + )}) - ${percentageActiveIncorrectStatus.toFixed(2)}%` + ); + + logText( + `⁉️ Deployed devices missing status (${activeMissingStatusUniqueNames.join( + ", " + )}) - ${percentageActiveMissingStatus.toFixed(2)}%` + ); + logger.info( + `⁉️ Deployed devices missing status (${activeMissingStatusUniqueNames.join( + ", " + )}) - ${percentageActiveMissingStatus.toFixed(2)}%` + ); + } + } catch (error) { + logText(`🐛🐛 Error checking active statuses: ${error.message}`); + logger.error(`🐛🐛 Error checking active statuses: ${error.message}`); + logger.error(`🐛🐛 Stack trace: ${error.stack}`); + } +}; + +logText("Active statuses job is now running....."); +const schedule = "30 */2 * * *"; // At minute 30 of every 2nd hour +cron.schedule(schedule, checkActiveStatuses, { + scheduled: true, +}); diff --git a/src/device-registry/bin/jobs/check-unassigned-devices-job.js b/src/device-registry/bin/jobs/check-unassigned-devices-job.js new file mode 100644 index 0000000000..027238a0b6 --- /dev/null +++ b/src/device-registry/bin/jobs/check-unassigned-devices-job.js @@ -0,0 +1,71 @@ +const constants = require("@config/constants"); +const log4js = require("log4js"); +const logger = log4js.getLogger( + `${constants.ENVIRONMENT} -- /bin/jobs/check-unassigned-devices-job` +); +const DeviceModel = require("@models/Device"); +const cron = require("node-cron"); +const UNASSIGNED_THRESHOLD = 0; +const { logText, logObject } = require("@utils/log"); + +const checkUnassignedDevices = async () => { + try { + const totalCount = await DeviceModel("airqo").countDocuments({ + isActive: false, + }); + + const result = await DeviceModel("airqo").aggregate([ + { + $match: { + isActive: false, + category: { $exists: false } || { $eq: "" }, + }, + }, + { + $group: { + _id: "$name", + }, + }, + ]); + + const unassignedDevicesCount = result.length; + const uniqueDeviceNames = result.map((doc) => doc._id); + logObject("unassignedDevicesCount", unassignedDevicesCount); + logObject("totalCount ", totalCount); + + if (unassignedDevicesCount === 0) { + return; + } + + const percentage = (unassignedDevicesCount / totalCount) * 100; + + logObject("percentage", percentage); + + if (percentage > UNASSIGNED_THRESHOLD) { + logText( + `🤦‍♀️🫣 ${percentage.toFixed( + 2 + )}% of deployed devices are not assigned to any category (${uniqueDeviceNames.join( + ", " + )})` + ); + logger.info( + `🤦‍♀️🫣 ${percentage.toFixed( + 2 + )}% of deployed devices are not assigned to any category (${uniqueDeviceNames.join( + ", " + )})` + ); + } + } catch (error) { + logText(`🐛🐛 Error checking unassigned devices: ${error.message}`); + logger.error(`🐛🐛 Error checking unassigned devices: ${error.message}`); + logger.error(`🐛🐛 Stack trace: ${error.stack}`); + } +}; + +logText("Unassigned devices job is now running....."); +const schedule = "30 */2 * * *"; // At minute 30 of every 2nd hour +cron.schedule(schedule, checkUnassignedDevices, { + scheduled: true, +}); diff --git a/src/device-registry/bin/jobs/check-unassigned-sites-job.js b/src/device-registry/bin/jobs/check-unassigned-sites-job.js new file mode 100644 index 0000000000..0367812a46 --- /dev/null +++ b/src/device-registry/bin/jobs/check-unassigned-sites-job.js @@ -0,0 +1,73 @@ +const constants = require("@config/constants"); +const log4js = require("log4js"); +const logger = log4js.getLogger( + `${constants.ENVIRONMENT} -- /bin/jobs/check-unassigned-sites-job` +); +const SitesModel = require("@models/Site"); +const cron = require("node-cron"); +const UNASSIGNED_THRESHOLD = 0; +const { logText, logObject } = require("@utils/log"); + +const checkUnassignedSites = async () => { + try { + // Count total number of active sites + const totalCount = await SitesModel("airqo").countDocuments({ + isOnline: true, + }); + + // Find sites with empty or non-existent grids array + const result = await SitesModel("airqo").aggregate([ + { + $match: { + isOnline: true, + grids: { $size: 0 }, + }, + }, + { + $group: { + _id: "$generated_name", + }, + }, + ]); + + const unassignedSiteCount = result.length; + const uniqueSiteNames = result.map((site) => site._id); + logObject("unassignedSiteCount", unassignedSiteCount); + logObject("totalCount", totalCount); + + if (unassignedSiteCount === 0) { + return; + } + + const percentage = (unassignedSiteCount / totalCount) * 100; + + logObject("percentage", percentage); + + if (percentage > UNASSIGNED_THRESHOLD) { + logText( + `⚠️🙉 ${percentage.toFixed( + 2 + )}% of active sites are not assigned to any grid (${uniqueSiteNames.join( + ", " + )})` + ); + logger.info( + `⚠️🙉 ${percentage.toFixed( + 2 + )}% of active sites are not assigned to any grid (${uniqueSiteNames.join( + ", " + )})` + ); + } + } catch (error) { + logText(`🐛🐛 Error checking unassigned sites: ${error.message}`); + logger.error(`🐛🐛 Error checking unassigned sites: ${error.message}`); + logger.error(`🐛🐛 Stack trace: ${error.stack}`); + } +}; + +logText("Unassigned sites job is now running....."); +const schedule = "30 */2 * * *"; // At minute 30 of every 2nd hour +cron.schedule(schedule, checkUnassignedSites, { + scheduled: true, +}); diff --git a/src/device-registry/bin/jobs/test/ut_check-active-statuses.js b/src/device-registry/bin/jobs/test/ut_check-active-statuses.js new file mode 100644 index 0000000000..74a855d0a6 --- /dev/null +++ b/src/device-registry/bin/jobs/test/ut_check-active-statuses.js @@ -0,0 +1,97 @@ +require("module-alias/register"); +const sinon = require("sinon"); +const chai = require("chai"); +const expect = chai.expect; +const sinonChai = require("sinon-chai"); + +const checkActiveStatuses = require("@bin/jobs/check-active-statuses"); + +const DeviceModelMock = sinon.mock(DeviceModel); +const logTextSpy = sinon.spy(console.log); +const logObjectSpy = sinon.spy(console.log); +const loggerErrorSpy = sinon.spy(logger.error); + +beforeEach(() => { + DeviceModelMock = sinon.mock(DeviceModel); + logTextSpy = sinon.spy(console.log); + logObjectSpy = sinon.spy(console.log); + loggerErrorSpy = sinon.spy(logger.error); +}); + +afterEach(() => { + sinon.restore(); +}); + +describe("checkActiveStatuses", () => { + describe("when devices have incorrect statuses", () => { + it("should log deployed devices with incorrect statuses", async () => { + DeviceModelMock.expects("countDocuments") + .twice() + .resolves(5); + + const result = await checkActiveStatuses(); + + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logObjectSpy).to.have.been.calledWith(sinon.match.object); + expect(logger.info).to.have.been.calledWith(sinon.match.string); + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logger.info).to.have.been.calledWith(sinon.match.string); + + sinon.assert.notCalled(loggerErrorSpy); + }); + }); + + describe("when devices have missing status fields", () => { + it("should log deployed devices missing status", async () => { + DeviceModelMock.expects("countDocuments") + .twice() + .resolves(3); + + const result = await checkActiveStatuses(); + + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logObjectSpy).to.have.been.calledWith(sinon.match.object); + expect(logger.info).to.have.been.calledWith(sinon.match.string); + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logger.info).to.have.been.calledWith(sinon.match.string); + + sinon.assert.notCalled(loggerErrorSpy); + }); + }); + + describe("when both conditions are met", () => { + it("should log both deployed devices with incorrect statuses and missing status fields", async () => { + DeviceModelMock.expects("countDocuments") + .thrice() + .resolves([5, 3]); + + const result = await checkActiveStatuses(); + + expect(logTextSpy).to.have.been.calledThrice(); + expect(logObjectSpy).to.have.been.calledTwice(); + expect(logger.info).to.have.been.calledTwice(); + expect(logger.info).to.have.been.calledWith(sinon.match.string); + expect(logger.info).to.have.been.calledWith(sinon.match.string); + + sinon.assert.notCalled(loggerErrorSpy); + }); + }); + + describe("when error occurs", () => { + it("should log the error message", async () => { + const error = new Error("Test error"); + + DeviceModelMock.expects("countDocuments").throws(error); + + await checkActiveStatuses(); + + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logger.error).to.have.been.calledWith(sinon.match.string); + expect(logger.error).to.have.been.calledWith(sinon.match.string); + expect(logger.error).to.have.been.calledWith(sinon.match.string); + + sinon.assert.notCalled(logObjectSpy); + sinon.assert.notCalled(logger.info); + }); + }); +}); diff --git a/src/device-registry/bin/jobs/test/ut_check-unassigned-devices.js b/src/device-registry/bin/jobs/test/ut_check-unassigned-devices.js new file mode 100644 index 0000000000..ddc00e1891 --- /dev/null +++ b/src/device-registry/bin/jobs/test/ut_check-unassigned-devices.js @@ -0,0 +1,85 @@ +require("module-alias/register"); +const sinon = require("sinon"); +const chai = require("chai"); +const expect = chai.expect; +const sinonChai = require("sinon-chai"); +const checkUnassignedDevices = require("@bin/jobs/check-unassigned-devices"); + +describe("checkUnassignedDevices", () => { + let DeviceModelMock; + let logTextSpy; + let logObjectSpy; + + beforeEach(() => { + DeviceModelMock = sinon.mock(DeviceModel); + logTextSpy = sinon.spy(console.log); + logObjectSpy = sinon.spy(console.log); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("when devices are assigned to categories", () => { + it("should not log anything", async () => { + DeviceModelMock.expects("countDocuments").resolves(100); + DeviceModelMock.expects("aggregate").resolves([ + { _id: "device1" }, + { _id: "device2" }, + ]); + + await checkUnassignedDevices(); + + expect(logTextSpy).to.not.have.been.calledWith(sinon.match.string); + expect(logObjectSpy).to.not.have.been.calledWith(sinon.match.any); + }); + }); + + describe("when devices are not assigned to categories", () => { + it("should log the percentage and unique device names", async () => { + UNASSIGNED_THRESHOLD = 50; + + DeviceModelMock.expects("countDocuments").resolves(100); + DeviceModelMock.expects("aggregate").resolves([ + { _id: "device1" }, + { _id: "device2" }, + ]); + + await checkUnassignedDevices(); + + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logObjectSpy).to.have.been.calledWith(sinon.match.object); + expect(logObjectSpy).to.have.been.calledWith(sinon.match.object); + }); + + it("should not log when percentage is below threshold", async () => { + UNASSIGNED_THRESHOLD = 60; + + DeviceModelMock.expects("countDocuments").resolves(100); + DeviceModelMock.expects("aggregate").resolves([ + { _id: "device1" }, + { _id: "device2" }, + ]); + + await checkUnassignedDevices(); + + expect(logTextSpy).to.not.have.been.calledWith(sinon.match.string); + expect(logObjectSpy).to.not.have.been.calledWith(sinon.match.object); + }); + }); + + describe("when an error occurs", () => { + it("should log the error message", async () => { + const error = new Error("Test error"); + + DeviceModelMock.expects("countDocuments").rejects.error(error); + + await checkUnassignedDevices(); + + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logger.error).to.have.been.calledWith(sinon.match.string); + expect(logger.error).to.have.been.calledWith(sinon.match.string); + }); + }); +}); diff --git a/src/device-registry/bin/jobs/test/ut_check-unassigned-sites-job.js b/src/device-registry/bin/jobs/test/ut_check-unassigned-sites-job.js new file mode 100644 index 0000000000..4354e8461d --- /dev/null +++ b/src/device-registry/bin/jobs/test/ut_check-unassigned-sites-job.js @@ -0,0 +1,92 @@ +require("module-alias/register"); +const sinon = require("sinon"); +const chai = require("chai"); +const expect = chai.expect; +const sinonChai = require("sinon-chai"); +const checkUnassignedSites = require("@bin/jobs/check-unassigned-sites-job"); + +// Mock the dependencies +const SitesModelMock = sinon.mock(SitesModel); +const logTextSpy = sinon.spy(console.log); +const logObjectSpy = sinon.spy(console.log); +const loggerErrorSpy = sinon.spy(logger.error); + +beforeEach(() => { + SitesModelMock = sinon.mock(SitesModel); + logTextSpy = sinon.spy(console.log); + logObjectSpy = sinon.spy(console.log); + loggerErrorSpy = sinon.spy(logger.error); +}); + +afterEach(() => { + sinon.restore(); +}); + +describe("checkUnassignedSites", () => { + describe("when there are unassigned sites", () => { + beforeEach(() => { + SitesModelMock.expects("countDocuments") + .once() + .resolves(100); + SitesModelMock.expects("aggregate") + .once() + .resolves([ + { + _id: "site1", + }, + { + _id: "site2", + }, + ]); + }); + + it("should log unassigned sites", async () => { + await checkUnassignedSites(); + + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logObjectSpy).to.have.been.calledTwice(); + expect(logger.info).to.have.been.calledWith(sinon.match.string); + + sinon.assert.notCalled(loggerErrorSpy); + }); + }); + + describe("when there are no unassigned sites", () => { + beforeEach(() => { + SitesModelMock.expects("countDocuments") + .once() + .resolves(100); + SitesModelMock.expects("aggregate") + .once() + .resolves([]); + }); + + it("should not log anything", async () => { + await checkUnassignedSites(); + + expect(logTextSpy).to.have.callCount(0); + expect(logObjectSpy).to.have.callCount(0); + expect(logger.info).to.have.callCount(0); + + sinon.assert.notCalled(loggerErrorSpy); + }); + }); + + describe("when error occurs", () => { + beforeEach(() => { + const error = new Error("Test error"); + SitesModelMock.expects("countDocuments").throws(error); + }); + + it("should log the error message", async () => { + await checkUnassignedSites(); + + expect(logTextSpy).to.have.been.calledWith(sinon.match.string); + expect(logger.error).to.have.been.calledTwice(); + expect(logger.error).to.have.been.calledWith(sinon.match.string); + + sinon.assert.notCalled(logObjectSpy); + sinon.assert.notCalled(logger.info); + }); + }); +}); diff --git a/src/device-registry/bin/server.js b/src/device-registry/bin/server.js index cae0c11909..7fcc8a885f 100644 --- a/src/device-registry/bin/server.js +++ b/src/device-registry/bin/server.js @@ -26,6 +26,9 @@ const stringify = require("@utils/stringify"); require("@bin/jobs/store-signals-job"); require("@bin/jobs/v2-store-readings-job"); require("@bin/jobs/v2-check-network-status-job"); +require("@bin/jobs/check-unassigned-devices-job"); +require("@bin/jobs/check-active-statuses"); +require("@bin/jobs/check-unassigned-sites-job"); if (isEmpty(constants.SESSION_SECRET)) { throw new Error("SESSION_SECRET environment variable not set");