Browse Source

First commit: working Reader, basic module in place.

- shapeless tests, to convert to PHPunit.
Frederic G. MARAND 9 years ago
commit
1ac39a7254

+ 55 - 0
lib/Drupal/redis_logger/Tests/test_scan.php

@@ -0,0 +1,55 @@
+<?php
+
+use Redis\Logger\Reader;
+use Redis\Logger\Settings;
+
+$redis = new Redis();
+$redis->connect('localhost');
+
+require_once "lib/Redis/Logger/Settings.php";
+require_once "lib/Redis/Logger/Entry.php";
+require_once "lib/Redis/Logger/Reader.php";
+require_once "lib/Redis/Logger/Writer.php";
+
+$reader = new Reader($redis);
+echo "\nscan()-ing Template pattern: " . Settings::REDIS_PATTERN . "\n";
+echo implode(', ', $templates = $reader->scan(Settings::REDIS_PATTERN, 0, 50)) . "\n\n";
+
+echo "getChannels: ";
+$channels = $reader->getChannels();
+echo implode(', ', $channels) . "\n\n";
+
+echo "getTemplates():\n";
+$templates = $reader->getTemplates();
+echo "Result: " . implode(', ', $templates) .   "\n\n";
+
+echo "getTemplates('cli'):\n";
+$templates = $reader->getTemplates("cli");
+echo "Result: " . implode(', ', $templates) .   "\n\n";
+
+echo "getTemplates('foo'):\n";
+$templates = $reader->getTemplates("foo");
+echo "Result: " . implode(', ', $templates) . "\n\n";
+
+echo "getTemplates(NULL, 4):\n";
+$templates = $reader->getTemplates(NULL, 4);
+echo "Result: " . implode(', ', $templates) . "\n\n";
+
+echo "getTemplates(NULL, 5):\n";
+$templates = $reader->getTemplates(NULL, 5);
+echo "Result: " . implode(', ', $templates) . "\n\n";
+
+foreach ($reader->getTemplates() as $template) {
+  echo "First page of events for template <$template>:\n";
+  $entries = $reader->getEntries($template);
+  foreach ($entries as $entry) {
+    echo "$entry\n";
+  }
+}
+
+echo "First page of events for template <nonexistent>:\n";
+$entries = $reader->getEntries("nonexistent");
+foreach ($entries as $entry) {
+  echo "$entry\n";
+}
+

+ 73 - 0
lib/Redis/Logger/Entry.php

@@ -0,0 +1,73 @@
+<?php
+/**
+ * @file
+ * Log Entry class.
+ */
+
+namespace Redis\Logger;
+
+/**
+ * Class Entry
+ *
+ * @package Redis\Logger
+ *
+ * @see hook_watchdog()
+ */
+class Entry {
+  /**
+   * @var string
+   */
+  public $type;
+
+  /**
+   * @var stdClass
+   *   A possibly incomplete Drupal user object.
+   */
+  public $user;
+
+  /**
+   * @var int
+   */
+  public $uid;
+
+  /**
+   * @var string
+   */
+  public $request_uri;
+
+  /**
+   * @var string
+   */
+  public $referer;
+
+  /**
+   * @var string
+   */
+  public $ip;
+
+  /**
+   * @var int
+   */
+  public $timestamp;
+
+  /**
+   * @var int
+   *   Use WATCHDOG_* constants.
+   */
+  public $severity;
+
+  /**
+   * @var string
+   */
+  public $link;
+
+  /**
+   * @var string
+   */
+  public $message;
+
+  /**
+   * @var string[]
+   */
+  public $variables;
+}

+ 204 - 0
lib/Redis/Logger/Reader.php

@@ -0,0 +1,204 @@
+<?php
+/**
+ * @file
+ * Contains the Redis\Logger\Reader class
+ */
+
+namespace Redis\Logger;
+
+
+/**
+ * This class reads the logger information stored in Redis:
+ *
+ * - messages templates
+ * - channels used in message templates
+ * - messages for a given template
+ *
+ * @package Redis\Logger
+ */
+class Reader {
+  /**
+   * @var \Redis
+   */
+  protected $redis;
+
+  /**
+   * @var \Redis\Logger\Settings
+   */
+  protected $settings;
+
+  /**
+   * @var string[]
+   */
+  protected $templateCache = NULL;
+
+  /**
+   * @param \Redis $redis
+   * @param \Redis\Logger\Settings $settings
+   *   We pass this constant instance in order to allow overriding it, say for
+   *   tests.
+   */
+  public function __construct(\Redis $redis, Settings $settings = NULL) {
+    $this->redis = $redis;
+    if (!isset($settings)) {
+      $settings = new Settings();
+    }
+    $this->settings = $settings;
+  }
+
+  /**
+   * Load and cache the list of templates in the instance for this page cycle.
+   *
+   * Note that the templates are in Redis storage format, including the prefix,
+   * to link directly to the results.
+   */
+  protected function ensureTemplateCache()  {
+    if (!isset($this->templateCache)) {
+      $this->templateCache = $this->scan($this->settings->getRedisPattern(), 0, $this->settings->getScanBatchSize());
+    }
+  }
+
+  /**
+   * Return the list of channels. Assumed to be constant for a given page cycle.
+   *
+   * @return string[int]
+   */
+  public function getChannels() {
+    $this->ensureTemplateCache();
+    $channels = array();
+    foreach ($this->templateCache as $key) {
+      preg_match($this->settings->getChannelRegex(), $key, $matches);
+      $channel = $matches[1];
+      $channels[$channel] = $channel;
+    }
+    ksort($channels);
+    return $channels;
+  }
+
+  /**
+   * @param $template
+   *   The Redis key for a template.
+   * @param int $skip
+   *   The number of pages to skip.
+   * @param int $entries_per_page
+   *   The maximum number of entries in a page.
+   */
+  public function getEntries($template, $skip = 0, $entries_per_page = 0) {
+    $entries_per_page = $this->settings->getEntriesPerPage($entries_per_page);
+
+    // Redis expect actual number of entries in the RANGE parameters.
+    $start = $skip * $entries_per_page;
+    $end = $start + $entries_per_page;
+    $entries = $this->redis->lRange($template, $start, $end);
+    return $entries;
+  }
+
+  /**
+   * @param mixed $criterium
+   *   Filter will always pass if criterium is not set, but be checked otherwise.
+   * @param string $regex
+   *   The regex against which to match to pass the filter.
+   * @param $string
+   *   The string to match against the regex.
+   *
+   * @return bool
+   */
+  protected function passesFilter($criterium, $regex, $string) {
+    if (!isset($criterium)) {
+      $ret = TRUE;
+    }
+    else {
+      $sts = preg_match($regex, $string, $matches);
+      $ret = $sts && ($matches[1] == $criterium);
+    }
+
+    return $ret;
+  }
+
+  /**
+   * @param null $channel
+   * @param int $severity
+   *   WATCHDOG_* : 0 to 7
+   * @param int $skip
+   *   Number of pages to skip.
+   * @param int $entries_per_page
+   *   Maximum number of entries per page.
+   */
+  public function getTemplates($channel = NULL, $severity = NULL, $skip = 0, $entries_per_page = 0) {
+    $this->ensureTemplateCache();
+    $matches = array();
+    $count = 0;
+    $channelRegex = $this->settings->getChannelRegex();
+    $severityRegex = $this->settings->getSeverityRegex();
+
+    $entries_per_page = $this->settings->getEntriesPerPage($entries_per_page);
+    $start = $skip * $entries_per_page;
+    $end = $start + $entries_per_page;
+
+    foreach ($this->templateCache as $template) {
+      if (!$this->passesFilter($channel, $channelRegex, $template)) {
+        continue;
+      }
+
+      if (!$this->passesFilter($severity, $severityRegex, $template)) {
+        continue;
+      }
+
+      $count++;
+      if ($count >= $skip) {
+        if ($count >= $end) {
+          break;
+        }
+        $matches[] = $template;
+      }
+    }
+
+    return $matches;
+  }
+
+  /**
+   * Perform a complete Redis SCAN series.
+   *
+   * @param string $pattern
+   *   The Redis SCAN MATCH optional argument.
+   * @param int $max
+   *   Maximum number of matches to be returned. Cursor is left dangling if $max
+   *   is lower than the maximum number of available results: only the minimum
+   *   number of SCAN iterations needed to reach $max results will be performed.
+   * @param int $batch_suggestion
+   *   The Redis SCAN COUNT optional argument.
+   *
+   * @return string[]
+   */
+  public function scan($pattern = '*', $max = 0, $batch_suggestion = 10) {
+    $redis = $this->redis;
+    $saved_scan_option = $redis->getOption(\Redis::OPT_SCAN);
+    $redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
+
+    $ret = array();
+    // Initialize our iterator to NULL.
+    $scan_it = NULL;
+
+    $count = 0;
+    $request_count = 1;
+    // Retry when we get no keys back.
+    while ($arr_keys = $redis->scan($scan_it, $pattern, $batch_suggestion)) {
+      $request_count++;
+      foreach ($arr_keys as $str_key) {
+        $ret[] = $str_key;
+        if ($max) {
+          $count++;
+          if ($count >= $max) {
+            break 2; // Break out of foreach + while.
+          }
+        }
+      }
+    }
+
+    if ($saved_scan_option != \Redis::SCAN_RETRY)  {
+      $redis->setOption(\Redis::OPT_SCAN, $saved_scan_option);
+    }
+
+    return $ret;
+  }
+}

+ 52 - 0
lib/Redis/Logger/Settings.php

@@ -0,0 +1,52 @@
+<?php
+/**
+ * @file
+ * Contains the Redis\Logger\Settings class.
+ */
+
+namespace Redis\Logger;
+
+/**
+ * Class Settings contains the default configuration for the Logger logic.
+ *
+ * @package Redis\Logger
+ */
+class Settings {
+  const PREFIX = "drupal:watchdog:";
+  const REDIS_PATTERN = 'drupal:watchdog:*:[01234567]:*';
+  const CHANNEL_REGEX = '/drupal:watchdog:([\w]+):[\d]:.*/';
+  const SEVERITY_REGEX = '/drupal:watchdog:[\w]+:([\d]):.*/';
+  const SCAN_BATCH_SIZE = 100;
+  const ENTRIES_PER_PAGE = 50;
+
+  /**
+   * @return string[int]
+   */
+  public static function Levels() {
+    return watchdog_severity_levels();
+  }
+
+  public static function getChannelRegex() {
+    return static::CHANNEL_REGEX;
+  }
+
+  public static function getEntriesPerPage($requested = 0) {
+    return $requested ?: static::ENTRIES_PER_PAGE;
+  }
+
+  public static function getPrefix() {
+    return static::PREFIX;
+  }
+
+  public static function getRedisPattern() {
+    return static::REDIS_PATTERN;
+  }
+
+  public static function getScanBatchSize() {
+    return static::SCAN_BATCH_SIZE;
+  }
+
+  public static function getSeverityRegex() {
+    return static::SEVERITY_REGEX;
+  }
+}

+ 112 - 0
lib/Redis/Logger/Writer.php

@@ -0,0 +1,112 @@
+<?php
+/**
+ * @file
+ * Contains the Logger model.
+ */
+
+namespace Redis\Logger;
+
+
+class Writer {
+
+  const PREFIX = 'drupal:logger';
+
+  /**
+   * @var \Redis
+   *   Assumes use of Predis, not PhpRedis
+   */
+  protected $redis;
+
+  /**
+   * @var \Redis\Logger\Writer
+   */
+  protected static $instance;
+
+  /**
+   * TODO implement
+   *
+   * @var int
+   *   Maximum size of per-template event list. Non-empty() values enable
+   *   trimming on insert, so have a tiny performance impact: two O(1) commands
+   *   instead of one.
+   */
+  protected $list_limit = NULL;
+
+  /**
+   * @var bool
+   *   - true: messages are stored during the page cycle and sent on shutdown.
+   *   - false: message are sent immediately
+   */
+  protected $deferred = FALSE;
+
+  /**
+   * @var \Redis\Logger\Entry[]
+   */
+  protected $entries = array();
+
+  /**
+   * Constructor.
+   *
+   * @param \Redis $redis
+   */
+  protected function __construct(\Redis $redis) {
+    $this->redis = $redis;
+  }
+
+  /**
+   * Public method to build a Logger from the singleton Redis client.
+   *
+   * @return static
+   */
+  public static function createFromGlobals() {
+    $redis = \Redis_Client::getClient();
+    assert('$redis instanceof \Redis');
+    return new static($redis);
+  }
+
+  /**
+   * @return string[]
+   */
+  public function getChannels() {
+    $templates = $this->getRawTemplates();
+  }
+
+  /**
+   * @return int[]
+   */
+  public function getRawTemplates($minimum_level = WATCHDOG_DEBUG, $channel = '*') {
+    $this->redis->getKeys(static::PREFIX . ":*:$channel:*");
+  }
+
+  /**
+   * Logger singleton accessor.
+   *
+   * @return \Redis\Logger\Writer
+   */
+  public static function instance() {
+    if (!isset(static::$instance)) {
+      static::$instance = static::createFromGlobals();
+    }
+
+    return static::$instance;
+  }
+
+  /**
+   * @param \Redis\Logger\Entry $entry
+   */
+  protected function post(Entry $entry) {
+
+  }
+
+  /**
+   * @param \Redis\Logger\Entry $entry
+   */
+  public function log(Entry $entry) {
+    if ($this->deferred) {
+      $this->entries[] = $entry;
+    }
+    else {
+      $this->post($entry);
+    }
+  }
+}

+ 25 - 0
redis_logger.admin.inc

@@ -0,0 +1,25 @@
+<?php
+use Redis\Logger\Writer;
+
+/**
+ * @file
+ * Administrative controllers for the Redis Logger module.
+ */
+
+function redis_logger_form_overview($form, $form_state) {
+  $logger = Redis\Logger\Writer::instance();
+
+  $form['levels'] = array(
+    '#description' => t('The semantics of these levels are defined by RFC5424.'),
+    '#options' => watchdog_severity_levels(),
+    '#title' => t('Severity'),
+    '#type' => 'select',
+  );
+  $form['channels'] = array(
+    '#description' => t('The channels listed here are those which have emitted at least one message.'),
+    '#options' => Writer::instance()->getChannels(),
+    '#title' => t('Severity'),
+    '#type' => 'select',
+  );
+  return $form;
+}

+ 9 - 0
redis_logger.info

@@ -0,0 +1,9 @@
+name = Redis logger
+description = An alternative Logger (watchdog) implementation.
+php = 5.4
+core = 7.x
+package = Performance and scalability
+
+dependencies[] = xautoload
+
+configure = admin/reports/redis/logger

+ 48 - 0
redis_logger.module

@@ -0,0 +1,48 @@
+<?php
+/**
+ * @file
+ * An alternate Drupal logger using Redis.
+ *
+ * Storage model and logic differ from http://drupal.org/project/redis_watchdog
+ */
+use Redis\Logger\Writer;
+
+/**
+ * Implements hook_xautoload().
+ */
+function redis_logger_xautoload($api) {
+  $api->absolute()->addPsr4('Redis\Logger\\', __DIR__ . '/lib/Redis/Logger');
+}
+
+/**
+ * Implements hook_menu().
+ */
+function redis_logger_menu() {
+  $items = array();
+  $items['admin/reports/redis/logger'] = array(
+    'title' => 'Recent log messages in Redis',
+    'description' => 'View events that have recently been logged.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('redis_logger_form_overview'),
+    'access arguments' => array('access site reports'),
+    'weight' => -1,
+    'file' => 'redis_logger.admin.inc',
+  );
+
+  $items['admin/reports/redis/logger/event/%'] = array(
+    'title' => 'Details',
+    'page callback' => 'redis_logger_page_event',
+    'page arguments' => array(5),
+    'access arguments' => array('access site reports'),
+    'file' => 'redis_logger.admin.inc',
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_watchdog().
+ */
+function redis_logger_watchdog(array $log_entry) {
+  dsm($log_entry);
+}