@mdf.js/service-registry
Version:
MMS - API - Service Registry
244 lines • 10 kB
JavaScript
"use strict";
/**
* Copyright 2024 Mytra Control S.L. All rights reserved.
*
* Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
* or at https://opensource.org/licenses/MIT.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Aggregator = void 0;
const tslib_1 = require("tslib");
const core_1 = require("@mdf.js/core");
const utils_1 = require("@mdf.js/utils");
const events_1 = tslib_1.__importDefault(require("events"));
const lodash_1 = require("lodash");
const types_1 = require("../types");
/**
* The Aggregator class serves as a central point for collecting and aggregating health checks
* and statuses from various components within an application. It also allows for the integration
* of external and worker-specific checks to provide a comprehensive view of the application's
* health status.
*
* This class extends EventEmitter to emit status updates, enabling other parts of the application
* to react to changes in health status as necessary.
*/
class Aggregator extends events_1.default {
/**
* Create an instance of the Health aggregator
* @param metadata - Metadata describing the application, used to enrich the health data.
* @param logger - Logger instance for logging activities related to health monitoring.
*/
constructor(metadata, logger) {
super();
this.metadata = metadata;
this.logger = logger;
/** Components monitored by the aggregator */
this.components = new Map();
/** External checks, included in the aggregator to be exposed in the overall diagnostic */
this.externalChecks = new Map();
/** Worker checks, included in the aggregator to be exposed in the overall diagnostic */
this.workerChecks = new Map();
/** Array of notes to be included in the health status */
this.notes = [];
/** Public output */
this.output = undefined;
/** Event handler for status event */
this.statusEventHandler = () => {
if (this.listenerCount('status') > 0) {
this.emit('status', this.status);
}
};
this.baseHealth = {
...(0, lodash_1.pick)(metadata, types_1.METADATA_PROPERTIES),
notes: this.notes,
output: this.output,
};
// Stryker disable next-line all
this.logger.debug(`New health aggregator instance created: ${JSON.stringify(metadata)}`);
}
/**
* Computes the health status of the application by aggregating individual component checks
* and determining the overall status.
*/
get health() {
const checks = this.checks;
const status = core_1.Health.overallStatus(checks);
return { ...this.baseHealth, notes: this.notes, output: this.output, status, checks };
}
/**
* Aggregates checks from all sources: registered components, external checks, and worker checks.
*/
get checks() {
let checks = {};
for (const [, components] of this.components) {
checks = (0, lodash_1.merge)(checks, components.checks);
}
this.addWorkerCheck(this.metadata.name, 'uptime', this.uptime);
for (const [key, _workerChecks] of this.workerChecks.entries()) {
checks = (0, lodash_1.merge)(checks, { [key]: _workerChecks });
}
for (const [key, _externalChecks] of this.externalChecks.entries()) {
checks = (0, lodash_1.merge)(checks, { [key]: _externalChecks });
}
return checks;
}
/** Overall component status */
get status() {
return core_1.Health.overallStatus(this.checks);
}
/**
* Register a resource or a list of resources to monitor for errors.
* @param component - Resource or list of resources to be registered
*/
register(component) {
const _components = Array.isArray(component) ? component : [component];
for (const entry of _components) {
if (this.isValidResource(entry) && !this.components.has(entry.name)) {
// Stryker disable next-line all
this.logger.debug(`Registering health handler for component: ${entry.name}`);
entry.on('status', this.statusEventHandler);
this.components.set(entry.name, entry);
}
}
}
/**
* Adds a timestamped note to the health status.
* @param note - Note to be added.
*/
addNote(note) {
this.notes.push(`${new Date().toISOString()} - ${note}`);
if (this.notes.length > types_1.DEFAULT_MAX_NUMBER_OF_NOTES) {
this.notes.shift();
}
}
/**
* Update or add a check measure.
* This should be used to inform about the state of resources behind the Component/Microservice,
* for example states of connections with field devices.
*
* The new check will be taking into account in the overall health status.
* The new check will be included in the `checks` object with the key "component:measure".
* If this key already exists, the `componentId` of the `check` parameter will be checked, if
* there is a check with the same `componentId` in the array, the check will be updated, in other
* case the new check will be added to the existing array.
*
* The maximum number external checks entries is 10, and the maximum number of checks per entry
* is 100.
* @param component - component identification
* @param measure - measure identification
* @param check - check to be updated or included
* @returns true, if the check has been updated or included
*/
addExternalCheck(component, measure, check) {
if (!this.isValidCheck(check)) {
return false;
}
let checks = this.externalChecks.get(`${component}:${measure}`);
if (!checks && this.externalChecks.size >= 10) {
return false;
}
else {
checks = checks || [];
}
const entryIndex = checks.findIndex(entry => entry.componentId === check.componentId);
if (entryIndex !== -1) {
checks[entryIndex] = check;
}
else if (this.externalChecks.size < 100) {
checks.push(check);
}
else {
return false;
}
this.externalChecks.set(`${component}:${measure}`, checks);
return true;
}
/**
* Update the health checks associated with workers.
* @param checks - Checks to be updated or included
*/
updateWorkersChecks(checks) {
for (const [key, workerChecks] of Object.entries(checks)) {
const checks = this.workerChecks.get(key) || [];
for (const check of workerChecks) {
const entryIndex = checks.findIndex(entry => entry.componentId === check.componentId);
if (entryIndex !== -1) {
checks[entryIndex] = check;
}
else {
checks.push(check);
}
}
this.workerChecks.set(key, checks);
}
}
/**
* Update or add a check measure for a worker.
* This should be used to inform about the state of resources behind the worker.
* The new check will be taking into account in the overall health status.
* The new check will be included in the `checks` object with the key "component:measure".
* If this key already exists, the `componentId` of the `check` parameter will be checked, if
* there is a check with the same `componentId` in the array, the check will be updated, in other
* case the new check will be added to the existing array.
* @param component - component identification
* @param measure - measure identification
* @param check - check to be updated or included
* @returns true, if the check has been updated or included
*/
addWorkerCheck(component, measure, check) {
if (!this.isValidCheck(check)) {
return false;
}
const checks = this.workerChecks.get(`${component}:${measure}`) || [];
const entryIndex = checks.findIndex(entry => entry.componentId === check.componentId);
if (entryIndex !== -1) {
checks[entryIndex] = check;
}
else {
checks.push(check);
}
this.workerChecks.set(`${component}:${measure}`, checks);
return true;
}
/**
* Check if the check is valid to be included in the health status
* @param check - Check to be validated
* @returns
*/
isValidCheck(check) {
return (check.status &&
core_1.Health.STATUSES.includes(check.status) &&
typeof check.componentId === 'string');
}
/** Check if the resource is valid to be monitored */
isValidResource(resource) {
return 'checks' in resource && 'on' in resource && 'off' in resource && 'name' in resource;
}
/** Return the the uptime of service as a check */
get uptime() {
return {
componentId: this.baseHealth.instanceId,
processId: process.pid,
componentType: 'system',
observedValue: (0, utils_1.prettyMS)(process.uptime() * 1000),
observedUnit: 'time',
status: core_1.Health.STATUS.PASS,
time: new Date().toISOString(),
};
}
/** Close the aggregator */
close() {
for (const [, component] of this.components) {
component.off('status', this.statusEventHandler);
}
this.components.clear();
this.externalChecks.clear();
this.workerChecks.clear();
this.notes.length = 0;
this.output = undefined;
// Stryker disable next-line all
this.logger.debug('Health aggregator closed');
}
}
exports.Aggregator = Aggregator;
//# sourceMappingURL=Aggregator.js.map