cloudfoundry/stratos

View on GitHub
src/frontend/packages/cloud-foundry/src/features/cf/tabs/cf-firehose/cloud-foundry-firehose-formatter.ts

Summary

Maintainability
A
1 hr
Test Coverage
import moment from 'moment';

import { UtilsService } from '../../../../../../core/src/core/utils.service';
import { AnsiColorizer } from '../../../../../../core/src/shared/components/log-viewer/ansi-colorizer';
import { FireHoseItem, HTTP_METHODS } from './cloud-foundry-firehose.types';

/**
 * Formats log messages from the Cloud Foundry firehose
 */

/* eslint-disable no-control-regex */
const ANSI_ESCAPE_MATCHER = new RegExp('\x1B\\[([0-9;]*)m', 'g');
/* eslint-enable no-control-regex */

// Format the messages from the Cloud Foundry firehose
export class CloudFoundryFirehoseFormatter {

  // Current filters
  public hoseFilters = {
    api: true,
    apps: true,
    metrics: true,
    counters: true,
    errors: true,
    containerMetrics: true,
    others: true
  };

  private colorizer = new AnsiColorizer();

  constructor(private utils: UtilsService) { }

  // Enable or disable all filters
  public showAll(all: boolean) {
    Object.keys(this.hoseFilters).forEach(filter => this.hoseFilters[filter] = all);
  }

  /**
   * Filter and format Firehose JSOM log message
   */
  public jsonFilter(jsonString: string): string {
    let filtered = jsonString;
    try {
      const cfEvent: FireHoseItem = JSON.parse(jsonString);
      switch (cfEvent.eventType) {
        case 4:
          filtered = this.handleApiEvent(cfEvent);
          break;
        case 5:
          filtered = this.handleAppLog(cfEvent);
          break;
        case 6:
          filtered = this.handleMetricEvent(cfEvent);
          break;
        case 7:
          filtered = this.handleCounterEvent(cfEvent);
          break;
        case 8:
          filtered = this.handleErrorEvent(cfEvent);
          break;
        case 9:
          filtered = this.handleContainerMetricsEvent(cfEvent);
          break;
        default:
          filtered = this.handleOtherEvent(cfEvent);
      }
    } catch (error) {
      console.error('Failed to filter jsonMessage from WebSocket: ' + jsonString);
      filtered = jsonString;
    }

    return filtered;
  }

  private buildOriginString(cfEvent, colour, bold?: boolean) {
    return this.buildTimestampString(cfEvent) + ': ' +
      this.colorizer.colorize('[' + cfEvent.deployment + '/' + cfEvent.origin + '/' + cfEvent.job + ']', colour, bold);
  }

  private buildTimestampString(cfEvent) {
    // CF time stamps are in nanoseconds
    const msStamp = Math.round(cfEvent.timestamp / 1000000);
    return moment(msStamp).format('HH:mm:ss.SSS');
  }

  private emphasizeName(dottedString, colour) {
    let metricName = dottedString;
    const lastDot = metricName.lastIndexOf('.');
    if (lastDot > -1) {
      // Weird bug where sometimes the name ends with a dot
      if (lastDot === dottedString.length - 1) {
        return this.colorizer.colorize(metricName.slice(0, -1), colour, true);
      }
      const prefix = metricName.slice(0, lastDot + 1);
      const name = metricName.slice(lastDot + 1);
      metricName = prefix + this.colorizer.colorize(name, colour, true);
    } else {
      metricName = this.colorizer.colorize(metricName, colour, true);
    }
    return metricName;
  }

  private handleApiEvent(cfEvent) {
    if (!this.hoseFilters.api) {
      return '';
    }
    const httpStartStop = cfEvent.httpStartStop;
    const method = HTTP_METHODS[httpStartStop.method - 1];
    const peerType = httpStartStop.peerType === 1 ? 'Client' : 'Server';
    const httpEventString = peerType + ' ' + this.colorizer.colorize(method, 'magenta', true) + ' ' +
      this.colorizer.colorize(httpStartStop.uri, null, true) +
      ', Status-Code: ' + this.colorizer.colorize(httpStartStop.statusCode, 'green') +
      ', Content-Length: ' + this.colorizer.colorize(this.utils.bytesToHumanSize(httpStartStop.contentLength), 'green') +
      ', User-Agent: ' + this.colorizer.colorize(httpStartStop.userAgent, 'green') +
      ', Remote-Address: ' + this.colorizer.colorize(httpStartStop.remoteAddress, 'green') + '\n';
    return this.buildOriginString(cfEvent, 'magenta') + ' ' + httpEventString;
  }

  handleAppLog(cfEvent) {
    if (!this.hoseFilters.apps) {
      return '';
    }
    const message = cfEvent.logMessage;
    let colour;
    if (message.message_type === 2) {
      colour = 'red';
    }
    const messageSource = this.colorizer.colorize('[' + message.source_type + '.' + message.source_instance + ']', 'green', true);
    const messageString = this.colorizer.colorize(atob(message.message), colour, false) + '\n';
    return this.buildOriginString(cfEvent, 'green') + ' ' + messageSource + ' ' + messageString;
  }

  handleMetricEvent(cfEvent) {
    if (!this.hoseFilters.metrics) {
      return '';
    }
    const valueMetric = cfEvent.valueMetric;
    const valueMetricString = this.emphasizeName(valueMetric.name, 'blue') + ': ' +
      this.colorizer.colorize(valueMetric.value + ' ' + valueMetric.unit, 'green', true) + '\n';
    return this.buildOriginString(cfEvent, 'blue') + ' ' + valueMetricString;
  }

  handleCounterEvent(cfEvent) {
    if (!this.hoseFilters.counters) {
      return '';
    }
    const counterEvent = cfEvent.counterEvent;
    let delta;
    let total;
    if (counterEvent.name.indexOf('ByteCount') !== -1) {
      delta = this.utils.bytesToHumanSize(counterEvent.delta);
      total = this.utils.bytesToHumanSize(counterEvent.total);
    } else {
      delta = counterEvent.delta;
      total = counterEvent.total;
    }
    const counterEventString = this.emphasizeName(counterEvent.name, 'yellow') +
      ': delta = ' + this.colorizer.colorize(delta, 'green', true) +
      ', total = ' + this.colorizer.colorize(total, 'green', true) + '\n';
    return this.buildOriginString(cfEvent, 'yellow') + ' ' + counterEventString;
  }

  handleContainerMetricsEvent(cfEvent) {
    if (!this.hoseFilters.containerMetrics) {
      return '';
    }
    const containerMetric = cfEvent.containerMetric;
    const metricString = 'App: ' + containerMetric.applicationId + '/' + containerMetric.instanceIndex +
      ' ' + this.colorizer.colorize('[', 'cyan', true) + this.colorizer.colorize('CPU: ', 'cyan', true) +
      this.colorizer.colorize(Math.round(containerMetric.cpuPercentage * 100) + '%', 'green', true) +
      ', ' + this.colorizer.colorize('Memory: ', 'cyan', true) +
      this.colorizer.colorize(this.utils.bytesToHumanSize(containerMetric.memoryBytes), 'green', true) +
      ', ' + this.colorizer.colorize('Disk: ', 'cyan', true) +
      this.colorizer.colorize(this.utils.bytesToHumanSize(containerMetric.diskBytes), 'green', true) +
      this.colorizer.colorize(']', 'cyan', true);
    return this.buildOriginString(cfEvent, 'cyan') + ' ' + metricString + '\n';
  }

  handleErrorEvent(cfEvent) {
    if (!this.hoseFilters.errors) {
      return '';
    }
    const errorObj = cfEvent.error;
    const errorString = 'ERROR: Source: ' + this.colorizer.colorize(errorObj.source, 'red', true) +
      ', Code: ' + this.colorizer.colorize(errorObj.code, 'red', true) +
      ', Message: ' + this.colorizer.colorize(errorObj.message, 'red', true);
    return this.buildOriginString(cfEvent, 'red', true) + ' ' + errorString + '\n';
  }

  handleOtherEvent(jsonString) {
    if (!this.hoseFilters.others) {
      return '';
    }
    return jsonString;
  }

  // Map each character index in the sanitized version of originalString to its original index in originalString
  mapSanitizedIndices(originalString) {
    let escapeMatch = ANSI_ESCAPE_MATCHER.exec(originalString);
    const mappedIndices = {};
    let mappedUpTo = 0;
    let offset = 0;

    ANSI_ESCAPE_MATCHER.lastIndex = 0;
    while (escapeMatch !== null) {
      while (mappedUpTo + offset < escapeMatch.index) {
        mappedIndices[mappedUpTo] = offset + mappedUpTo++;
      }
      offset += escapeMatch[0].length;
      escapeMatch = ANSI_ESCAPE_MATCHER.exec(originalString);
    }
    while (mappedUpTo + offset < originalString.length) {
      mappedIndices[mappedUpTo] = offset + mappedUpTo++;
    }
    return mappedIndices;
  }

  // Determine which colour and bold modes are active where the highlight ends
  getPreviousModes(toEndOfMatch) {
    let escapeMatch = ANSI_ESCAPE_MATCHER.exec(toEndOfMatch);
    let boldOn = null;
    let prevColour = null;
    let lastColourMatches = null;

    ANSI_ESCAPE_MATCHER.lastIndex = 0;
    while (escapeMatch !== null) {
      lastColourMatches = escapeMatch;
      escapeMatch = ANSI_ESCAPE_MATCHER.exec(toEndOfMatch);
    }
    if (lastColourMatches !== null) {
      boldOn = lastColourMatches[1].indexOf('1') === 0;
      if (lastColourMatches[1] === '1') {
        prevColour = null;
      } else {
        if (boldOn) {
          prevColour = lastColourMatches[1][3];
        } else {
          prevColour = lastColourMatches[1][1];
        }
      }
    }

    return {
      bold: boldOn,
      colour: prevColour
    };
  }
}