You are here

function jsonlog_watchdog in JSONlog 7

Same name and namespace in other branches
  1. 7.2 jsonlog.module \jsonlog_watchdog()

Logs any watchdog call - at or above a certain severity threshold - as JSON to a custom log file.

Default severity threshold: warning.

Prefers server environment variables for Drupal conf variables. Any jsonlog conf variable may be overridden by a 'drupal_[jsonlog variable]' server environment var.

JSON fields (watchdog standard fields are not explained here):

  • @timestamp: ISO-8601 milliseconds timestamp instead of watchdog's native seconds timestamp
  • @version: (always) 1
  • message_id: jsonlog site ID + unique padding
  • site_id: (default) server's hostname + database name + database prefix (if any)
  • tags: comma-separated list; becomes array
  • type
  • severity
  • method: HTTP request method, or 'cli' (if drush)
  • request_uri
  • referer
  • uid
  • username: name of current user, or empty
  • client_ip: equivalent to watchdog standard 'ip' field
  • link
  • variables: watchdog standard, except null if empty
  • trunc: null if the log entry as a whole doesn't exceed the json_truncate setting; otherwise array of original length, truncated length

Implements hook_watchdog().

Parameters

array $log_entry:

File

./jsonlog.module, line 484
JSONlog module.

Code

function jsonlog_watchdog(array $log_entry) {
  static $_threshold, $_site_id, $_file, $_truncate, $_severity, $_tags;

  // Don't load more settings than threshold, in case current entry isn't sufficiently severe.
  if (!$_threshold) {

    // A threshold of zero (emergency) wouldnt make sense in the real world, so no reason to check by type (boolean).
    if (!($_threshold = getenv('drupal_jsonlog_severity_threshold'))) {
      $_threshold = variable_get('jsonlog_severity_threshold', WATCHDOG_WARNING);
    }
  }

  // Severity is upside down; less is more.
  if ($log_entry['severity'] > $_threshold) {
    return;
  }

  // Load the rest of the settings.
  if (!$_site_id) {

    // Site ID: Try server environment var before Drupal conf. var.
    if (!($_site_id = getenv('drupal_jsonlog_siteid'))) {
      if (!($_site_id = variable_get('jsonlog_siteid'))) {
        module_load_include('inc', 'jsonlog');
        variable_set('jsonlog_siteid', $_site_id = jsonlog_default_site_id());
      }
    }

    // File: Try server environment var before Drupal conf. var.
    if (!($_file = getenv('drupal_jsonlog_file'))) {
      if (!($_file = variable_get('jsonlog_file'))) {
        module_load_include('inc', 'jsonlog');
        if ($_file = jsonlog_default_file()) {
          variable_set('jsonlog_file', $_file);
        }
        else {
          error_log('Drupal jsonlog, site ID[' . $_site_id . '], failed to establish server\'s default log dir.');
        }
      }
    }

    // Truncation: Try server environment var before Drupal conf. var.
    if (($_truncate = getenv('drupal_jsonlog_truncate')) === FALSE) {
      $_truncate = variable_get('jsonlog_truncate', JSONLOG_TRUNCATE_DEFAULT);
    }
    if ($_truncate) {

      // Kb to bytes.
      $_truncate *= 1024;

      // Substract estimated max length of everything but message content.
      $_truncate -= 768;

      // Message will get longer when JSON encoded, because of hex encoding of <>&" chars.
      $_truncate *= 7 / 8;
    }
    $_severity = array(
      'emergency',
      'alert',
      'critical',
      'error',
      'warning',
      'notice',
      'info',
      'debug',
    );

    // Tags: append Drupal conf. var to server env. var.
    $tags_server = ($tags = getenv('drupal_jsonlog_tags')) !== FALSE ? $tags : '';
    $tags_site = variable_get('jsonlog_tags', '');
    if ($tags_server) {
      $tags = $tags_server;
      if ($tags_site) {
        $tags .= ',' . $tags_site;
      }
    }
    else {
      $tags = $tags_site;
    }
    if ($tags) {
      $_tags = explode(',', $tags);
    }
  }

  // Create the entry.
  $entry = new stdClass();

  // Strip tags if message starts with < (Inspect logs in tag).
  if (($message = $log_entry['message']) && $message[0] === '<') {
    $message = strip_tags($message);
  }

  // Escape newline, (drupal_)json_encode() doesn't escape control characters (Inspect uses newline).
  // Escape null control char.
  $message = str_replace(array(
    "\n",
    "\0",
  ), array(
    '\\n',
    '_NUL_',
  ), $message);

  // If truncation required, start by skipping variables.
  $variables = $log_entry['variables'];

  // Truncate message.
  if ($_truncate && ($le = strlen($message)) > $_truncate) {

    // Deliberately not drupal_strlen(); need 'physical' length, not (possibly shorter) multibyte length.
    // Flag variables truncated by setting it to false.
    if ($variables) {
      $variables = FALSE;
    }

    // Truncate multibyte safe until ASCII length is equal to/less than max. byte length.
    $truncation = array(
      $le,
      strlen($message = drupal_truncate_bytes($message, (int) $_truncate)),
    );
  }
  else {
    $truncation = NULL;
  }
  $entry->message = $message;
  unset($message);

  // Use a milliseconds timestamp instead of watchdog()'s seconds timestamp.
  $millis = round(microtime(TRUE) * 1000);
  $seconds = (int) floor($millis / 1000);
  $millis -= $seconds * 1000;
  $millis = str_pad($millis, 3, '0', STR_PAD_LEFT);
  $entry->{'@timestamp'} = substr(gmdate('c', $seconds), 0, 19) . '.' . $millis . 'Z';
  $entry->{'@version'} = 1;
  $entry->message_id = uniqid($_site_id, TRUE);
  $entry->site_id = $_site_id;
  $entry->tags = $_tags;
  $entry->type = $log_entry['type'];
  $entry->severity = $_severity[$log_entry['severity']];
  $entry->method = !empty($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'cli';
  $entry->request_uri = $log_entry['request_uri'];
  $entry->referer = $log_entry['referer'];
  $entry->uid = $uid = $log_entry['uid'];
  $entry->username = $uid && !empty($GLOBALS['user']->name) ? $GLOBALS['user']->name : '';
  $entry->client_ip = $log_entry['ip'];
  $entry->link = !$log_entry['link'] ? NULL : strip_tags($log_entry['link']);
  $entry->variables = $variables ? $variables : NULL;
  $entry->trunc = $truncation;

  // File append, using lock (write, doesn't prevent reading).
  // If failure: log filing error to web server's default log.
  if (!file_put_contents($_file, "\n" . drupal_json_encode($entry), FILE_APPEND | LOCK_EX)) {
    error_log('Drupal jsonlog, site ID[' . $_site_id . '], failed to write to file[' . $_file . '].');
  }
}