monitoring-8.x-1.x-dev/src/Plugin/monitoring/SensorPlugin/ResponseTimeSensorPlugin.php

src/Plugin/monitoring/SensorPlugin/ResponseTimeSensorPlugin.php
<?php

namespace Drupal\monitoring\Plugin\monitoring\SensorPlugin;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\monitoring\Attribute\SensorPlugin;
use Drupal\monitoring\Result\SensorResultInterface;
use Drupal\monitoring\SensorPlugin\SensorPluginBase;

/**
 * Monitors response times from a PHP access log.
 */
#[SensorPlugin(
  id: 'response_time',
  label: new TranslatableMarkup('Response time'),
  addable: TRUE,
  metric_type: 'gauge',
)]
class ResponseTimeSensorPlugin extends SensorPluginBase {

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    $form['main_metric'] = [
      '#type' => 'select',
      '#title' => $this->t('Main monitored metric'),
      '#options' => [
        'average' => $this->t('Average'),
        'percentile_90' => $this->t('90th percentile'),
        'percentile_95' => $this->t('95th percentile'),
      ],
      '#default_value' => $this->sensorConfig->getSetting('main_metric'),
      '#description' => $this->t('You can select which metric is used for the warning and critical sensors. All metrics are available in the log.'),
      '#required' => TRUE,
    ];
    $form['lines'] = [
      '#type' => 'number',
      '#title' => $this->t('Number of entries assessed'),
      '#default_value' => $this->sensorConfig->getSetting('lines'),
      '#description' => $this->t('How many entries are assessed for this metric. The time frame that this represents depends on the overall traffic of the website.'),
      '#required' => TRUE,
    ];
    $form['log_file'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Location of the log file'),
      '#default_value' => $this->sensorConfig->getSetting('log_file'),
      '#description' => $this->t('File where the access time requests are logged (php.access.log).'),
      '#required' => TRUE,
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function runSensor(SensorResultInterface $result) {
    $time_response_data = $this->getTimeResponseData();
    if (!$time_response_data['times']) {
      $result->setStatus(SensorResultInterface::STATUS_WARNING);
      $result->setMessage('Log file @filename is empty.', [
        '@filename' => $this->sensorConfig->getSetting('log_file'),
      ]);
    }
    else {
      $setting = $this->sensorConfig->getSetting('main_metric');
      $times = array_values($time_response_data['times']);
      $average = (int) (array_sum($times) / count($times));
      $percentile_90 = (int) $this->getPercentile($times, 90);
      $percentile_95 = (int) $this->getPercentile($times, 95);
      $result->setValue($$setting);
      $result->addStatusMessage( 'Average: ' . $average . ' ms, 90%: ' . $percentile_90 . ' ms, 95%: ' . $percentile_95 . 'ms, start date: ' . $time_response_data['start']);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getDefaultConfiguration() {
    return [
      'caching_time' => 0,
      'value_type' => 'number',
      'settings' => [
        'main_metric' => 'percentile_90',
        'lines' => '1000',
        'log_file' => '',
      ],
    ];
  }

  /**
   * Gets the response time data.
   *
   * @return array
   *   The response time data.
   *
   * @throws \Exception
   *   Thrown when the log file cannot be found or read.
   */
  protected function getTimeResponseData(): array {
    $lines = $this->sensorConfig->getSetting('lines');
    $filename = $this->sensorConfig->getSetting('log_file');
    if (!$filename) {
      throw new \Exception(t('Log file not defined.'));
    }
    $data = $this->getEndOfFile($filename, $lines);

    $start = explode(' ', $data[0])[0];
    $times = [];
    foreach ($data as $line) {
      // 2020-05-07 00:17:04 GET 301 245.353 ms 16384 kB 36.68% /
      if (preg_match('/(\d{4}-\d{2}-\d{2}).+ ([0-9\.]+) ms/', $line, $match)) {
        $times[] = $match[2];
      }
    }
    // Keeping raw data for further improvements.
    return ['times' => $times, 'start' => $start, 'raw_data' => $lines];
  }

  /**
   * Returns the value of the metric corresponding to the given percentile.
   *
   * @param array $array
   *   Array of values.
   * @param int $percentile
   *   Which percentile to get (1 to 99).
   *
   * @return float|int|mixed
   *   Percentile value.
   */
  protected function getPercentile(array $array, int $percentile) {
    $percentile = min(100, max(0, $percentile));
    $array = array_values($array);
    sort($array);
    $index = ($percentile / 100) * (count($array) - 1);
    $fraction_part = $index - floor($index);
    $int_part = floor($index);

    $percentile = $array[$int_part];
    $percentile += ($fraction_part > 0) ? $fraction_part * ($array[$int_part + 1] - $array[$int_part]) : 0;

    return $percentile;
  }

  /**
   * Returns an array with the n lines at the end of the file.
   *
   * @param string $filepath
   *   Path of the file.
   * @param int $lines
   *   Number of lines.
   *
   * @return string[]
   *   Array with the n lines at the end of the file.
   *
   * @throws \Exception
   *   Thrown when the log file cannot be found or read.
   */
  protected function getEndOfFile(string $filepath, int $lines = 1) {
    // From https://gist.github.com/lorenzos/1711e81a9162320fde20.
    // https://stackoverflow.com/questions/15025875/what-is-the-best-way-to-read-last-lines-i-e-tail-from-a-file-using-php/15025877#15025877.

    // Open file.
    if (!file_exists($filepath)) {
      throw new \Exception(new FormattableMarkup('Log file @filename not found.', [
        '@filename' => $this->sensorConfig->getSetting('log_file'),
      ]));
    }
    $f = fopen($filepath, "rb");

    if ($f === FALSE) {
      throw new \Exception(new FormattableMarkup('Log file @filename could not be read.', [
        '@filename' => $this->sensorConfig->getSetting('log_file'),
      ]));
    }

    $buffer = 4096;

    // Jump to last character.
    fseek($f, -1, SEEK_END);

    // Read it and adjust line number if necessary.
    // (Otherwise the result would be wrong
    // if file doesn't end with a blank line).
    if (fread($f, 1) != "\n") {
      $lines -= 1;
    }

    // Start reading.
    $output = '';
    // While we would like more.
    while (ftell($f) > 0 && $lines >= 0) {
      // Figure out how far back we should jump.
      $seek = min(ftell($f), $buffer);
      // Do the jump (backwards, relative to where we are).
      fseek($f, -$seek, SEEK_CUR);
      // Read a chunk and prepend it to our output.
      $output = ($chunk = fread($f, $seek)) . $output;
      // Jump back to where we started reading.
      fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
      // Decrease our line counter.
      $lines -= substr_count($chunk, "\n");
    }
    // While we have too many lines.
    // (Because of buffer size we might have read too many).
    while ($lines++ < 0) {
      // Find first newline and remove all text before that.
      $output = substr($output, strpos($output, "\n") + 1);
    }
    // Close file and return.
    fclose($f);
    return explode("\n", trim($output));
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc