findanyemail/winston-transport-stackdriver-error-reporting

View on GitHub
src/index.js

Summary

Maintainability
A
0 mins
Test Coverage
import forOwn from 'for-own'
import isPlainObject from 'is-plain-object'
import errorReporter from '@google-cloud/error-reporting'
import {Transport} from 'winston/lib/winston/transports/transport'

/**
 * Winston transport for Stackdriver Error Reporting service.
 */
class StackdriverErrorReporting extends Transport {

  /**
   * Class constructor.
   *
   * @param {Object} options Configuration options.
   */
  constructor(options = {}) {

    super(options);

    this.name = 'StackdriverErrorReporting';
    this.level = options.level || 'error';
    this.mode = options.mode.toLowerCase() || 'api';
    this.serviceContext = options.serviceContext || {service: 'unknown', version: 'unknown'};
    if (this.mode !== 'console' && this.mode !== 'api') {
      throw new Error('StackdriverErrorReporting: parameter "mode" is expected to be "console" or "api".');
    }

    if (this.mode === 'api') {
      this._client = errorReporter(Object.assign({}, options, {
        ignoreEnvironmentCheck: true,
      }));
    }
  }

  /**
   * Send entry to Stackdriver Error Reporting.
   *
   * @param {String} level - Winston log entry level.
   * @param {String} message - Log entry message.
   * @param {Object} meta - Winston meta information.
   * @param {Function} callback - Callback to let Winston know we are done.
   */
  log(level, message, meta, callback) {

    const promise = (this.mode === 'api')
      ? this.logApi(level, message, meta)
      : this.logConsole(level, message, meta);

    promise
      .then(() => callback(null, true))
      .catch((error) => callback(error, false));
  }

  /**
   * Send entry to Stackdriver Error Reporting with a help of console.log().
   *
   * @param {String} level - Winston log entry level.
   * @param {String} message - Log entry message.
   * @param {Object} meta - Winston meta information.
   * @return {Promise}
   */
  logConsole(level, message, meta) {

    const eventTime = (new Date()).toISOString();
    const errors = this.extractErrorsFromMeta(meta);
    errors.forEach((error) => {

      const stackTrace = Array.isArray(error.stack) ? error.stack.join("\n") : error.stack;
      const message = JSON.stringify({
        eventTime,
        message: stackTrace,
        severity: level.toUpperCase(),
        serviceContext: this.serviceContext,
      });
      if (meta.context) {
        message.context = meta.context;
      }

      console.log(message);
    });

    return Promise.resolve();
  }

  /**
   * Send entry to Stackdriver Error Reporting with a help of API.
   *
   * @param {String} level - Winston log entry level.
   * @param {String} message - Log entry message.
   * @param {Object} meta - Winston meta information.
   * @return {Promise}
   */
  logApi(level, message, meta) {

    const promises = [];

    const errors = this.extractErrorsFromMeta(meta);
    errors.forEach((error) => {

      const stackTrace = Array.isArray(error.stack) ? error.stack.join("\n") : error.stack;
      promises.push(
        new Promise((resolve, reject) => {
          let request = null;
          if (meta.context && meta.context.httpRequest) {
            request = meta.context.httpRequest;
          }
          this._client.report(error, request, stackTrace, (reportError) => {
            if (reportError) {
              console.log('Failed to send error to Stackdriver Error Reporting.', reportError);
              return reject(reportError);
            }
            return resolve();
          })
        })
      );
    });

    return Promise.all(promises);
  }

  /**
   * Extracts all errors from Winston metadata.
   *
   * @param {Object} data - Winston meta data.
   * @return {Array}
   */
  extractErrorsFromMeta(data) {
    if (!data) {
      return [];
    }

    let errors = [];

    if (data.stack) {
      errors.push(data);
    } else if (isPlainObject(data)) {
      forOwn(data, (value) => {
        errors = errors.concat(this.extractErrorsFromMeta(value));
      });
    } else if (Array.isArray(data)) {
      data.forEach((value) => {
        errors = errors.concat(this.extractErrorsFromMeta(value));
      });
    }

    return errors;
  }
}

export default StackdriverErrorReporting