You are here

new_relic_rpm.module in New Relic 7

Drupal module implementing New Relic.

File

new_relic_rpm.module
View source
<?php

/**
 * @file
 * Drupal module implementing New Relic.
 */

/**
 * Implements hook_help().
 *
 * @param $path
 * @param $arg
 * @return string
 */
function new_relic_rpm_help($path, $arg) {
  switch ($path) {
    case 'admin/help#new_relic_rpm':
      $output = '<p>' . t("This module's purpose is to enhance integration between New Relic and Drupal to enable more visibility into your website performance. It provides useful advanced settings, reports, and monitoring of your New Relic scores from within the Drupal website.") . '</p>';
      $output .= '<p>' . t('See the <a href="@project_page">project page on Drupal.org</a> for more details.', array(
        '@project_page' => 'https://www.drupal.org/project/new_relic_rpm',
      )) . '</p>';
      return $output;
  }
}

/**
 * Implements hook_menu().
 */
function new_relic_rpm_menu() {
  $items['admin/content/new-relic-rpm'] = array(
    'title' => 'New Relic deployments',
    'description' => 'Mark a new deployment for this site.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'new_relic_rpm_deploy',
    ),
    'access arguments' => array(
      'create new relic rpm deployments',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'new_relic_rpm.settings.inc',
  );
  $items['admin/config/development/new-relic-rpm'] = array(
    'title' => 'New Relic settings',
    'description' => 'Alter settings and manage your New Relic Integration.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'new_relic_rpm_settings',
    ),
    'access arguments' => array(
      'administer new relic rpm',
    ),
    'file' => 'new_relic_rpm.settings.inc',
  );
  $items['admin/reports/new-relic-rpm'] = array(
    'title' => 'New Relic reports',
    'description' => 'View New Relic reports and statistics.',
    'page callback' => 'new_relic_rpm_reporting',
    'access arguments' => array(
      'view new relic rpm reports',
    ),
    'file' => 'new_relic_rpm.reports.inc',
  );
  $items['admin/reports/new-relic-rpm/details/%'] = array(
    'title' => 'Application Details',
    'description' => 'Get details for a specific application.',
    'page callback' => 'new_relic_rpm_reporting_details',
    'page arguments' => array(
      4,
      5,
    ),
    'access arguments' => array(
      'view new relic rpm reports',
    ),
    'file' => 'new_relic_rpm.reports.inc',
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function new_relic_rpm_permission() {
  return array(
    'administer new relic rpm' => array(
      'title' => t('Administer new relic rpm'),
    ),
    'view new relic rpm reports' => array(
      'title' => t('View new relic rpm reports'),
    ),
    'create new relic rpm deployments' => array(
      'title' => t('Create new relic rpm deployments'),
    ),
  );
}

/**
 * Implements hook_cron().
 *
 * This is used to set cron tasks to be not tracked by New Relic if so desired.
 */
function new_relic_rpm_cron() {
  if (new_relic_rpm_extension_installed()) {
    $cron_tracking = variable_get('new_relic_rpm_track_cron', 'norm');
    new_relic_rpm_set_job_state($cron_tracking);

    // When using menu items as transaction names, we need to make sure that
    // cron runs still show up as cron.php. Otherwise, the cron runs default to
    // the menu item of the home page.
    if (variable_get('new_relic_rpm_use_menu_item_as_transaction', FALSE)) {
      newrelic_name_transaction('cron.php');
    }
  }
}

/**
 * Implements hook_flush_caches().
 *
 * Detect when cache tables are cleared.
 */
function new_relic_rpm_flush_caches() {
  if (extension_loaded('newrelic')) {
    _new_relic_rpm_deploy('Drupal Cache Flush', 'Drupal Cache Flush: all', 'All caches cleared.');
  }
}

/**
 * Implements hook_init().
 *
 * Sets New Relic error and exceptions handler and handles transaction naming.
 */
function new_relic_rpm_init() {
  if (extension_loaded('newrelic')) {
    if (variable_get('new_relic_rpm_disable_autorum', FALSE)) {
      newrelic_disable_autorum();
    }
    if (variable_get('new_relic_rpm_override_exception_handler', FALSE)) {
      set_exception_handler('_new_relic_rpm_exception_handler');
    }
    if (variable_get('new_relic_rpm_use_menu_item_as_transaction', FALSE)) {
      $menu_item = menu_get_item();
      if (!empty($menu_item['path'])) {
        $name = $menu_item['path'];
        if (variable_get('new_relic_rpm_add_node_type_to_node_page_paths', FALSE)) {
          $parts = explode('/', $name);
          if (isset($parts[0]) && $parts[0] === 'node' && isset($parts[1]) && $parts[1] === '%') {
            $node = menu_get_object();
            if (!empty($node->type)) {
              $parts[1] .= $node->type;
              $name = implode('/', $parts);

              // Node Type
              newrelic_add_custom_parameter('type', $node->type);
            }

            // Node Op (operation)
            if (isset($parts[2])) {
              newrelic_add_custom_parameter('node_operation', check_plain($parts[2]));
            }

            // Node ID
            if (!empty($node->nid)) {
              newrelic_add_custom_parameter('nid', $node->nid);
            }

            // Revision ID
            if (!empty($node->vid)) {
              newrelic_add_custom_parameter('vid', $node->vid);
            }
          }
        }
        newrelic_name_transaction($name);
      }
    }

    // This cannot be in hook_boot because we need user_has_role().
    $ignored_roles = variable_get('new_relic_rpm_ignored_roles', array());
    if (!empty($ignored_roles)) {
      foreach ($ignored_roles as $role_id) {
        if (user_has_role($role_id)) {
          new_relic_rpm_set_job_state('ignore');
          return;
        }
      }
    }
  }
}

/**
 * Provides custom PHP exception handling.
 *
 * Will invoke newrelic_notice_error() to forward the exception to New Relic.
 *
 * @param object $exception
 *   The exception object that was thrown.
 */
function _new_relic_rpm_exception_handler($exception) {
  require_once DRUPAL_ROOT . '/includes/errors.inc';
  try {

    // Forward the exception to New Relic.
    newrelic_notice_error(NULL, $exception);

    // Set flag to prevent duplicate logging by watchdog.
    $arr_error = _drupal_decode_exception($exception);
    $arr_error['new_relic_already_logged'] = TRUE;

    // Log the message to the watchdog and return an error page to the user.
    _drupal_log_error($arr_error, TRUE);
  } catch (Exception $exception2) {

    // Another uncaught exception was thrown while handling the first one.
    // If we are displaying errors, then do so with no possibility of a further
    // uncaught exception being thrown.
    if (error_displayable()) {
      print '<h1>Additional uncaught exception thrown while handling exception.</h1>';
      print '<h2>Original</h2><p>' . _drupal_render_exception_safe($exception) . '</p>';
      print '<h2>Additional</h2><p>' . _drupal_render_exception_safe($exception2) . '</p><hr />';
    }
  }
}

/**
 * Implements hook_boot().
 *
 * This is used to set various New Relic settings by URL.
 */
function new_relic_rpm_boot() {
  $ignore_urls = variable_get('new_relic_rpm_ignore_urls', '');
  $bg_urls = variable_get('new_relic_rpm_bg_urls', '');
  $exclu_urls = variable_get('new_relic_rpm_exclusive_urls', '');
  $job_state = '';

  // Handles cases where this is getting called from the command line and q
  // isn't set.
  $path = isset($_GET['q']) ? $_GET['q'] : '';
  if (!empty($exclu_urls) && !new_relic_rpm_page_match($path, $exclu_urls)) {
    $job_state = 'ignore';
  }
  elseif (!empty($ignore_urls) && new_relic_rpm_page_match($path, $ignore_urls)) {
    $job_state = 'ignore';
  }
  if ($job_state !== 'ignore' && !empty($bg_urls) && new_relic_rpm_page_match($path, $bg_urls)) {
    $job_state = 'bg';
  }
  if ($job_state !== '') {
    new_relic_rpm_set_job_state($job_state);
  }
}

/**
 * Implements hook_watchdog().
 */
function new_relic_rpm_watchdog(array $log_entry) {
  global $base_url;

  // Don't do anything if the new relic extension is not available.
  if (!function_exists('newrelic_notice_error')) {
    return;
  }

  // Skip if already logged.
  if (!empty($log_entry['variables']['new_relic_already_logged'])) {
    return;
  }

  // Check if the severity is supposed to be logged.
  if (!in_array($log_entry['severity'], variable_get('new_relic_rpm_watchdog_severities', array()), true)) {
    return;
  }

  // Avoid marking when cron completed normally as an Notice Error
  if ($log_entry['type'] === 'cron' && $log_entry['severity'] === WATCHDOG_NOTICE && $log_entry['message'] === 'Cron run completed.') {
    return;
  }
  $severity_list = watchdog_severity_levels();
  $message = '@message | Severity: (@severity) @severity_desc | Type: @type | Request URI:  @request_uri | Referrer URI: @referer_uri | User: (@uid) @name | IP Address: @ip';
  $message = strtr($message, array(
    '@severity' => $log_entry['severity'],
    '@severity_desc' => $severity_list[$log_entry['severity']],
    '@type' => $log_entry['type'],
    '@ip' => $log_entry['ip'],
    '@request_uri' => $log_entry['request_uri'],
    '@referer_uri' => $log_entry['referer'],
    '@uid' => $log_entry['uid'],
    '@name' => format_username($log_entry['user']),
    '@message' => strip_tags(strtr($log_entry['message'], is_array($log_entry['variables']) ? $log_entry['variables'] : array())),
  ));
  newrelic_notice_error($message);
}

/**
 * Implements hook_preprocess_html().
 */
function new_relic_rpm_preprocess_html(&$variables) {

  // Support RUM monitoring of cached pages by adding the New Relic timing header & footer.
  if (variable_get('new_relic_rpm_add_manual_rum_instrumentation', FALSE) && new_relic_rpm_extension_installed()) {

    // drupal_add_html_head() works better than drupal_add_js because it can be weighted earlier in the DOM.
    // @see http://www.metaltoad.com/blog/new-relic-real-user-monitoring-hooks-drupal
    drupal_add_html_head(array(
      '#type' => 'html_tag',
      '#tag' => 'script',
      '#value' => newrelic_get_browser_timing_header(FALSE),
      '#weight' => -999,
    ), 'newrelic');
    drupal_add_js(newrelic_get_browser_timing_footer(FALSE), array(
      'type' => 'inline',
      'scope' => 'footer',
      'weight' => 1000,
    ));
  }
}

/**
 * Tells New Relic if a job should be ignored or counted as a background job.
 *
 * @param $setting
 */
function new_relic_rpm_set_job_state($setting) {
  if (!new_relic_rpm_extension_installed()) {
    return;
  }
  switch ($setting) {
    case 'bg':
      newrelic_background_job(TRUE);
      break;
    case 'ignore':
      newrelic_ignore_transaction();
      break;
  }
}

/**
 * Checks if the new_relic extension is installed.
 *
 * @return bool
 *   TRUE if the extension is installed, FALSE if not.
 */
function new_relic_rpm_extension_installed() {
  static $newrelic_module;
  if (!isset($newrelic_module)) {
    $newrelic_module = function_exists('newrelic_background_job');
  }

  // If newrelic module isn't loaded into php, error and return.
  if (!$newrelic_module) {

    // Only throw error if we've booted far enough to watchdog.
    // i.e. drush updatedb.
    if (function_exists('module_implements') && !variable_get('new_relic_rpm_suppress_module_not_enabled_error_always', FALSE) && (PHP_SAPI !== 'cli' || !variable_get('new_relic_rpm_suppress_module_not_enabled_error_on_cli', FALSE))) {
      watchdog('New Relic', 'New Relic module not enabled.', array(), WATCHDOG_ERROR);
    }
    return FALSE;
  }
  return TRUE;
}

/**
 * Implements hook_modules_enabled().
 */
function new_relic_rpm_modules_enabled($modules) {

  // Make the Deploy call to New Relic.
  if (!empty($modules)) {
    new_relic_rpm_module_deploy($modules, NULL);
  }
}

/**
 * Implements hook_modules_disabled().
 */
function new_relic_rpm_modules_disabled($modules) {

  // Make the Deploy call to New Relic.
  if (!empty($modules)) {
    new_relic_rpm_module_deploy(NULL, $modules);
  }
}

/**
 * Log module enable/disable actions as a deploy call to New Relic.
 *
 * @param $new_modules
 * @param $disable_modules
 */
function new_relic_rpm_module_deploy($new_modules, $disable_modules) {

  // If the API Key and/or App name is/are not set, there is no sense in
  // attempting to create deployments.
  if (variable_get('new_relic_rpm_module_deployment', FALSE) && ini_get('newrelic.appname') && variable_get('new_relic_rpm_api_key', '')) {
    $modules_installed = !empty($new_modules);
    $modules_removed = !empty($disable_modules);
    if ($modules_installed) {
      $m_inst = 'Modules Installed: ' . implode(', ', $new_modules) . "\n";
    }
    else {
      $m_inst = '';
    }
    if ($modules_removed) {
      $m_remv = 'Modules Removed: ' . implode(', ', $disable_modules) . "\n";
    }
    else {
      $m_remv = '';
    }
    _new_relic_rpm_deploy('Drupal Module Install/Uninstall', 'Drupal modules were installed: ' . ($modules_installed ? 'YES' : 'NO') . ' and uninstalled: ' . ($modules_removed ? 'YES' : 'NO'), $m_inst . $m_remv);
  }
}

/**
 * Send deployments to New Relic's web API.
 *
 * @param string $user
 *   (optional) User deploying changes.
 * @param string $description
 *   (optional) Description of the deployment.
 * @param string $changelog
 *   (optional) A list of changes for this deployment.
 * @param string $revision
 *   (optional) Revision id of the deployment.
 */
function _new_relic_rpm_deploy($user = NULL, $description = NULL, $changelog = NULL, $revision = NULL) {
  $post_vars['deployment[application_id]'] = ini_get('newrelic.appname');
  if (isset($user)) {
    $post_vars['deployment[user]'] = $user;
  }
  if (isset($description)) {
    $post_vars['deployment[description]'] = $description;
  }
  if (isset($changelog)) {
    $post_vars['deployment[changelog]'] = $changelog;
  }
  if (isset($revision)) {
    $post_vars['deployment[revision]'] = $revision;
  }
  $deployments = new_relic_rpm_curl('https://rpm.newrelic.com/deployments.xml', $post_vars);
  watchdog('New Relic', 'Action logged as deployment. Log data: <pre>%details</pre>', array(
    '%details' => $deployments,
  ), WATCHDOG_INFO);
}

/**
 * This is the generic cURL function all New Relic Web API calls go through.
 *
 * @param $url
 * @param array $post
 * @return bool|string
 */
function new_relic_rpm_curl($url, $post = array()) {
  $api_key = variable_get('new_relic_rpm_api_key', '');

  // Set the header with the API key.
  $headers[] = "x-api-key: {$api_key}";

  // Set up the cURL request.
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);

  // There's some notes about Windows not having the CA built-in
  if (stripos(PHP_OS, 'WIN') === 0) {
    $curl_ssl_verify = FALSE;
  }
  else {
    $curl_ssl_verify = TRUE;
  }
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $curl_ssl_verify);
  if (!empty($post)) {
    curl_setopt($ch, CURLOPT_POST, TRUE);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
  }
  curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  $return_data = curl_exec($ch);
  if (false !== stripos($return_data, 'Access denied')) {
    return FALSE;
  }
  return $return_data;
}

/**
 * Check if a path matches any pattern in a set of patterns.
 *
 * This is a direct copy of drupal_match_path without the static cache since we
 * don't need it. This is needed because we need our hook_boot to be able to
 * operate before path.inc has been bootstrapped.
 *
 * @param string $path
 *   The path to match.
 * @param string $patterns
 *   String containing a set of patterns separated by \n, \r or \r\n.
 *
 * @return int
 *   1 if there is a match, 0 if there is not a match.
 */
function new_relic_rpm_page_match($path, $patterns) {
  $regexp = '/^(' . preg_replace(array(
    '/(\\r\\n?|\\n)/',
    '/\\\\\\*/',
    '/(^|\\|)\\\\<front\\\\>($|\\|)/',
  ), array(
    '|',
    '.*',
    '\\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\\2',
  ), preg_quote($patterns, '/')) . ')$/';
  return preg_match($regexp, $path);
}

Functions

Namesort descending Description
new_relic_rpm_boot Implements hook_boot().
new_relic_rpm_cron Implements hook_cron().
new_relic_rpm_curl This is the generic cURL function all New Relic Web API calls go through.
new_relic_rpm_extension_installed Checks if the new_relic extension is installed.
new_relic_rpm_flush_caches Implements hook_flush_caches().
new_relic_rpm_help Implements hook_help().
new_relic_rpm_init Implements hook_init().
new_relic_rpm_menu Implements hook_menu().
new_relic_rpm_modules_disabled Implements hook_modules_disabled().
new_relic_rpm_modules_enabled Implements hook_modules_enabled().
new_relic_rpm_module_deploy Log module enable/disable actions as a deploy call to New Relic.
new_relic_rpm_page_match Check if a path matches any pattern in a set of patterns.
new_relic_rpm_permission Implements hook_permission().
new_relic_rpm_preprocess_html Implements hook_preprocess_html().
new_relic_rpm_set_job_state Tells New Relic if a job should be ignored or counted as a background job.
new_relic_rpm_watchdog Implements hook_watchdog().
_new_relic_rpm_deploy Send deployments to New Relic's web API.
_new_relic_rpm_exception_handler Provides custom PHP exception handling.