chromium/chrome/browser/resources/nearby_internals/logging_tab.ts

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import './log_object.js';
import './shared_style.css.js';

import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {NearbyLogsBrowserProxy} from './cross_device_logs_browser_proxy.js';
import {getTemplate} from './logging_tab.html.js';
import type {LogMessage, LogProvider, SelectOption} from './types.js';
import {Severity} from './types.js';

/**
 * Converts log message to string format for saved download file.
 */
function logToSavedString(log: LogMessage): string {
  // Convert to string value for |line.severity|.
  let severity;
  switch (log.severity) {
    case Severity.INFO:
      severity = 'INFO';
      break;
    case Severity.WARNING:
      severity = 'WARNING';
      break;
    case Severity.ERROR:
      severity = 'ERROR';
      break;
    case Severity.VERBOSE:
      severity = 'VERBOSE';
      break;
  }

  // Reduce the file path to just the file name for logging simplification.
  const file = log.file.substring(log.file.lastIndexOf('/') + 1);

  return `[${log.time} ${severity} ${file} (${log.line})] ${log.text}\n`;
}

const nearbyShareLogProvider: LogProvider = {
  messageAddedEventName: 'log-message-added',
  bufferClearedEventName: 'log-buffer-cleared',
  logFilePrefix: 'nearby_internals_logs_',
  getLogMessages: () => NearbyLogsBrowserProxy.getInstance().getLogMessages(),
};

const quickPairLogProvider: LogProvider = {
  messageAddedEventName: 'quick-pair-log-message-added',
  bufferClearedEventName: 'quick-pair-log-buffer-cleared',
  logFilePrefix: 'fast_pair_logs_',
  getLogMessages: () =>
      NearbyLogsBrowserProxy.getInstance().getQuickPairLogMessages(),
};

/**
 * Gets a log provider instance for a feature.
 */
function getLogProvider(feature: string): LogProvider {
  switch (feature) {
    case 'nearby-share':
      return nearbyShareLogProvider;
    case 'quick-pair':
      return quickPairLogProvider;
    default:
      return quickPairLogProvider;
  }
}


const LoggingTabElementBase = WebUiListenerMixin(PolymerElement);

class LoggingTabElement extends LoggingTabElementBase {
  static get is() {
    return 'logging-tab';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      logList_: {
        type: Array,
        value: () => [],
      },

      filteredLogList_: {
        type: Array,
        value: () => [],
      },

      feature: {
        type: String,
      },

      currentFilter_: {
        type: String,
      },

      currentSeverity_: {
        type: Severity,
        value: Severity.VERBOSE,
      },

      logLevelList_: {
        type: Array,
        value: [
          {name: 'VERBOSE', value: Severity.VERBOSE},
          {name: 'INFO', value: Severity.INFO},
          {name: 'WARNING', value: Severity.WARNING},
          {name: 'ERROR', value: Severity.ERROR},
        ],
      },

    };
  }

  private logList_: LogMessage[];
  private filteredLogList_: LogMessage[];
  private feature: string;
  private currentFilter_: string;
  private currentSeverity_: Severity;
  private logLevelList_: SelectOption[];
  private logProvider_: LogProvider;

  /**
   * When the page is initialized, notify the C++ layer and load in the
   * contents of its log buffer. Initialize WebUI Listeners.
   */
  override connectedCallback() {
    super.connectedCallback();

    this.logProvider_ = getLogProvider(this.feature);
    this.addWebUiListener(
        this.logProvider_.messageAddedEventName,
        (log: LogMessage) => this.onLogMessageAdded_(log));
    this.addWebUiListener(
        this.logProvider_.bufferClearedEventName,
        () => this.onWebUiLogBufferCleared_());
    this.logProvider_.getLogMessages().then(
        logs => this.onGetLogMessages_(logs));
  }

  /**
   * Clears javascript logs displayed, but c++ log buffer remains.
   */
  private onClearLogsButtonClicked_(): void {
    this.clearLogBuffer_();
  }

  /**
   * Saves and downloads all javascript logs.
   */
  private onSaveUnfilteredLogsButtonClicked_(): void {
    this.onSaveLogsButtonClicked_(false);
  }

  /**
   * Saves and downloads javascript logs that currently appear on the page.
   */
  private onSaveFilteredLogsButtonClicked_(): void {
    this.onSaveLogsButtonClicked_(true);
  }

  /**
   * Saves and downloads javascript logs.
   */
  private onSaveLogsButtonClicked_(filtered: boolean): void {
    let blob;
    if (filtered) {
      blob = new Blob(
          this.filteredLogList_.map(logToSavedString),
          {type: 'text/plain;charset=utf-8'});
    } else {
      blob = new Blob(
          this.logList_.map(logToSavedString),
          {type: 'text/plain;charset=utf-8'});
    }
    const url = URL.createObjectURL(blob);

    const anchorElement = document.createElement('a');
    anchorElement.href = url;
    anchorElement.download =
        this.logProvider_.logFilePrefix + new Date().toJSON() + '.txt';
    document.body.appendChild(anchorElement);
    anchorElement.click();

    window.setTimeout(function() {
      document.body.removeChild(anchorElement);
      window.URL.revokeObjectURL(url);
    }, 0);
  }

  /**
   * Adds a log message to the javascript log list displayed. Called from the
   * C++ WebUI handler when a log message is added to the log buffer.
   */
  private onLogMessageAdded_(log: LogMessage): void {
    this.push('logList_', log);
    if ((log.text.match(this.currentFilter_) ||
         log.file.match(this.currentFilter_)) &&
        log.severity >= this.currentSeverity_) {
      this.push('filteredLogList_', log);
    }
  }

  private addLogFilter_(): void {
    const logLevelSelector: HTMLSelectElement|null =
        this.shadowRoot!.querySelector('#logLevelSelector');
    if (logLevelSelector) {
      switch (Number(logLevelSelector.value)) {
        case Severity.VERBOSE:
          this.set(
              'filteredLogList_',
              this.logList_.filter((log) => log.severity >= Severity.VERBOSE));
          this.currentSeverity_ = Severity.VERBOSE;
          break;
        case Severity.INFO:
          this.set(
              'filteredLogList_',
              this.logList_.filter((log) => log.severity >= Severity.INFO));
          this.currentSeverity_ = Severity.INFO;
          break;
        case Severity.WARNING:
          this.set(
              'filteredLogList_',
              this.logList_.filter((log) => log.severity >= Severity.WARNING));
          this.currentSeverity_ = Severity.WARNING;
          break;
        case Severity.ERROR:
          this.set(
              'filteredLogList_',
              this.logList_.filter((log) => log.severity >= Severity.ERROR));
          this.currentSeverity_ = Severity.ERROR;
          break;
      }
    }

    const elem: HTMLSelectElement|null =
        this.shadowRoot!.querySelector('#logSearch');
    if (elem) {
      this.currentFilter_ = elem.value;
      this.set(
          'filteredLogList_',
          this.filteredLogList_.filter(
              (log) =>
                  (log.text.match(this.currentFilter_) ||
                   log.file.match(this.currentFilter_))));
    }
  }

  /**
   * Called in response to WebUI handler clearing log buffer.
   */
  private onWebUiLogBufferCleared_(): void {
    this.clearLogBuffer_();
  }

  /**
   * Parses an array of log messages and adds to the javascript list sent in
   * from the initial page load.
   */
  private onGetLogMessages_(logs: LogMessage[]): void {
    this.logList_ = logs.concat(this.logList_);
    this.filteredLogList_ = logs.concat(this.filteredLogList_);
  }

  /**
   * Clears the javascript log buffer.
   */
  private clearLogBuffer_(): void {
    this.logList_ = [];
    this.filteredLogList_ = [];
  }
}

customElements.define(LoggingTabElement.is, LoggingTabElement);