@mdf.js/service-registry
Version:
MMS - API - Service Registry
347 lines • 16 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.ServiceRegistry = void 0;
const tslib_1 = require("tslib");
const crash_1 = require("@mdf.js/crash");
const logger_1 = require("@mdf.js/logger");
const utils_1 = require("@mdf.js/utils");
const events_1 = tslib_1.__importDefault(require("events"));
const control_1 = require("./control");
const observability_1 = require("./observability");
const settings_1 = require("./settings");
const types_1 = require("./types");
class ServiceRegistry extends events_1.default {
/**
* Create a new instance of the Service Registry
* @param bootstrapOptions - Bootstrap settings, define how the Custom and the Service Registry
* settings should be loaded.
* @param serviceRegistryOptions - Service Registry settings, used as a base for the Service
* Registry configuration manager.
* @param customSettings - Custom settings provided by the user, used as a base for the Custom
* configuration manager.
*/
constructor(bootstrapOptions, serviceRegistryOptions, customSettings) {
super();
/** Resources attached to the service registry observability */
this._resources = [];
/** Flag to indicate if the service has performed the bootstrap */
this._booted = false;
/** Flag to indicate if the service has started */
this._started = false;
/** Return the health information from the observability instance */
this.onHealthCommand = async () => {
return this._observability.health;
};
/** Return the service stats from the observability instance */
this.onStatsCommand = async () => {
return this._observability.metrics;
};
/** Return the errors stored in the registry from the observability instance */
this.onErrorsCommand = async () => {
return (0, utils_1.deCycle)(this._observability.errors);
};
/** Return the custom settings from the configuration manager */
this.onConfigCommand = async () => {
return this.settings;
};
/**
* Perform the finish of the service engine and exit the process
* @param signal - The signal received
*/
this.onFinishCommand = async (signal) => {
// Stryker disable next-line all
this._logger.warn(`Received ${signal} signal, finishing application engine ...`);
try {
await this.shutdown();
await this.stop();
}
catch (rawError) {
const cause = crash_1.Crash.from(rawError);
this._logger.crash(cause);
}
finally {
// Stryker disable next-line all
this._logger.info(`Application engine finished`);
setTimeout(process.exit, types_1.SHUTDOWN_DELAY, signal === 'SIGINT' ? 0 : 1);
}
};
/** Handle the command event from the OpenC2 consumer */
this.onCommandEvent = (command) => {
this.emit('command', command);
};
/**
* Wrap the start method of the resource to avoid errors
* @param resource - the resource to be wrapped
*/
this.wrappedStart = async (resource) => {
if ('start' in resource && typeof resource.start === 'function') {
await (0, utils_1.retryBind)(resource.start, resource, [], this.retryOptions);
}
else {
// Stryker disable next-line all
this._logger.info(`${resource.name} has not a start method`);
await Promise.resolve();
}
};
/**
* Wrap the stop method of the resource to avoid errors
* @param resource - the resource to be wrapped
* @returns
*/
this.wrappedStop = async (resource) => {
if ('stop' in resource && typeof resource.stop === 'function') {
await (0, utils_1.retryBind)(resource.stop, resource, [], this.retryOptions);
}
else {
// Stryker disable next-line all
this._logger.info(`${resource.name} has not a stop method`);
await Promise.resolve();
}
};
/** Perform the bootstrap of all the service registry resources */
this.bootstrap = async () => {
try {
if (this._booted) {
return;
}
// Stryker disable next-line all
this._logger.info(`Welcome to ${this.identification}`);
// Stryker disable next-line all
this._logger.info('Bootstrapping application engine ...');
await (0, utils_1.retryBind)(this._observability.start, this._observability, [], this.retryOptions);
await (0, utils_1.retryBind)(this._settingsManager.start, this._settingsManager, [], this.retryOptions);
const links = JSON.stringify(this._observability.links, null, 2);
// Stryker disable next-line all
this._logger.info(`Observability engine started, the health information is at: ${links}`);
if (this._consumer.instance) {
await (0, utils_1.retryBind)(this._consumer.start, this._consumer, [], this.retryOptions);
// Stryker disable next-line all
this._logger.info('OpenC2 Consumer engine started');
}
this._booted = true;
}
catch (rawError) {
const cause = crash_1.Crash.from(rawError);
const error = new crash_1.Crash(`Error bootstrapping the application engine: ${cause.message}`, {
cause,
});
this._logger.crash(error);
throw error;
}
};
/** Perform the shutdown of all the service registry resources */
this.shutdown = async () => {
try {
if (!this._booted) {
return;
}
// Stryker disable next-line all
this._logger.info('Shutting down application engine ...');
if (this._consumer.instance) {
await (0, utils_1.retryBind)(this._consumer.stop, this._consumer, [], this.retryOptions);
// Stryker disable next-line all
this._logger.info('OpenC2 Consumer engine stopped');
}
await (0, utils_1.retryBind)(this._observability.stop, this._observability, [], this.retryOptions);
await (0, utils_1.retryBind)(this._settingsManager.stop, this._settingsManager, [], this.retryOptions);
// Stryker disable next-line all
this._logger.info('Observability engine stopped');
this._booted = false;
}
catch (rawError) {
const cause = crash_1.Crash.from(rawError);
const error = new crash_1.Crash(`Error shutting down the application engine: ${cause.message}`, {
cause,
});
// Stryker disable next-line all
this._logger.crash(error);
throw error;
}
};
/** Perform the initialization of all the service resources that has been attached */
this.start = async () => {
const startedResources = [];
try {
if (this._started) {
return;
}
if (!this._booted) {
await this.bootstrap();
}
if (this._settingsManager.isPrimary) {
// Stryker disable next-line all
this._logger.info('Application resources are not started in the primary cluster node');
this._started = true;
return;
}
// Stryker disable next-line all
this._logger.info('Starting application resources ...');
for (const resource of this._resources) {
// Stryker disable next-line all
this._logger.info(`Starting resource: ${resource.name} ...`);
await this.wrappedStart(resource);
// Stryker disable next-line all
this._logger.info(`... ${resource.name} started`);
startedResources.push(resource);
}
// Stryker disable next-line all
this._logger.info('... application resources started');
this._started = true;
}
catch (rawError) {
const cause = crash_1.Crash.from(rawError);
const error = new crash_1.Crash(`Error starting the application resources: ${cause.message}`, {
cause,
});
this._logger.crash(error);
// Stryker disable next-line all
this._logger.info('Rolling back started resources ...');
for (const resource of startedResources) {
// Stryker disable next-line all
this._logger.info(`Stopping resource: ${resource.name} ...`);
await this.wrappedStop(resource);
// Stryker disable next-line all
this._logger.info(`... ${resource.name} stopped`);
}
throw error;
}
};
/** Perform the stop of all the service resources that has been attached */
this.stop = async () => {
try {
if (!this._started) {
return;
}
if (this._booted) {
await this.shutdown();
}
if (this._settingsManager.isPrimary) {
// Stryker disable next-line all
this._logger.info('Application resources are not stopped in the primary cluster node');
this._started = false;
return;
}
// Stryker disable next-line all
this._logger.info('Stopping application resources ...');
for (const resource of this._resources) {
// Stryker disable next-line all
this._logger.info(`Stopping resource: ${resource.name} ...`);
await this.wrappedStop(resource);
// Stryker disable next-line all
this._logger.info(`... ${resource.name} stopped`);
}
// Stryker disable next-line all
this._logger.info('... application resources stopped');
this._started = false;
}
catch (rawError) {
const cause = crash_1.Crash.from(rawError);
const error = new crash_1.Crash(`Error stopping the application resources: ${cause.message}`, {
cause,
});
// Stryker disable next-line all
this._logger.crash(error);
throw error;
}
};
this._settingsManager = new settings_1.SettingsManager(bootstrapOptions, serviceRegistryOptions, customSettings);
this._logger = (0, logger_1.SetContext)(new logger_1.Logger(this._settingsManager.name, this._settingsManager.logger), this._settingsManager.name, this._settingsManager.instanceId);
this._observability = new observability_1.Observability({
...this._settingsManager.observability,
logger: this._logger,
});
this._consumer = new control_1.ControlManager(this._settingsManager.serviceRegistrySettings, this._logger, this.resolverMap);
this._observability.attach(this._settingsManager);
if (this._consumer.instance && !this._consumer.error && (bootstrapOptions === null || bootstrapOptions === void 0 ? void 0 : bootstrapOptions.consumer)) {
this._consumer.on('command', this.onCommandEvent.bind(this));
this._observability.attach(this._consumer.instance);
}
else if (this._consumer.error) {
// Stryker disable next-line all
this._logger.warn('OpenC2 Consumer is not available, the service is not able to receive commands');
this._observability.push(this._consumer.error);
}
process.on('SIGINT', () => this.onFinishCommand('SIGINT'));
process.on('SIGTERM', () => this.onFinishCommand('SIGTERM'));
}
/** @returns Default resolver map for the OpenC2 Consumer interface */
get resolverMap() {
if (this._settingsManager.namespace) {
return {
[`query:${this._settingsManager.namespace}:health`]: this.onHealthCommand.bind(this),
[`query:${this._settingsManager.namespace}:stats`]: this.onStatsCommand.bind(this),
[`query:${this._settingsManager.namespace}:errors`]: this.onErrorsCommand.bind(this),
[`query:${this._settingsManager.namespace}:config`]: this.onConfigCommand.bind(this),
[`start:${this._settingsManager.namespace}:resources`]: this.start.bind(this),
[`stop:${this._settingsManager.namespace}:resources`]: this.stop.bind(this),
[`restart:${this._settingsManager.namespace}:all`]: this.onFinishCommand.bind(this, 'SIGINT'),
};
}
else {
return undefined;
}
}
/** @returns The retry options used for starting resources and service */
get retryOptions() {
return {
logger: this._logger.crash,
...this._settingsManager.retryOptions,
};
}
/** @returns Service Register health information */
get errors() {
return this._observability.errors;
}
/** @returns Service Register health information */
get health() {
return this._observability.health;
}
/** @returns Service Register status */
get status() {
return this._observability.status;
}
/** @returns Service Register settings */
get serviceRegistrySettings() {
return this._settingsManager.serviceRegistrySettings;
}
/** @returns Custom settings */
get customSettings() {
return this._settingsManager.customSettings;
}
/** @returns Service settings */
get settings() {
return this._settingsManager.settings;
}
/** @returns The logger instance */
get logger() {
return this._logger;
}
/** @return The application identification string */
get identification() {
return `${this.health.name} - ${this.health.serviceId}/${this.health.serviceGroupId} - ${this.health.release} - running with instanceId: [${this.health.instanceId}]`;
}
/**
* Register a resource within the service observability
* @param resource - The resource or resources to be register
*/
register(resource) {
const resources = Array.isArray(resource) ? resource : [resource];
for (const entry of resources) {
this._resources.push(entry);
// Stryker disable next-line all
this._logger.debug(`Registering resource: ${entry.name}`);
this._observability.attach(entry);
}
}
get(path, defaultValue) {
return this._settingsManager.customRegisterConfigManager.get(path, defaultValue);
}
}
exports.ServiceRegistry = ServiceRegistry;
//# sourceMappingURL=ServiceRegistry.js.map