APIs

Show:
/**
  @module ember-flexberry
*/

import Ember from 'ember'; //TODO Import Module. Replace Ember.Logger, Ember.onerror.
import Service, { inject as service } from '@ember/service';
import Evented from '@ember/object/evented';
import { getOwner } from '@ember/application';
import RSVP from 'rsvp';
import { isNone } from '@ember/utils';
import { A, isArray } from '@ember/array';
import { assert } from '@ember/debug';
import { get, set } from '@ember/object';
import Queue from 'ember-flexberry-data/utils/queue';

const messageCategory = {
  error: { name: 'ERROR', priority: 1 },
  warn: { name: 'WARN', priority: 2 },
  log: { name: 'LOG', priority: 3 },
  info: { name: 'INFO', priority: 4 },
  debug: { name: 'DEBUG', priority: 5 },
  deprecate: { name: 'DEPRECATION', priority: 6 },
  promise: { name: 'PROMISE', priority: 7 }
};

const joinArguments = function() {
  let result = '';
  for (let i = 0, len = arguments.length; i < len; i++) {
    if (arguments[i] && arguments[i].toString) {
      result += arguments[i].toString();
    }

    if (i < len) {
      result += ' ';
    }
  }

  return result;
};

/**
  Log service (stores client-side logs, warns, errors, ... into application log).

  @class LogService
  @extends <a href="https://emberjs.com/api/ember/release/classes/Service">Service</a>
*/
export default Service.extend(Evented, {
  /**
    Cache containing references to original Logger methods.
    Cache is needed to restore original methods on service destroy.

    @property _originalMethodsCache
    @type Object[]
    @default null
    @private
  */
  _originalMethodsCache: null,

  /* Queue for storing to log operation calls */
  _queue: Queue.create(),

  /**
    Ember data store.

    @property store
    @type <a href="http://emberjs.com/api/data/classes/DS.Store.html">DS.Store</a>
  */
  store: service('store'),

  /**
    Flag: indicates whether log service is enabled or not (if not, nothing will be stored to application log).

    @property enabled
    @type Boolean
    @default false
    @example
    ```
    // LogServise 'enabled' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true
          }
          ...
        }
        ...
    };
    ```
  */
  enabled: false,

  /**
    The name of a model that represents log entity.

    @property applicationLogModelName
    @type String
    @default 'i-i-s-caseberry-logging-objects-application-log'
    @example
    ```
    // Log service 'applicationLogModelName' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            applicationLogModelName: 'custom-application-log'
          }
          ...
        }
        ...
    };
    ```
  */
  applicationLogModelName: 'i-i-s-caseberry-logging-objects-application-log',

  /**
    Flag: indicates whether log service will store error messages to application log or not.

    @property storeErrorMessages
    @type Boolean
    @default true
    @example
    ```
    // Log service 'storeErrorMessages' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            storeErrorMessages: false
          }
          ...
        }
        ...
    };
    ```
  */
  storeErrorMessages: true,

  /**
    Flag: indicates whether log service will store 'WARN' messages to application log or not.

    @property storeWarnMessages
    @type Boolean
    @default false
    @example
    ```
    // Log service 'storeWarnMessages' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            storeWarnMessages: true
          }
          ...
        }
        ...
    };
    ```
  */
  storeWarnMessages: false,

  /**
    Flag: indicates whether log service will store 'LOG' messages to application log or not.

    @property storeLogMessages
    @type Boolean
    @default false
    @example
    ```
    // Log service 'storeLogMessages' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            storeLogMessages: true
          }
          ...
        }
        ...
    };
    ```
  */
  storeLogMessages: false,

  /**
    Flag: indicates whether log service will store 'INFO' messages to application log or not.

    @property storeInfoMessages
    @type Boolean
    @default false
    @example
    ```
    // Log service 'storeInfoMessages' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            storeInfoMessages: true
          }
          ...
        }
        ...
    };
    ```
  */
  storeInfoMessages: false,

  /**
    Flag: indicates whether log service will store 'DEBUG' messages to application log or not.

    @property storeDebugMessages
    @type Boolean
    @default false
    @example
    ```
    // Log service 'storeDeprecationMessages' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            storeDebugMessages: true
          }
          ...
        }
        ...
    };
    ```
  */
  storeDebugMessages: false,

  /**
    Flag: indicates whether log service will store 'DEPRECATION' messages to application log or not.

    @property storeDeprecationMessages
    @type Boolean
    @default false
    @example
    ```
    // Log service 'storeDeprecationMessages' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            storeDeprecationMessages: true
          }
          ...
        }
        ...
    };
    ```
  */
  storeDeprecationMessages: false,

  /**
    Flag: indicates whether log service will store promise errors to application log or not.

    @property storePromiseErrors
    @type Boolean
    @default false
    @example
    ```
    // Log service 'storePromiseErrors' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            storePromiseErrors: true
          }
          ...
        }
        ...
    };
    ```
  */
  storePromiseErrors: false,

  /**
    Flag: indicates whether log service will display promise errors in console.

    @property showPromiseErrors
    @type Boolean
    @default false
    @example
    ```
    // Log service 'showPromiseErrors' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            showPromiseErrors: true
          }
          ...
        }
        ...
    };
    ```
  */
  showPromiseErrors: false,

  /**
    Flag: indicates whether log service will skip error messages that defined in errorMessageFilters array variable.

    @property errorMessageFilterActive
    @type Boolean
    @default false
    @example
    ```
    // Log service 'errorMessageFilterActive' setting could be also defined through application config/environment.js
    module.exports = function(environment) {
      var ENV = {
        ...
        APP: {
          ...
          log: {
            enabled: true,
            errorMessageFilterActive: true
          }
          ...
        }
        ...
    };
    ```
  */
  errorMessageFilterActive: false,

  /**
    Error messages which must be skipped when flag errorMessageFilterActive is true.

    @property errorMessageFilters
    @type Array
    @default [{ category: 'PROMISE', message: "TransitionAborted" }]
  */
  errorMessageFilters: A([
    { category: 'PROMISE', message: 'TransitionAborted' }
  ]),

  /**
    Initializes log service.
    Ember services are singletons, so this code will be executed only once since application initialization.
  */
  init() {
    this._super(...arguments);

    let _this = this;
    let originalMethodsCache = A();

    this.initProperties();

    let originalEmberLoggerError = Ember.Logger.error;
    originalMethodsCache.pushObject({
      methodOwner: Ember.Logger,
      methodName: 'error',
      methodReference: originalEmberLoggerError
    });

    let onError = function(error) {
      // If `this` is not undefined then assuming this function was called as promise error handler. So we not performing it.
      if (!this || error instanceof Error) {
        originalEmberLoggerError(error);
        _this._onError(error, false);
      }
    };

    let onPromiseError = function(reason) {
      if (_this.get('showPromiseErrors')) {
        originalEmberLoggerError(reason);
      }

      _this._onError(reason, true);
    };

    // Assign Ember.onerror & RSVP.on('error', ...) handlers (see http://emberjs.com/api/#event_onerror).
    Ember.onerror = onError;
    RSVP.on('error', onPromiseError);

    // Extend Ember.Logger.error logic.
    Ember.Logger.error = function() {
      originalEmberLoggerError(...arguments);

      return _this._queue.attach((resolve, reject) => {
        return _this._storeToApplicationLog(messageCategory.error, joinArguments(...arguments), '').then((result) => {
          resolve(result);
        }).catch((reason) => {
          reject(reason);
        });
      });
    };

    // Extend Ember.Logger.warn logic.
    let originalEmberLoggerWarn = Ember.Logger.warn;
    originalMethodsCache.pushObject({
      methodOwner: Ember.Logger,
      methodName: 'warn',
      methodReference: originalEmberLoggerWarn
    });

    Ember.Logger.warn = function() {
      originalEmberLoggerWarn(...arguments);

      return _this._queue.attach((resolve, reject) => {
        let message = joinArguments(...arguments);
        if (message.indexOf('DEPRECATION') === 0) {
          return _this._storeToApplicationLog(messageCategory.deprecate, message, '').then((result) => {
            resolve(result);
          }).catch((reason) => {
            reject(reason);
          });
        } else {
          return _this._storeToApplicationLog(messageCategory.warn, message, '').then((result) => {
            resolve(result);
          }).catch((reason) => {
            reject(reason);
          });
        }
      });
    };

    // Extend Ember.Logger.log logic.
    let originalEmberLoggerLog = Ember.Logger.log;
    originalMethodsCache.pushObject({
      methodOwner: Ember.Logger,
      methodName: 'log',
      methodReference: originalEmberLoggerLog
    });

    Ember.Logger.log = function() {
      originalEmberLoggerLog(...arguments);

      return _this._queue.attach((resolve, reject) => {
        return _this._storeToApplicationLog(messageCategory.log, joinArguments(...arguments), '').then((result) => {
          resolve(result);
        }).catch((reason) => {
          reject(reason);
        });
      });
   };

    // Extend Ember.Logger.info logic.
    let originalEmberLoggerInfo = Ember.Logger.info;
    originalMethodsCache.pushObject({
      methodOwner: Ember.Logger,
      methodName: 'info',
      methodReference: originalEmberLoggerInfo
    });

    Ember.Logger.info = function() {
      originalEmberLoggerInfo(...arguments);

      return _this._queue.attach((resolve, reject) => {
        return _this._storeToApplicationLog(messageCategory.info, joinArguments(...arguments), '').then((result) => {
          resolve(result);
        }).catch((reason) => {
          reject(reason);
        });
      });
    };

    // Extend Ember.Logger.debug logic.
    let originalEmberLoggerDebug = Ember.Logger.debug;
    originalMethodsCache.pushObject({
      methodOwner: Ember.Logger,
      methodName: 'debug',
      methodReference: originalEmberLoggerDebug
    });

    Ember.Logger.debug = function() {
      originalEmberLoggerDebug(...arguments);

      return _this._queue.attach((resolve, reject) => {
        return _this._storeToApplicationLog(messageCategory.debug, joinArguments(...arguments), '').then((result) => {
          resolve(result);
        }).catch((reason) => {
          reject(reason);
        });
      });
    };

    this.set('_originalMethodsCache', originalMethodsCache);
  },

  /**
   * Initializes properties of a log service.
   */
  initProperties() {
    const config = getOwner(this).resolveRegistration('config:environment');
    const logConfiguration = config.APP.log;

    this.set('enabled', typeof logConfiguration.enabled === 'boolean' && logConfiguration.enabled);
    this.set('storeErrorMessages', typeof logConfiguration.storeErrorMessages === 'boolean' && logConfiguration.storeErrorMessages);
    this.set('storeWarnMessages', typeof logConfiguration.storeWarnMessages === 'boolean' && logConfiguration.storeWarnMessages);
    this.set('storeLogMessages', typeof logConfiguration.storeLogMessages === 'boolean' && logConfiguration.storeLogMessages);
    this.set('storeInfoMessages', typeof logConfiguration.storeInfoMessages === 'boolean' && logConfiguration.storeInfoMessages);
    this.set('storeDebugMessages', typeof logConfiguration.storeDebugMessages === 'boolean' && logConfiguration.storeDebugMessages);
    this.set('storeDeprecationMessages', typeof logConfiguration.storeDeprecationMessages === 'boolean' && logConfiguration.storeDeprecationMessages);
    this.set('storePromiseErrors', typeof logConfiguration.storePromiseErrors === 'boolean' && logConfiguration.storePromiseErrors);
    this.set('showPromiseErrors', typeof logConfiguration.showPromiseErrors === 'boolean' && logConfiguration.showPromiseErrors);
    this.set('errorMessageFilterActive', typeof logConfiguration.errorMessageFilterActive === 'boolean' && logConfiguration.errorMessageFilterActive);

    if (typeof logConfiguration.applicationLogModelName === 'string') {
      this.set('applicationLogModelName', logConfiguration.applicationLogModelName);
    }
  },

  /**
    Destroys log service.
  */
  willDestroy() {
    this._super(...arguments);

    // Restore original Ember.Logger methods.
    let originalMethodsCache = this.get('_originalMethodsCache');
    if (isArray(originalMethodsCache)) {
      originalMethodsCache.forEach((cacheEntry) => {
        set(cacheEntry.methodOwner, cacheEntry.methodName, cacheEntry.methodReference);
      });
    }

    // Cleanup Ember.onerror & RSVP.on('error', ...) handlers (see http://emberjs.com/api/#event_onerror).
    Ember.onerror = null;
    RSVP.off('error');
  },

  /**
    Stores given message to application log.

    @method _storeToApplicationLog
    @param {String} category Message category: 'ERROR', 'WARN', 'LOG', 'INFO', 'DEBUG', 'DEPRECATION'.
    @param {String} message Message itself.
    @param {String} formattedMessage Full message content in JSON format.
    @private
  */
  _storeToApplicationLog(category, message, formattedMessage) {
    let isSkippedMessage = false;
    let errorMessageFilters = this.get('errorMessageFilters');
    let errorMessageFilterActive = this.get('errorMessageFilterActive');

    if (errorMessageFilterActive) {
      errorMessageFilters.forEach(errorMessageFilter => {
        if (category.name === errorMessageFilter.category && message.indexOf(errorMessageFilter.message) !== -1) {
          isSkippedMessage = true;
        }
      });
    }

    if (!this.get('enabled') || isSkippedMessage ||
      category.name === messageCategory.error.name && !this.get('storeErrorMessages') ||
      category.name === messageCategory.warn.name && !this.get('storeWarnMessages') ||
      category.name === messageCategory.log.name && !this.get('storeLogMessages') ||
      category.name === messageCategory.info.name && !this.get('storeInfoMessages') ||
      category.name === messageCategory.debug.name && !this.get('storeDebugMessages') ||
      category.name === messageCategory.deprecate.name && !this.get('storeDeprecationMessages') ||
      category.name === messageCategory.promise.name && !this.get('storePromiseErrors')) {
      return new RSVP.Promise((resolve) => {
        this._triggerEvent(category.name);
        resolve();
      });
    }

    let appConfig = getOwner(this).factoryFor('config:environment').class;
    let applicationLogProperties = {
      category: category.name,
      eventId: 0,
      priority: category.priority,
      severity: '',
      title: '',
      timestamp: new Date(),
      machineName:  location.hostname,
      appDomainName: navigator.userAgent,
      processId: document.location.href,
      processName: 'EMBER-FLEXBERRY',
      threadName: appConfig.modulePrefix,
      win32ThreadId: '',
      message: message,
      formattedMessage: formattedMessage
    };

    const applicationLogModelName = this.get('applicationLogModelName');
    let store = this.get('store');

    // Break if message already exists in store (to avoid infinit loop when message is generated while saving itself).
    let applicationLogModel = store.peekAll(applicationLogModelName).findBy('message', message);
    if (applicationLogModel !== undefined) {
      /* eslint-disable no-unused-vars */
      return new RSVP.Promise((resolve, reject) => {
        this._triggerEvent(category.name, applicationLogModel);
        resolve();
      });
      /* eslint-enable no-unused-vars */
    }

    /* eslint-disable no-unused-vars */
    return new RSVP.Promise((resolve, reject) => {
      store.createRecord(applicationLogModelName, applicationLogProperties).save().then((applicationLogModel) => {
        this._triggerEvent(category.name, applicationLogModel);
        resolve(applicationLogModel);
      }).catch((reason) => {
        // Switch off remote logging on rejection to avoid infinite loop.
        this.set('enabled', false);
        reject(reason);
      });
    });
    /* eslint-enable no-unused-vars */
  },

  _triggerEvent(eventName, applicationLogModel) {
    assert('Logger Error: event name should be a string', typeof eventName === 'string');
    let eventNameToTrigger = eventName.toLowerCase();
    this.trigger(eventNameToTrigger, applicationLogModel);
  },

  _onError(error, isPromiseError) {
    let _this = this;
    _this._queue.attach((resolve, reject) => {
      if (isNone(error)) {
        resolve();
      }

      if (typeof error === 'string') {
        error = new Error(error);
      }

      const message = get(error, 'message') || error.toString();

      let formattedMessageBlank = {
        name: error && error.name ? error.name : null,
        message: error && error.message ? error.message : null,
        fileName: error && error.fileName ? error.fileName : null,
        lineNumber: error && error.lineNumber ? error.lineNumber : null,
        columnNumber: error && error.columnNumber ? error.columnNumber : null,
        stack: error && error.stack ? error.stack : null
      };

      let formattedMessage = JSON.stringify(formattedMessageBlank);

      return _this._storeToApplicationLog(isPromiseError ? messageCategory.promise : messageCategory.error, message, formattedMessage).then((result) => {
        resolve(result);
      }).catch((reason) => {
        reject(reason);
      });
    });
  }
});