diff --git a/admin/index.html b/admin/index.html index 1bc2077..e3d41ef 100644 --- a/admin/index.html +++ b/admin/index.html @@ -22,6 +22,14 @@ color: red; } + .task-message.task-result-okay { + color: green; + } + + .task-message.task-result-warning { + color: red; + } + .task-description { max-width: 220px; } diff --git a/admin/js/main.js b/admin/js/main.js index e4cee3d..3eb0c82 100644 --- a/admin/js/main.js +++ b/admin/js/main.js @@ -383,7 +383,8 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest } return 'task-' + field + ' ' + (task.values.enabled ? 'task-enabled' : 'task-disabled') + ' ' - + 'task-state-' + task.values.state; + + 'task-state-' + task.values.state + ' ' + + 'task-result-' + (task.values.result ? task.values.result : 'none'); }; } @@ -403,6 +404,7 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest nga.field('description').cssClasses(taskClasses('description')), nga.field('schedule').cssClasses(taskClasses('schedule')), nga.field('state').cssClasses(taskClasses('state')), + nga.field('message').cssClasses(taskClasses('message')), nga.field('runningSince').map(formatMoment).cssClasses(taskClasses('runningSince')), nga.field('lastRunStarted').map(formatMoment).cssClasses(taskClasses('lastRunStarted')), nga.field('lastRunDuration').map(formatDuration).cssClasses(taskClasses('lastRunDuration')) diff --git a/publish.sh b/publish.sh index 3b7a3c1..8a2e7bd 100755 --- a/publish.sh +++ b/publish.sh @@ -46,4 +46,3 @@ if confirm "Continue publishing?"; then cd dist npm publish fi - diff --git a/server/jobs/FixNodeFilenamesJob.ts b/server/jobs/FixNodeFilenamesJob.ts index f5d80fd..ce5e4c1 100644 --- a/server/jobs/FixNodeFilenamesJob.ts +++ b/server/jobs/FixNodeFilenamesJob.ts @@ -1,8 +1,12 @@ import {fixNodeFilenames} from "../services/nodeService"; +import {jobResultOkay} from "./scheduler"; export default { name: 'FixNodeFilenamesJob', description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.', - run: fixNodeFilenames + async run() { + await fixNodeFilenames(); + return jobResultOkay(); + }, } diff --git a/server/jobs/MailQueueJob.ts b/server/jobs/MailQueueJob.ts index 4f4742f..f7405aa 100644 --- a/server/jobs/MailQueueJob.ts +++ b/server/jobs/MailQueueJob.ts @@ -1,8 +1,12 @@ import * as MailService from "../services/mailService" +import {jobResultOkay} from "./scheduler"; export default { name: 'MailQueueJob', description: 'Send pending emails (up to 5 attempts in case of failures).', - run: MailService.sendPendingMails, -} + async run() { + await MailService.sendPendingMails(); + return jobResultOkay(); + }, +}; diff --git a/server/jobs/MonitoringMailsSendingJob.ts b/server/jobs/MonitoringMailsSendingJob.ts index 5600372..02720e6 100644 --- a/server/jobs/MonitoringMailsSendingJob.ts +++ b/server/jobs/MonitoringMailsSendingJob.ts @@ -1,8 +1,12 @@ import * as MonitoringService from "../services/monitoringService"; +import {jobResultOkay} from "./scheduler"; export default { name: 'MonitoringMailsSendingJob', description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.', - run: MonitoringService.sendMonitoringMails, + async run() { + await MonitoringService.sendMonitoringMails(); + return jobResultOkay(); + }, }; diff --git a/server/jobs/NodeInformationRetrievalJob.ts b/server/jobs/NodeInformationRetrievalJob.ts index beeaa8f..5e65661 100644 --- a/server/jobs/NodeInformationRetrievalJob.ts +++ b/server/jobs/NodeInformationRetrievalJob.ts @@ -1,8 +1,20 @@ import * as MonitoringService from "../services/monitoringService"; +import {jobResultOkay, jobResultWarning} from "./scheduler"; export default { name: 'NodeInformationRetrievalJob', description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.', - run: MonitoringService.retrieveNodeInformation, + async run () { + const result = await MonitoringService.retrieveNodeInformation(); + if (result.failedParsingNodesCount > 0) { + return jobResultWarning( + `Warning: ${result.failedParsingNodesCount} of ${result.totalNodesCount} nodes could not be processed.` + ); + } else { + return jobResultOkay( + `${result.totalNodesCount} nodes have been processed.` + ); + } + }, }; diff --git a/server/jobs/OfflineNodesDeletionJob.ts b/server/jobs/OfflineNodesDeletionJob.ts index ed800f6..9b39d89 100644 --- a/server/jobs/OfflineNodesDeletionJob.ts +++ b/server/jobs/OfflineNodesDeletionJob.ts @@ -1,8 +1,12 @@ import * as MonitoringService from "../services/monitoringService"; +import {jobResultOkay} from "./scheduler"; export default { name: 'OfflineNodesDeletionJob', description: 'Delete nodes that are offline for more than 100 days.', - run: MonitoringService.deleteOfflineNodes, + async run() { + await MonitoringService.deleteOfflineNodes(); + return jobResultOkay(); + }, }; diff --git a/server/jobs/scheduler.ts b/server/jobs/scheduler.ts index 1c29370..1ebe7e1 100644 --- a/server/jobs/scheduler.ts +++ b/server/jobs/scheduler.ts @@ -10,11 +10,35 @@ import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob"; import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob"; import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob"; +export enum JobResultState { + OKAY = "okay", + WARNING = "warning", +} + +export type JobResult = { + state: JobResultState, + message?: string, +}; + +export function jobResultOkay(message?: string): JobResult { + return { + state: JobResultState.OKAY, + message + } +} + +export function jobResultWarning(message?: string): JobResult { + return { + state: JobResultState.WARNING, + message + } +} + export interface Job { name: string, description: string, - run(): Promise, + run(): Promise, } export enum TaskState { @@ -34,6 +58,7 @@ export class Task { public lastRunStarted: moment.Moment | null, public lastRunDuration: number | null, public state: TaskState, + public result: JobResult | null, public enabled: boolean, ) {} @@ -47,7 +72,7 @@ export class Task { this.lastRunStarted = this.runningSince; this.state = TaskState.RUNNING; - const done = (state: TaskState):void => { + const done = (state: TaskState, result: JobResult | null): void => { const now = moment(); const duration = now.diff(this.runningSince || now); Logger.tag('jobs').profile('[%sms]\t%s', duration, this.name); @@ -55,13 +80,14 @@ export class Task { this.runningSince = null; this.lastRunDuration = duration; this.state = state; + this.result = result; }; - this.job.run().then(() => { - done(TaskState.IDLE); + this.job.run().then(result => { + done(TaskState.IDLE, result); }).catch((err: any) => { Logger.tag('jobs').error("Job %s failed: %s", this.name, err); - done(TaskState.FAILED); + done(TaskState.FAILED, null); }); } } @@ -92,7 +118,8 @@ function schedule(expr: string, job: Job): void { null, null, TaskState.IDLE, - true + null, + true, ); cron.schedule(expr, task.run); diff --git a/server/resources/taskResource.ts b/server/resources/taskResource.ts index c90507f..4668129 100644 --- a/server/resources/taskResource.ts +++ b/server/resources/taskResource.ts @@ -3,11 +3,11 @@ import _ from "lodash"; import CONSTRAINTS from "../validation/constraints"; import ErrorTypes from "../utils/errorTypes"; import * as Resources from "../utils/resources"; -import {getTasks, Task} from "../jobs/scheduler"; +import {Entity} from "../utils/resources"; +import {getTasks, Task, TaskState} from "../jobs/scheduler"; import {normalizeString} from "../utils/strings"; import {forConstraint} from "../validation/validator"; import {Request, Response} from "express"; -import {Entity} from "../utils/resources"; const isValidId = forConstraint(CONSTRAINTS.id, false); @@ -20,6 +20,8 @@ interface ExternalTask { lastRunStarted: number | null, lastRunDuration: number | null, state: string, + result: string | null, + message: string | null, enabled: boolean, } @@ -33,6 +35,8 @@ function toExternalTask(task: Task): ExternalTask { lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(), lastRunDuration: task.lastRunDuration || null, state: task.state, + result: task.state !== TaskState.RUNNING && task.result ? task.result.state : null, + message:task.state !== TaskState.RUNNING && task.result ? task.result.message || null : null, enabled: task.enabled }; } diff --git a/server/services/monitoringService.test.ts b/server/services/monitoringService.test.ts index 8224b02..1edf951 100644 --- a/server/services/monitoringService.test.ts +++ b/server/services/monitoringService.test.ts @@ -389,6 +389,8 @@ test('parseNodesJson() should succeed parsing no nodes', () => { // then expect(result.importTimestamp.isValid()).toBe(true); expect(result.nodes).toEqual([]); + expect(result.failedNodesCount).toEqual(0); + expect(result.totalNodesCount).toEqual(0); }); test('parseNodesJson() should skip parsing invalid nodes', () => { @@ -423,6 +425,8 @@ test('parseNodesJson() should skip parsing invalid nodes', () => { // then expect(result.importTimestamp.isValid()).toBe(true); expect(result.nodes).toEqual([]); + expect(result.failedNodesCount).toEqual(2); + expect(result.totalNodesCount).toEqual(2); expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(2); }); @@ -467,5 +471,7 @@ test('parseNodesJson() should parse valid nodes', () => { expect(result.importTimestamp.isValid()).toBe(true); expect(result.nodes).toEqual([expectedParsedNode]); + expect(result.failedNodesCount).toEqual(1); + expect(result.totalNodesCount).toEqual(2); expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(1); }); diff --git a/server/services/monitoringService.ts b/server/services/monitoringService.ts index bb3b48d..cf89a4b 100644 --- a/server/services/monitoringService.ts +++ b/server/services/monitoringService.ts @@ -45,8 +45,15 @@ export type ParsedNode = { export type NodesParsingResult = { importTimestamp: Moment, nodes: ParsedNode[], + failedNodesCount: number, + totalNodesCount: number, } +export type RetrieveNodeInformationResult = { + failedParsingNodesCount: number, + totalNodesCount: number, +}; + let previousImportTimestamp: Moment | null = null; async function insertNodeInformation(nodeData: ParsedNode, node: Node): Promise { @@ -228,7 +235,9 @@ export function parseNodesJson (body: string): NodesParsingResult { const result: NodesParsingResult = { importTimestamp: parseTimestamp(json.timestamp), - nodes: [] + nodes: [], + failedNodesCount: 0, + totalNodesCount: 0, }; if (!result.importTimestamp.isValid()) { @@ -240,12 +249,14 @@ export function parseNodesJson (body: string): NodesParsingResult { } for (const nodeData of json.nodes) { + result.totalNodesCount += 1; try { const parsedNode = parseNode(result.importTimestamp, nodeData); Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`); result.nodes.push(parsedNode); } catch (error) { + result.failedNodesCount += 1; Logger.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData); } } @@ -434,11 +445,15 @@ async function withUrlsData(urls: string[]): Promise { return results; } -async function retrieveNodeInformationForUrls(urls: string[]): Promise { +async function retrieveNodeInformationForUrls(urls: string[]): Promise { const datas = await withUrlsData(urls); let maxTimestamp = datas[0].importTimestamp; let minTimestamp = maxTimestamp; + + let failedParsingNodesCount = 0; + let totalNodesCount = 0; + for (const data of datas) { if (data.importTimestamp.isAfter(maxTimestamp)) { maxTimestamp = data.importTimestamp; @@ -446,6 +461,9 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise { if (data.importTimestamp.isBefore(minTimestamp)) { minTimestamp = data.importTimestamp; } + + failedParsingNodesCount += data.failedNodesCount; + totalNodesCount += data.totalNodesCount; } if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) { @@ -456,7 +474,10 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise { maxTimestamp.format(), previousImportTimestamp.format() ); - return; + return { + failedParsingNodesCount, + totalNodesCount, + }; } previousImportTimestamp = maxTimestamp; @@ -502,6 +523,11 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise { minTimestamp.unix() ] ); + + return { + failedParsingNodesCount, + totalNodesCount, + } } export async function getAll(restParams: RestParams): Promise<{total: number, monitoringStates: any[]}> { @@ -605,7 +631,7 @@ export async function disable(token: string): Promise { return newNode; } -export async function retrieveNodeInformation(): Promise { +export async function retrieveNodeInformation(): Promise { const urls = config.server.map.nodesJsonUrl; if (_.isEmpty(urls)) { throw new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl')