modules/logger/main.js

!function LoggerModuleWrapper() {
  let core; let mod; const o = {}; const pipelines = function() {};

  /**
   * Blackrock Logger Module
   *
   * @public
   * @class Server.Modules.Logger
   * @augments Server.Modules.Core.Module
   * @param {Server.Modules.Core} coreObj - The Core Module Singleton
   * @return {Server.Modules.Logger} module - The Logger Module Singleton
   *
   * @description This is the Logger Module of the Blackrock Application Server.
   * It provides a logging method that can be used throughout the framework /
   * application server, as well as directly from within apps. It has routing
   * capabilities, allowing log events to be routed to - the Console, a Log File
   * on the filesystem, ElasticSearch and emitted directly to the Core Module instance.
   *
   * @example
   * const loggerModule = req.core.module('logger');
   *
   * @author Darren Smith
   * @copyright Copyright (c) 2021 Darren Smith
   * @license Licensed under the LGPL license.
   */
  module.exports = function LoggerModule(coreObj) {
    if (mod) return mod;
    // eslint-disable-next-line no-extend-native
    String.prototype.endsWith = function LoggerEndsWith(suffix) {
      return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };
    if (process.send) {
      fs.appendFileSync('/tmp/blackrock-daemon-fix.txt', 'Daemon Fix\n');
      fs.unlinkSync('/tmp/blackrock-daemon-fix.txt');
    }
    o.logBuffer = []; o.daemonInControl = false; o.logOverride = false;
    o.coreObjTimeout = 500; o.consoleEnabled = false; o.analyticsStore = {sessionEventCount: 0};
    o.latestHeartbeat = {}; o.logBuffer = []; o.fs = require('fs');
    core = coreObj; mod = new core.Mod('Logger');
    o.log = mod.log = function LoggerModuleQuickLog(level, logMsg, attrObj, evtName) {
      if (o.logOverride) return o.newLog(level, logMsg, attrObj);
      let currentDate = new Date();
      currentDate = currentDate.toISOString();
      const sEvt = pipelines.init.detectAvailableSinks({'noLog': true});
      const evt = {
        'level': level, 'logMsg': logMsg, 'attrObj': attrObj,
        'datestamp': currentDate, 'sinks': sEvt.sinks, 'evtName': evtName,
      };
      o.logBuffer.push(evt);
      return true;
    };
    let loadDependencies = false;
    core.on('CORE_START_DAEMON', function LoggerModuleOnStartDaemon() {
      o.daemonInControl = true;
    });
    core.on('CORE_LOAD_DEPENDENCIES', function LoggerModuleOnLoadDep() {
      loadDependencies = true;
    });
    o.log('debug', 'Logger > Initialising...',
        {module: mod.name}, 'LOGGER_INIT');
    let intervalCounter = 0;
    const interval = setInterval(function LoggerModuleInitInterval() {
      if (loadDependencies) {
        clearInterval(interval);
        pipelines.sendToSinks();
        pipelines.init();
      }
      if (intervalCounter >= 500) clearInterval(interval);
      intervalCounter += 10;
    }, 10);
    /**
     * (Undocumented) Unload Logger Module
     *
     * @private
     * @memberof Server.Modules.Logger
     * @function unload
     * @ignore
     *
     * @description
     * Tbc...
     *
     * @example
     * Tbc...
     */
    mod.unload = function LoggerModuleUnload() {
      o.log('debug',
          'Logger > Closing any open logging connections and shutting down.',
          {module: mod.name}, 'LOGGER_UNLOAD');
      if (o.fileStream) {
        // eslint-disable-next-line no-delete-var
        // noinspection JSAnnotator
        delete o.fileStream;
        core.emit('module-shut-down', 'Logger');
      } else {
        core.emit('module-shut-down', 'Logger');
      }
    };
    return mod;
  };

  /**
   * (Internal > Pipeline [1]) Setup Logger Module
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init
   * @ignore
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init = function LoggerInitPipeline() {
    // noinspection JSUnresolvedFunction, JSUnresolvedVariable
    core.lib.rxPipeline({}).pipe(

        // Fires once on server initialisation:
        pipelines.init.setupFileStream,
        core.lib.operators.map((evt) => {
          if (evt) return pipelines.init.detectAvailableSinks(evt);
        }),
        pipelines.init.setupViewAnalytics,
        pipelines.init.setupJobs,
        pipelines.init.setupGetAndUpdateLatestHeartbeat,
        pipelines.init.loadCachedHeartbeats,
        pipelines.init.fireServerBootAnalyticsEvent,
        pipelines.init.bindEnableConsole,
        pipelines.init.bindLogEndpoints

    ).subscribe();
  };

  /**
   * (Internal > Pipeline [2]) Send To Sinks
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.sendToSinks
   * @ignore
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.sendToSinks = function LoggerSendToSinksPipeline() {
    // noinspection JSUnresolvedFunction
    core.lib.rxPipeline({}, null, mod, 'logEvent').pipe(

        // Fires once per Log Event:
        pipelines.sendToSinks.unbufferEvents,
        pipelines.sendToSinks.fanoutToSinks

    ).subscribe(function LoggerSendToSinksPipelineSub(evt) {
      switch (evt.activeSink) {
        case 'console': pipelines.sendToSinks.sendToConsole(evt); break;
        case 'file': pipelines.sendToSinks.sendToFile(evt); break;
        case 'elasticsearch': pipelines.sendToSinks.sendToElasticSearch(evt); break;
        case 'core': pipelines.sendToSinks.sendToCoreObject(evt); break;
      }
    });
  };

  /**
   * (Internal > Pipeline [5]) Process Analytics Event
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.processAnalyticsEvt
   * @ignore
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.processAnalyticsEvt = function LoggerProcessAnalyticsEvtPipeline() {
    // noinspection JSUnresolvedFunction
    core.lib.rxPipeline({}).pipe(

        // Fires once per Analytics Log Event (Request)
        pipelines.processAnalyticsEvt.processAnalyticsEvt

    ).subscribe();
  };


  /**
   * (Internal > Stream Methods [1]) Setup File Stream
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.setupFileStream
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.setupFileStream = function LoggerIPLSetupFileStream(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function LoggerIPLSetupFileStreamOp(observer, evt) {
      if (core.cfg().logger.sinks.file && core.cfg().logger.sinks.file.enabled === true) {
        let location;
        if (core.cfg().logger.sinks.file.location) location = core.cfg().logger.sinks.file.location;
        else location = core.fetchBasePath('root') + '/blackrock.log';
        if (o.fs.existsSync(location)) o.fileStream = fs.createWriteStream(location, {flags: 'a'});
        o.log('debug', 'Logger  > [1] Setup the File Stream',
            {module: mod.name}, 'LOGGER_SETUP_FILE_STREAM');
      } else {
        o.log('debug', 'Logger > [1] Skipped Creation of File Stream',
            {module: mod.name}, 'LOGGER_NO_FILE_STREAM');
      }
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [2]) Detect Available Sinks
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.detectAvailableSinks
   * @ignore
   * @param {object} evt - The Incoming Event
   * @return {object} evt - The Outgoing Event
   */
  pipelines.init.detectAvailableSinks = function LoggerIPLDetectAvailableSinks(evt) {
      evt.sinks = {}; evt.sinks.core = true;
      if (core.cfg().logger.enabled === true) {
        if (core.cfg().logger.sinks.console && core.cfg().logger.sinks.console.enabled === true) {
          evt.sinks.console = true;
        }
        if (core.cfg().logger.sinks.file && core.cfg().logger.sinks.file.enabled === true) evt.sinks.file = true;
        if (core.cfg().logger.sinks.elasticsearch && core.cfg().logger.sinks.elasticsearch.enabled === true) {
          evt.sinks.elasticsearch = true;
        }
        if (!evt.noLog) {
          o.log('debug',
              'Logger > [2] Detected Available Log Sinks',
              {module: mod.name, sinks: evt.sinks}, 'LOGGER_DETECTED_SINKS');
        }
      } else {
        if (!evt.noLog) {
          o.log('debug',
              'Logger > [2] Did Not Detect Log Sinks - As Logger is Disabled in Config',
              {module: mod.name}, 'LOGGER_DISABLED');
        }
      }
      return evt;
  };

  /**
   * (Internal > Stream Methods [3]) Setup View Analytics Method
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.setupViewAnalytics
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * {
   *   "2000": {
   *      "01": {
   *         "01": [
   *            {
   *               "server": {
   *                  "dateLastBoot": "2020-12-20 00:00:00",
   *                  "dateCacheLastSaved": "2020-12-20 00:00:00"
   *               },
   *               "msgs": {
   *                  "reqSize": 0,
   *                  "avgProcessingTime": 0,
   *                  "avgMemUsed": 0,
   *                  "avgCpuLoad": 0,
   *                  "resSize": 0
   *               }
   *            }
   *         ]
   *      }
   *   }
   */
  pipelines.init.setupViewAnalytics = function LoggerIPLSetupViewAnalytics(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function LoggerIPLSetupViewAnalytics(observer, evt) {
      // noinspection JSValidateTypes
      const ViewClass = new core.Base().extend({
        constructor: function LoggerIPLViewAnalyticsClassConstructor() {
          return this;
        },
        callback: function LoggerIPLViewAnalyticsClassCallback(cb) {
          return cb(this.evt);
        },
        process: function LoggerIPLViewAnalyticsClassPipe() {
          const self = this;
          const viewObject = {
            server: {dateLastBoot: '', dateCacheLastSaved: ''},
            msgs: {
              totalReqSize: 0, totalResSize: 0,
              totalReqCount: 0, totalResCount: 0,
              avgReqSize: 0, avgResSize: 0,
              avgProcessingTime: 0, avgMemUsed: 0, avgCpuLoad: 0,
            },
          };
          viewObject.server.dateLastBoot = self.fetchMaxValue('dateLastBoot');
          viewObject.server.dateCacheLastSaved = self.fetchMaxValue('dateCacheLastSaved');
          viewObject.msgs.totalReqSize = self.fetchTotalValue('reqSize');
          viewObject.msgs.totalResSize = self.fetchTotalValue('resSize');
          viewObject.msgs.totalReqCount = self.fetchCount('reqSize');
          viewObject.msgs.totalResCount = self.fetchCount('resSize');
          viewObject.msgs.avgReqSize = self.fetchAvgValue('reqSize');
          viewObject.msgs.avgResSize = self.fetchAvgValue('resSize');
          viewObject.msgs.avgProcessingTime = self.fetchAvgValue('avgProcessingTime');
          viewObject.msgs.avgMemUsed = self.fetchAvgValue('avgMemUsed');
          viewObject.msgs.avgCpuLoad = self.fetchAvgValue('avgCpuLoad');
          return viewObject;
        },
        stub: function LoggerModuleViewAnalyticsClassStub() {
          return {
            serverParams: ['dateLastBoot', 'dateCacheLastSaved'],
            msgsParams: ['reqSize', 'avgProcessingTime', 'avgMemUsed', 'avgCpuLoad', 'resSize'],
            dp: this.getDatePartsAndPrepareStore(),
          };
        },
        getDatePartsAndPrepareStore: function LoggerModuleViewAnalyticsClassGetDateParts() {
          // noinspection JSUnresolvedVariable
          const dayjs = core.lib.dayjs; const dateObject = dayjs().format('YYYY-MM-DD').split('-');
          const year = dateObject[0]; const month = dateObject[1]; const day = dateObject[2];
          if (!o.analyticsStore[year]) o.analyticsStore[year] = {};
          if (!o.analyticsStore[year][month]) o.analyticsStore[year][month] = {};
          if (!o.analyticsStore[year][month][day]) o.analyticsStore[year][month][day] = [];
          return {year: year, month: month, day: day};
        },
        sortBy: function LoggerIPLViewAnalyticsClassSortBy(array, intField, param, direction) {
          if (direction === 'min') {
            array.sort(function LoggerIPLViewAnalyticsClassSortBySortFnOne(a, b) {
              if (!a[intField] && b[intField]) return 0 - b[intField][param];
              else if (!a[intField] && !b[intField]) return 0;
              else if (a[intField] && !b[intField]) return a[intField][param] - 0;
              else return a[intField][param] - b[intField][param];
            });
          } else {
            array.sort(function LoggerIPLViewAnalyticsClassSortBySortFnTwo(b, a) {
              if (((a && !a[intField]) || !a) && b && b[intField]) return 0 - b[intField][param];
              else if (((a && !a[intField]) || !a) && ((b && !b[intField]) || !b)) return 0;
              else if (a && a[intField] && ((b && !b[intField]) || !b)) return a[intField][param] - 0;
              else return a[intField][param] - b[intField][param];
            });
          }
          return array;
        },
        fetchMaxValue: function LoggerIPLViewAnalyticsClassFetchMaxValue(param) {
          const s = this.stub(); const dp = s.dp; const serverParams = s.serverParams;
          const msgsParams = s.msgsParams; const sortBy = this.sortBy;
          let intField;
          if (serverParams.includes(param)) intField = 'server';
          else if (msgsParams.includes(param)) intField = 'msgs';
          let daysEvts = o.analyticsStore[dp.year][dp.month][dp.day];
          daysEvts = sortBy(daysEvts, intField, param, 'max');
          if (!daysEvts || !daysEvts[0] || !daysEvts[0][intField] || !daysEvts[0][intField][param]) return 0;
          return daysEvts[0][intField][param];
        },
        /* fetchMinValue: function LoggerIPLViewAnalyticsClassFetchMinValue(param) {
          const s = this.stub(); const dp = s.dp; const serverParams = s.serverParams;
          const msgsParams = s.msgsParams; const sortBy = this.sortBy;
          let intField;
          if (serverParams.includes(param)) intField = 'server';
          else if (msgsParams.includes(param)) intField = 'msgs';
          let daysEvts = o.analyticsStore[dp.year][dp.month][dp.day];
          daysEvts = sortBy(daysEvts, intField, param, 'min');
          if (!daysEvts || !daysEvts[0] || !daysEvts[0][intField] || !daysEvts[0][intField][param]) {
            return 0;
          }
          return daysEvts[0][intField][param];
        }, */
        fetchTotalValue: function LoggerIPLViewAnalyticsClassFetchTotalValue(param) {
          const s = this.stub(); const dp = s.dp; const serverParams = s.serverParams;
          const msgsParams = s.msgsParams;
          let intField;
          if (serverParams.includes(param)) intField = 'server';
          else if (msgsParams.includes(param)) intField = 'msgs';
          const daysEvts = o.analyticsStore[dp.year][dp.month][dp.day];
          let sumTotal = 0;
          for (let i = 0; i < daysEvts.length; i++) {
            if (daysEvts[i] && daysEvts[i][intField] && daysEvts[i][intField][param]) {
              sumTotal += daysEvts[i][intField][param];
            }
          }
          return sumTotal;
        },
        fetchAvgValue: function LoggerIPLViewAnalyticsClassFetchAvgValue(param) {
          const s = this.stub(); const dp = s.dp; const serverParams = s.serverParams;
          const msgsParams = s.msgsParams;
          let intField;
          if (serverParams.includes(param)) intField = 'server';
          else if (msgsParams.includes(param)) intField = 'msgs';
          const daysEvts = o.analyticsStore[dp.year][dp.month][dp.day];
          let avgValue; let sumTotal = 0; let recordCount = 0;
          for (let i = 0; i < daysEvts.length; i++) {
            if (daysEvts[i] && daysEvts[i][intField] && daysEvts[i][intField][param]) {
              sumTotal += daysEvts[i][intField][param];
              recordCount ++;
            }
          }
          // eslint-disable-next-line prefer-const
          avgValue = sumTotal / recordCount;
          return avgValue;
        },
        fetchCount: function LoggerIPLViewAnalyticsClassFetchCount(param) {
          const s = this.stub(); const dp = s.dp; const serverParams = s.serverParams;
          const msgsParams = s.msgsParams;
          let intField;
          if (serverParams.includes(param)) intField = 'server';
          else if (msgsParams.includes(param)) intField = 'msgs';
          const daysEvts = o.analyticsStore[dp.year][dp.month][dp.day];
          let recordCount = 0;
          for (let i = 0; i < daysEvts.length; i++) {
            if (daysEvts[i] && daysEvts[i][intField] && daysEvts[i][intField][param]) recordCount ++;
          }
          return recordCount;
        },
      });
      if (!mod.analytics) mod.analytics = {};
      const viewObject = new ViewClass();
      mod.analytics.view = function LoggerIPLExternalViewAnalyticsFn() {
        return viewObject.process();
      };
      o.log('debug',
          'Logger > [3] View Analytics Setup & Ready For Use',
          {module: mod.name}, 'LOGGER_VIEW_ANALYTICS_SETUP');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [4]) Setup Jobs
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.setupJobs
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.setupJobs = function LoggerIPLSetupJobs(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function(observer, evt) {
      o.log('debug',
          'Logger > [4] Setting Up Heartbeat + Cache Jobs',
          {module: mod.name}, 'LOGGER_SETUP_HEARTBEAT_JOBS');
      if (core.cfg().logger.heartbeat) {
        const heartbeatJob = function LoggerIPLHeartbeatJob() {
          const beat = core.module('logger').analytics.view();
          const roundAndLabel = function LoggerIPLRoundAndLabel(param) {
            if (param >= 1024 && param < 1048576) {
              param = Math.round(param / 1024);
              param = Number(param).toLocaleString();
              param += ' KB';
            } else if (param >= 1048576) {
              param = Math.round(param / 1024 / 1024);
              param = Number(param).toLocaleString();
              param += ' MB';
            } else {
              param = Math.round(param);
              param = Number(param).toLocaleString();
              param += ' Bytes';
            }
            return param;
          };
          o.latestHeartbeat.totalReqSize = beat.msgs.totalReqSize = roundAndLabel(beat.msgs.totalReqSize);
          o.latestHeartbeat.totalResSize = beat.msgs.totalResSize = roundAndLabel(beat.msgs.totalResSize);
          o.latestHeartbeat.avgReqSize = beat.msgs.avgReqSize = roundAndLabel(beat.msgs.avgReqSize);
          o.latestHeartbeat.avgResSize = beat.msgs.avgResSize = roundAndLabel(beat.msgs.avgResSize);
          o.latestHeartbeat.avgMemUsed = beat.msgs.avgMemUsed = roundAndLabel(beat.msgs.avgMemUsed);
          o.latestHeartbeat.avgCpuLoad = beat.msgs.avgCpuLoad = Math.round(beat.msgs.avgCpuLoad) + '%';
          o.latestHeartbeat.totalReqCount = beat.msgs.totalReqCount;
          o.latestHeartbeat.totalResCount = beat.msgs.totalResCount;
          o.latestHeartbeat.avgProcessingTime = beat.msgs.avgProcessingTime;
          o.latestHeartbeat.dateLastBoot = beat.msgs.dateLastBoot;
          o.latestHeartbeat.dateCacheLastSaved = beat.msgs.dateCacheLastSaved;
          if (!o.latestHeartbeat.peerCount) o.latestHeartbeat.peerCount = 1;
          let appStats = {}; appStats.appsMemoryUse = '0 Bytes';
          appStats.appsCount = 0; appStats.appsRouteCount = 0;
          if (core.module('app-engine') && core.module('app-engine').appStats) {
            appStats = core.module('app-engine').appStats();
            appStats.appsMemoryUse = roundAndLabel(appStats.appsMemoryUse);
          }
          let runningInSandbox = 'No';
          if (core.module('sandbox') && core.cfg()['app-engine'].sandbox.default === true) runningInSandbox = 'Yes';
          const loadedAppCount = appStats.appsCount + ` (` + appStats.appsMemoryUse + `)`;
          const totalRouteCtrlCount = appStats.appsRouteCount + ` (` + appStats.appsMemoryUse + `)`;
          if (core.cfg().logger.heartbeat.console && o.consoleEnabled && !core.globals.get('silent')) {
            console.log(`

    ========================================================================================================

       ,ad8PPPP88b,     ,d88PPPP8ba,        MSGS & SYSTEM STATS (THIS SERVER):
   d8P"      "Y8b, ,d8P"      "Y8b       Total Request Size: ` + beat.msgs.totalReqSize + `
  dP'           "8a8"           \`Yd      Total Response Size: ` + beat.msgs.totalResSize + `
  8(              "              )8      Total Request Count: ` + beat.msgs.totalReqCount + `
  I8                             8I      Total Response Count: ` + beat.msgs.totalResCount + `
   Yb,     Server Heartbeat    ,dP       Average Request Size: ` + beat.msgs.avgReqSize + `
    "8a,                     ,a8"        Average Response Size: ` + beat.msgs.avgResSize + `
      "8a,                 ,a8"          Average Processing Time: ` + (beat.msgs.avgProcessingTime * 100) + ` ms
        "Yba             adP"            Average Memory Use: ` + beat.msgs.avgMemUsed + `
          \`Y8a         a8P'              Average CPU Load: ` + beat.msgs.avgCpuLoad + `
            \`88,     ,88'
              "8b   d8"                  SERVER INFORMATION (THIS SERVER):
               "8b d8"                   Date of Last Boot: ` + beat.server.dateLastBoot + `
                \`888'                    Date Cache Last Saved: ` + beat.server.dateCacheLastSaved + `
                  "



    ,ad8PPPP88b,     ,d88PPPP8ba,        MORE SERVER INFORMATION:
   d8P"      "Y8b, ,d8P"      "Y8b       Server Status: ` + core.status + `
  dP'           "8a8"           \`Yd      Server Mode: Live (Stand-Alone)
  8(              "              )8      Processes Running On This Server: 1
  I8                             8I      Loaded Module Count: ` + core.module.count('modules') + `
   Yb,                         ,dP       Loaded Interface Count: ` + core.module.count('interfaces') + `
    "8a,                     ,a8"        Servers In Farm: ` + o.latestHeartbeat.peerCount + `
      "8a,                 ,a8"          
        "Yba             adP"            APP INFORMATION:
          \`Y8a         a8P'              Loaded App Count: ` + loadedAppCount + `
            \`88,     ,88'                Total Route / Controller Count: ` + totalRouteCtrlCount + `
              "8b   d8"                  Running in Sandbox: ` + runningInSandbox + `
               "8b d8"                   
                \`888'                    
                  "

 =========================================================================================================
     ` );
          }
        };
        const cacheJob = function LoggerIPLCacheJob() {
          const content = JSON.stringify(o.analyticsStore);
          const path = core.fetchBasePath('cache') + '/heartbeat/heartbeats.json';
          o.fs.writeFile(path, content, {encoding: 'utf8', flag: 'w'},
              function LoggerModuleCacheJobWriteFileCallback(err) {});
        };
        core.module.isLoaded('jobs').then(function LoggerIPLIsJobsLoadedThen(jobsMod) {
          // noinspection JSUnresolvedVariable
          jobsMod.add({
            id: 'CH01', name: 'Console Server Heartbeat Job',
            type: 'recurring', delay: core.cfg().logger.heartbeat.heartbeatFreq, local: true,
          }, heartbeatJob, {});
          // noinspection JSUnresolvedVariable
          jobsMod.add({
            id: 'SH02', name: 'Server Heartbeat Cache Job',
            type: 'recurring', delay: core.cfg().logger.heartbeat.cacheFreq, local: true,
          }, cacheJob, {});
        }).catch(function LoggerIPLIsJobsLoadedCatch(err) {
          o.log('debug',
              'Logger > [4] Cannot Load Jobs Module',
              {module: mod.name, error: err}, 'LOGGER_NO_LOAD_JOBS');
        });
      }
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [5]) Setup Get & Update Latest Heartbeat Method
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.setupGetAndUpdateLatestHeartbeat
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.setupGetAndUpdateLatestHeartbeat = function LoggerIPLSetupGetAndUpdateLatestHb(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function(observer, evt) {
      o.log('debug',
          'Logger > [5] Setting up the \'getLatestHeartbeat\' and ' +
          '\'updateLatestHeartbeat\' Methods on Logger',
          {module: mod.name}, 'LOGGER_BOUND_GET_UPDATE_HEARTBEAT_METHODS');
      /**
       * Get Latest Heartbeat
       *
       * @public
       * @memberof Server.Modules.Logger
       * @function getLatestHeartbeat
       * @return {object} latestHeartbeat - The Latest Heartbeat
       *
       * @description
       * Tbc...
       *
       * @example
       * Tbc...
       */
      mod.getLatestHeartbeat = function LoggerGetLatestHeartbeat() {
        return o.latestHeartbeat;
      };
      /**
       * (Undocumented) Update Latest Heartbeat
       *
       * @private
       * @memberof Server.Modules.Logger
       * @function updateLatestHeartbeat
       * @param {string} key - Latest Heartbeat Key
       * @param {object|*} value - Latest Heartbeat Value
       * @return {boolean} result - Result of Updating Latest Heartbeat
       * @ignore
       *
       * @description
       * Tbc...
       *
       * @example
       * Tbc...
       */
      mod.updateLatestHeartbeat = function LoggerUpdateLatestHeartbeat(key, value) {
        o.latestHeartbeat[key] = value;
        return true;
      };
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [6]) Load Cached Heartbeats
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.loadCachedHeartbeats
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.loadCachedHeartbeats = function LoggerIPLLoadCachedHeartbeats(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function(observer, evt) {
      o.log('debug',
          'Logger > [6] Loading cached heartbeats if they exist',
          {module: mod.name}, 'LOGGER_LOAD_CACHED_HEARTBEATS');
      setTimeout(function LoggerIPLLoadCachedHeartbeatsTimeout() {
        const path = core.fetchBasePath('cache') + '/heartbeat/heartbeats.json';
        o.fs.readFile(path, 'utf8',
            function LoggerIPLLoadCachedHeartbeatsReadFileCb(err, content) {
          if (content) o.analyticsStore = JSON.parse(content);
        });
      }, 70);
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [7]) Fire the "Server Boot" Analytics Event
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.fireServerBootAnalyticsEvent
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.fireServerBootAnalyticsEvent = function LoggerIPLFireServerBootAnalyticsEvt(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function LoggerIPLFireServerBootAnalyticsEvtOp(observer, evt) {
      o.log('debug',
          'Logger > [7] Firing the "Server Boot" Analytics Event',
          {module: mod.name}, 'LOGGER_FIRE_SERVER_BOOT_EVT');
      setTimeout(function LoggerIPLFireServerBootAnalyticsEvtTimeout() {
        // noinspection JSUnresolvedFunction
        mod.analytics.log({'server': {'dateLastBoot': core.lib.dayjs().format()}});
      }, 70);
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [8]) Binds the 'enableConsole' method to this module
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.bindEnableConsole
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.bindEnableConsole = function LoggerIPLBindEnableConsole(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function LoggerIPLBindEnableConsoleOp(observer, evt) {
      o.log('debug',
          'Logger > [8a] Binding \'enableConsole\' method to this module',
          {module: mod.name}, 'LOGGER_BOUND_ENABLE_CONSOLE');
      /**
       * (Undocumented) Enable Console
       *
       * @private
       * @memberof Server.Modules.Logger
       * @function enableConsole
       * @ignore
       *
       * @description
       * This method is called internally to enable the console (switch from caching log requests directed
       * at the console to actually sending them through to the console), once the Logger Module has
       * finished initialising.
       *
       * @example
       * Tbc...
       */
      mod.enableConsole = function LoggerBindEnableConsole() {
        o.log('debug',
            'Logger > [8b] \'enableConsole\' has been called',
            {module: mod.name}, 'LOGGER_ENABLE_CONSOLE_CALLED');
        o.consoleEnabled = true;
      };
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [9]) Bind Log Endpoints
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.init.bindLogEndpoints
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.bindLogEndpoints = function LoggerIPLBindLogEndpoints(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function LoggerIPLBindLogEndpointsOp(observer, evt) {
      o.log('debug',
          'Logger > [9] Bound Analytics & Log Endpoint On The Logger Module',
          {module: mod.name}, 'LOGGER_BOUND_ANALYTICS_LOG_ENDPOINTS');
      o.logOverride = true;
      /**
       * Log an Event
       *
       * @public
       * @memberof Server.Modules.Logger
       * @function log
       * @param {string} level - Log Level (startup|error|warning|debug|[custom])
       * @param {string} logMsg - Log Message
       * @param {object} [attrObj] - Attributes Object
       * @param {string} [evtName] - Event Name
       * @return {boolean} result - Result (True|False)
       *
       * @description
       * This method is the main method used to log new events in the Blackrock Application Server. You
       * must pass it a 'level' (startup|error|warning|debug|[custom]) and message at a minimum. The
       * Attributes Object and Event Name are optional.
       *
       * @example
       * const log = req.core.module('logger').log;
       * log('debug', 'This is a sample message', {type: "sample"}, 'SAMPLE_MSG');
       * // If Console Enabled, Output Is: 'XXXX-XX-XX XX-XX-XX (debug) This is a sample message'
       */
      mod.log = o.log = o.newLog = function LoggerModuleLog(level, logMsg, attrObj, evtName) {
        let currentDate = new Date();
        currentDate = currentDate.toISOString();
        const evt2 = {
          'datestamp': currentDate, 'level': level, 'logMsg': logMsg,
          'attrObj': attrObj, 'evtName': evtName, 'sinks': evt.sinks,
        };
        mod.emit('logEvent', evt2);
        observer.next();
        return true;
      };
      if (!o.daemonInControl) core.emit('updateLogFn');
      if (!mod.analytics) {
        /**
         * Analytics Logger Object
         *
         * @public
         * @memberof Server.Modules.Logger
         * @property {function} log - Log Method
         *
         * @description
         * This method is used internally to track server analytics such as up time, total and average number
         * of requests and responses going through the Router Module, Etc...
         *
         * @example
         * Tbc...
         */
        mod.analytics = {};
      }
      /**
       * Log an Analytics Event
       *
       * @public
       * @memberof Server.Modules.Logger
       * @function analytics.log
       * @param {object} query - Analytics Log Query
       *
       * @description
       * This method is used internally to track server analytics such as up time, total and average number
       * of requests and responses going through the Router Module, Etc...
       *
       * @example
       * Tbc...
       */
      mod.analytics.log = function LoggerModuleAnalyticsLog(query) {
        const evt2 = {};
        evt2.analyticsEvent = {'query': query};
        pipelines.processAnalyticsEvt();
      };
    }, source);
  };


  /**
   * (Internal > Stream  Methods [1]) Un-Buffer Log Events When Console Enabled
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.sendToSinks.unbufferEvents
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.sendToSinks.unbufferEvents = function LoggerSTSPLUnbufferEvents(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function(observer, evt) {
      if (o.consoleEnabled && o.logBuffer) {
        for (let i = 0; i < o.logBuffer.length; i++) {
          observer.next(o.logBuffer[i]);
        }
        observer.next(evt);
        o.logBuffer = [];
      } else o.logBuffer.push(evt);
    }, source);
  };

  /**
   * (Internal > Stream  Methods [2]) Fan-Out To Sinks
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.sendToSinks.fanoutToSinks
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.sendToSinks.fanoutToSinks = function LoggerModuleFanoutToSinks(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function(observer, evt) {
      // eslint-disable-next-line guard-for-in
      for (const sink in evt.sinks) {
        let evt2 = {
          'level': evt.level, 'logMsg': evt.logMsg, 'attrObj': evt.attrObj,
          'evtName': evt.evtName, 'sinks': evt.sinks,
        };
        if (evt.datestamp) evt2.datestamp = evt.datestamp;
        // noinspection JSUnfilteredForInLoop
        switch (sink) {
          case 'console': evt2.activeSink = 'console'; observer.next(evt2); evt2 = {}; break;
          case 'file': evt2.activeSink = 'file'; observer.next(evt2); evt2 = {}; break;
          case 'core': evt2.activeSink = 'core'; observer.next(evt2); evt2 = {}; break;
          case 'elasticsearch': evt2.activeSink = 'elasticsearch'; observer.next(evt2); evt2 = {}; break;
          default: break;
        }
      }
    }, source);
  };


  /**
   * (Internal > Stream Methods [2.5]) Send Log Event to Core Object Event Emitter
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.sendToSinks.sendToCoreObject
   * @ignore
   * @param {object} evt - The Request Event
   * @return {object} evt - The Response Event
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.sendToSinks.sendToCoreObject = function LoggerModuleSendToCoreObject(evt) {
    const level = evt.level; const logMsg = evt.logMsg; const attrObj = evt.attrObj;
    const currentDate = evt.datestamp; const evtName = evt.evtName;
    let logMessage; let logMod; const logMsgSplit = logMsg.split('>');
    if (logMsgSplit && logMsgSplit[0]) logMod = logMsgSplit[0].trim(); else logMod = '';
    if (logMsgSplit && logMsgSplit[1]) logMessage = logMsgSplit[1].trim(); else logMessage = '';
    setTimeout(function() {
      const emitObj = {
        'datestamp': currentDate, 'evtName': evtName, 'level': level,
        'module': logMod, 'msg': logMessage, 'attr': attrObj,
      };
      core.emit('log', emitObj);
      if (evtName) core.emit(evtName, emitObj);
      if (logMod) core.emit(logMod, emitObj);
      if (o.coreObjTimeout > 0) o.coreObjTimeout --;
    }, o.coreObjTimeout);
    return evt;
  };

  /**
   * (Internal > Stream Methods [3]) Send Log Event to Console
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.sendToSinks.sendToConsole
   * @ignore
   * @param {object} evt - The Request Event
   * @return {object} evt - The Response Event
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.sendToSinks.sendToConsole = function LoggerModuleSendToConsole(evt) {
    const level = evt.level; const logMsg = evt.logMsg; const attrObj = evt.attrObj; const currentDate = evt.datestamp;
    if (evt.activeSink === 'console' && !core.globals.get('silent')) {
      // noinspection JSUnresolvedVariable
      if (core.cfg().logger.levels.includes(level)) {
        console.log(currentDate + ' (' + level + ') ' + logMsg);
        // noinspection JSUnresolvedVariable
        if (attrObj && Object.keys(attrObj).length >= 1 && core.cfg().logger.logMetadataObjects) {
          console.log(attrObj);
        }
      }
    }
    return evt;
  };

  /**
   * (Internal > Stream Methods [4]) Send Log Event to Console
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.sendToSinks.sendToFile
   * @ignore
   * @param {object} evt - The Request Event
   * @return {object} evt - The Response Event
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.sendToSinks.sendToFile = function LoggerModuleSendToFile(evt) {
    const level = evt.level; const logMsg = evt.logMsg;
    const attrObj = evt.attrObj; const currentDate = evt.datestamp;
    if (evt.activeSink === 'file') {
      // noinspection JSUnresolvedVariable
      if (o.fileStream && core.cfg().logger.levels.includes(level)) {
        o.fileStream.write(currentDate + ' (' + level +') ' + logMsg + '\n\n');
        // noinspection JSUnresolvedVariable
        if (attrObj && core.cfg().logger.logMetadataObjects === true) {
          o.fileStream.write(JSON.stringify(attrObj));
        }
      }
    }
    return evt;
  };

  /**
   * (Internal > Stream Methods [5]) Send Log Event to ElasticSearch
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.sendToSinks.sendToElasticSearch
   * @ignore
   * @param {object} evt - The Request Event
   * @return {object} evt - The Response Event
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.sendToSinks.sendToElasticSearch = function LoggerModuleSendToElasticSearch(evt) {
    const level = evt.level; const logMsg = evt.logMsg;
    const attrObj = evt.attrObj; const currentDate = evt.datestamp;
    if (evt.activeSink === 'elasticsearch') {
      const httpModule = core.module('http', 'interface');
      if (!httpModule) return;
      const client = httpModule.client;
      let indexBucket = currentDate.split('T');
      indexBucket = indexBucket[0];
      const index = core.cfg().logger.sinks.elasticsearch['base_index'] + '-' + indexBucket;
      const baseUri = core.cfg().logger.sinks.elasticsearch['base_uri'];
      // noinspection JSUnresolvedVariable
      if (core.cfg().logger.levels.includes(level)) {
        const body = {
          'timestamp': currentDate,
          'level': level,
          'message': logMsg,
          'attributes': attrObj,
        };
        // noinspection JSUnresolvedVariable
        if (attrObj && core.cfg().logger.logMetadataObjects === true) body.attributes = attrObj;
        client.request({
          'url': baseUri + '/' + index + '/_doc/',
          'method': 'POST',
          'headers': {'Content-Type': 'application/json'},
          'encoding': 'utf8',
          'data': body,
        }, function LoggerModuleSendToElasticSearchCallback() {});
      }
    }
    return evt;
  };


  /**
   * (Internal > Stream Methods [6]) Process Analytics Event
   *
   * @private
   * @memberof Server.Modules.Logger
   * @function pipelines.processAnalyticsEvt.processAnalyticsEvt
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   *   {
   *      "server": {
   *         "dateLastBoot": "2020-12-20 00:00:00",
   *         "dateCacheLastSaved": "2020-12-20 00:00:00"
   *      },
   *      "msgs": {
   *         "reqSize": 0,
   *         "avgProcessingTime": 0,
   *         "avgMemUsed": 0,
   *         "avgCpuLoad": 0,
   *         "resSize": 0
   *      }
   *   }
   */
  pipelines.processAnalyticsEvt.processAnalyticsEvt = function LoggerPAEPLProcessAnalyticsEvt(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function LoggerPAEPLProcessAnalyticsEvtOp(observer, evt) {
      if (evt.analyticsEvent) {
        const query = evt.analyticsEvent.query;
        // noinspection JSUnresolvedVariable
        const dayjs = core.lib.dayjs;
        const dateObject = dayjs().format('YYYY-MM-DD').split('-');
        const year = dateObject[0]; const month = dateObject[1]; const day = dateObject[2];
        if (!o.analyticsStore[year]) o.analyticsStore[year] = {};
        if (!o.analyticsStore[year][month]) o.analyticsStore[year][month] = {};
        if (!o.analyticsStore[year][month][day]) o.analyticsStore[year][month][day] = [];
        // eslint-disable-next-line guard-for-in
        for (const param1 in query) {
          // eslint-disable-next-line guard-for-in
          for (const param2 in query[param1]) {
            if (!o.analyticsStore[year][month][day][o.analyticsStore.sessionEventCount]) {
              o.analyticsStore[year][month][day][o.analyticsStore.sessionEventCount] = {};
              o.analyticsStore[year][month][day][o.analyticsStore.sessionEventCount][param1] = {};
            }
            const dateAnalyticsEvtStore = o.analyticsStore[year][month][day];
            // noinspection JSUnfilteredForInLoop
            dateAnalyticsEvtStore[o.analyticsStore.sessionEventCount][param1][param2] = query[param1][param2];
          }
        }
        o.analyticsStore.sessionEventCount ++;
      }
      observer.next(evt);
    }, source);
  };
}();