2

I'm developing a module intended to proactively warn the admin if there are problems with the site. The idea is to periodically check the system status report and sends an email notice if certain statuses have gone from REQUIREMENT_OK to REQUIREMENT_WARNING or REQUIREMENT_ERROR. I'm planning on firing this check in hook_cron().

But what if the problem with the site is that cron is broken? What is a safe and performant way for the site to periodically check if the 'cron_last' variable is too old, that doesn't rely on cron?

I would like to keep the module self-contained and avoid external dependencies or "phone home" solutions.

  • 2
    Interesting question...but given that PHP doesn't have the ability to periodically execute a script, it's hard to imagine how you could do this in a self-contained module. You'll need something to execute that script periodically, whether it's cron, ping service or something different. You could probably even find a PHP extension which adds that ability, but that would obviously count as a dependency – Clive Oct 26 '15 at 18:27
  • 2
    What @Clive said. This is PHP, you need something that triggers your script. There really is no other way. Btw, our drupal.org/project/monitoring project does this, among many other things. We explicitly rely on something external to call us so we're not depending on the core cron, we just provide services and API's to run our checks. – Berdir Oct 26 '15 at 20:16
  • 1
    Actually, Drupal core already does this check on system_admin_config_page(). Find a hook that suites your need, and add some checking to avoid calling system_check(TRUE) too often. – kiamlaluno Oct 26 '15 at 20:44
  • system_status(TRUE) just tells me if there is a status issue, but not the details. The output I'll generate is closer to system_status(FALSE), but filtered down, and formatted for email. I think hook_cron() does suit my need, with the important exception of checking whether cron itself has been running. Would a hook_init() call be a terrible thing if all it did was check 'cron_last'? – Les Lim Oct 26 '15 at 21:12
  • @Berdir drupal.org/project/monitoring is impressive. Munin and Nagios are off the table for the project I'm working on, but I might try building a simple email alert system on top of Monitoring instead. – Les Lim Oct 26 '15 at 21:40
3

What is a safe and performant way for the site to periodically check if the 'cron_last' variable is too old, that doesn't rely on cron?

As said before by @Clive, you a script running periodically to do this check for you. Having said so, the simplest way is to create a basic module that checks the last time Drupal Cron ran succesfuly and acts based on that. You could include a drush command so you can call it via a server cronjob. E.g.

Check if cron hasn't run in X minutes and send e-mail if so, using Drush

1) Create a file named cronwatch.drush.inc

2) Create the drush command that you will invoke via a crontab / automated task in your server. For instance, you would be running drush cronwatch every X minutes.

/**
 * Implements hook_drush_command().
 */
function cronwatch_drush_command() {
  $items = array();
  $items['cronwatch'] = array(
    'description' => dt('Check last time Drupal cron ran and send an email if necessary.'),
  );
  return $items;
}

3) Add a function to check if Drupal Cron hasn't run in longer than X minutes, send an alert email.

/**
 * Callback function for drush cronwatch command.
 */
function drush_cronwatch() {
  // Minutes since the last time cron run.
  $last_cron = variable_get('cron_last', 0);
  $minutes_ago = (time() - $last_cron) / 60;

  // How many minutes to allow without having run the cron.
  // This number should be larger than the frecuency of Drupal cron. E.g. If
  // Drupal cron is set to run every 60m, this number could be 70.
  $tolerance = 70;

  // If the cron last ran longer than tolerance minutes ago, it means that it
  // probably failed.
  if ($minutes_ago >= $tolerance) {
    $to = 'you@example.com';  // E-mail to get the alerts
    $message = t('Drupal cron could not run successfuly. Last succesful run was !minutes minutes ago.', array('!minutes' => floor($minutes_ago)));
    drush_mail_send_email($to, 'Drupal Cron failed', $message);
  }
}

4) Finally, this is the function to send an e-mail via drush:

/**
 * Send an e-mail to a specified e-mail address from within Drush.
 * @see http://drupal.org/files/mail.drush_.inc__0.txt
 */
function drush_mail_send_email($to, $subject = '', $body = '') {
  // Define $from and headers
  if (!$from = drush_get_option('from')) {
    // E-mail to send the alerts from. Replace here if desired.
    $from = ini_get('sendmail_from');
  }
  $headers = array();
  $headers['From'] = $headers['Sender'] = $headers['Return-Path'] = $headers['Errors-To'] = $from;

  // D7 implementation of drupal_mail
  if (function_exists('drupal_mail_system')) {
    // Prepare the message.
    $message = drupal_mail('drush', 'key', $to, NULL, array(), $from, FALSE);

    $message['subject'] = $subject;
    $message['body'] = array();
    $message['body'][] = $body;
    $message['headers'] = $headers;

    // Retrieve the responsible implementation for this message.
    $system = drupal_mail_system('drush', 'key');
    // Format the message body.
    $message = $system->format($message);
    // Send e-mail.
    $message['result'] = $system->mail($message);
    $result = $message['result'];

    // D6 implementation of drupal_mail_send
  }
  else {
    $message = array(
      'to' => $to,
      'subject' => $subject,
      'body' => $body,
      'headers' => $headers,
    );
    $result = drupal_mail_send($message);
  }

  // Return result.
  if ($result) {
    drush_log(dt('E-mail message sent to !to', array('!to' => $to)), 'ok');
  }
  else {
    drush_set_error('DRUSH_MAIL_ERROR', dt('An error occurred while sending the e-mail message.'));
    watchdog('cronwatch', 'An error occurred while sending the e-mail message with Drush.', array(), WATCHDOG_ERROR);
  }
}

And there you go. If Drupal Cron is set to run -let's say- every 60 min, and for some reason failed, the next time this script is executed, it will send you an alert e-mail and you can act based on that.

---- EDIT: Added code to check without depending on Drush / Server Cronjobs ----

Check if cron hasn't run in X minutes and send e-mail if so (no Drush / Server cronjobs)

You could also run the checks without Drush nor server cronjobs. For this, you will necessarily need to hook your checks to a certain event in your website (e.g. every page load).

In your cronwatch.module file:

1) In this case, we check on every page load, that is almost guaranteed to run periodically. However, you can use any other hook to trigger your checks.

Let's find out when the Drupal cron check was last executed.

/**
 * Implements hook_page_alter().
 *
 * Finds out when was the last time Drupal cron check was executed. If it's
 * been longer than the desired frequency, proceeds with a new check.
 *
 * We depend on page loads in order to emulate a 'periodically running' event,
 * as an alternative to a server cronjob / scheduled task.
 */
function cronwatch_page_alter() {
  // Defines how often we want to run the checks (in minutes).
  // We need this flag variable, since we don't want to make a check on
  // every single page load.
  $frequency = 60;

  // Only proceed if it's been longer than 60 minutes since the last check.
  $last_check = variable_get('cronwatch_last_check_timestamp', 0);
  $minutes_ago = (REQUEST_TIME - $last_check) / 60;

  if ($minutes_ago < $frequency) {
    return;
  }

  // More than 60 min have passed since the last check. So, let's proceed with
  // a new check.
  if (cronwatch_check()) {
    // Check was succesful. Now update the 'last check timestamp' variable so
    // the next check runs an hour from now.
    variable_set('cronwatch_last_check_timestamp', REQUEST_TIME);
  }
}

2) Make the actual check for the last time Drupal cron ran. If more than X minutes have passed, send an email.

/**
 * Checks the last time Drupal cron ran, and acts based on that.
 */
function cronwatch_check() {
  // Minutes since the last time cron run.
  $last_cron = variable_get('cron_last', 0);
  $minutes_ago = (time() - $last_cron) / 60;

  // How many minutes to allow without having run the cron.
  // This number should be larger than the frecuency of Drupal cron. E.g. If
  // Drupal cron is set to run every 60m, this number could be 70.
  $tolerance = 70;

  // If the cron last ran longer than tolerance minutes ago, it means that it
  // probably failed.
  if ($minutes_ago >= $tolerance) {
    $to = 'you@example.com';  // E-mail to get the alerts
    $message = t('Drupal cron could not run successfuly. Last succesful run was !minutes minutes ago.', array('!minutes' => floor($minutes_ago)));
    $mail_sent = cronwatch_mail_send_email($to, t('Drupal Cron failed'), $message);

    if ($mail_sent) {
      return TRUE;
    }
  }
  return FALSE;
}

3) Function to send an e-mail

/**
 * Sends an e-mail from the cronwatch module.
 */
function cronwatch_mail_send_email($to, $subject, $body) {
  // Let's get the mail ready to be sent.
  $module = 'cronwatch';
  $key = 'alert_mail';
  $from = variable_get('site_mail', ini_get('sendmail_from'));
  $language = language_default();

  // Build the mail, but don't send it yet.
  $message = drupal_mail($module, $key, $to, $language, array(), $from, FALSE);

  // Prepare the message.
  $message['subject'] = $subject;
  $message['body'][] = $body;

  // Retrieve the responsible implementation for this message.
  $system = drupal_mail_system($module, $key);

  // Format the message body.
  $message = $system->format($message);

  // Send e-mail (Drupal will log any error).
  $message['result'] = $system->mail($message);

  return $message['result'];
}

Only drawback is you depend on page loads, so accuracy would be affected by a website with very few or no visits (in that case, do you even need this?). And, of course, this code could be refactored, but this should do the trick.

  • This approach would certainly work, and extra credit for the exceptionally well-commented code. This does sidestep the need for external dependencies (not counting Drush), but for the moment I'm holding out for a solution that doesn't require additional setup or server access beyond enabling the module. When I hand the project off, I want the number of potential maintenance points to be at a minimum. – Les Lim Oct 27 '15 at 5:03
  • I see. I have updated my answer to include an option that works without Drush nor any server setup. Your site only needs to have visits ;) – typologist Oct 27 '15 at 20:09
  • Nice! I was considering something similar using hook_init(). I think depending on traffic I'll also want to acquire a lock on cronwatch_check(), but I can take it from here. – Les Lim Oct 28 '15 at 2:09
  • Awesome! Certainly, you could use hook_init() too, which is run only in non-cached pages, though. – typologist Oct 28 '15 at 14:27
0

This is a pretty heavy handed solution, but you could implement a hook in a module that runs this check for you. Try to find a page that isn't hit very frequently and run it whenever that page is loaded.

Another possibility would be to install the Services module then hit a url from a remote script that kicks off your module to do the check. This introduces a pretty big dependency as well (another server), but it might not be bad if you do have another server. You could have servers checking on each other across a number of projects (maybe I'm overthinking this...)

  • I want the module to be general-use, so I don't want to make judgments about which page is not hit frequently. Interesting thought, though. And Services is definitely too heavy for what I have in mind. – Les Lim Oct 26 '15 at 19:13
  • If it needs to be more general purpose, it could run anytime someone hits a node edit form, or the content admin page. You could expose it to rules and let the user select an event that triggers your module. If it isn't based on something intrinsic to the site itself, you are going to be faced with some kind of external dependency. – Quint Oct 26 '15 at 20:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.