Sunday, November 26, 2017

Buffered logger in Drupal 8

Let's imagine you want to send log records from your Drupal 8 site to your email box, 3rd party service or to some other destination in order to know about warnings, errors etc. Most probably you don't want to send an email each time when some action happens as you don't want to decrease page performance. So, in this case, you should write a "buffered logger" which will keep all log entries in a buffer and send them only when it's overflown or on shutdown function. So let's write it.

Define a Drupal logger


Let's say we have a custom module called logger_example, so create a directory src/Logger and put there a file BufferLogger.php (you can choose any name you want, it's just an example) with next content:
<?php

namespace Drupal\logger_example\Logger;

use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;

/**
 * Class BufferLogger
 *
 * @package Drupal\logger_example\Logger
 */
class BufferLogger implements LoggerInterface {

  use LoggerTrait;

  /**
   * @var array
   */
  private $buffer;

  /**
   * @var int
   */
  private $bufferLimit;

  /**
   * BufferLogger constructor.
   */
  public function __construct($bufferLimit = 10) {
    $this->buffer = [];
    $this->bufferLimit = $bufferLimit;

    drupal_register_shutdown_function([$this, 'flush']);
  }

  /**
   * Logs with an arbitrary level.
   *
   * @param mixed $level
   * @param string $message
   * @param array $context
   *
   * @return void
   */
  public function log($level, $message, array $context = []) {
    $this->buffer[] = [
      'level' => $level,
      'message' => $message,
      'context' => $context,
    ];

    // Flush buffer when it's full.
    if (count($this->buffer) == $this->bufferLimit) {
      $this->flush();
    }
  }

  /**
   * Log messages into needed destination.
   */
  public function flush() {
    // It's not "full buffer" case.
    // If buffer is empty it means it's usual call of shutdown function
    // and there is nothing to log.
    if (empty($this->buffer)) {
      return;
    }

    // TODO: Log buffered log entries here.

    // Reset the buffer.
    $this->buffer = [];
  }

}

Let's find out how does it work. First of all, it has $buffer and $bufferLimit properties. First one is an array which will contain all buffered records and the second one is a setting which means "how many log records do you want to keep before sending?".

In a constructor, we register a public method called flush as a PHP shutdown function which will actually send buffered records to the needed destination. We need this shutdown function to handle the case when the buffer isn't full but it isn't empty as well and we want to send those buffered records anyway.

The BufferLogger::log() method saves records in an array and flushes them if needed. So we do not send any log entries each time when log() is called.

The last method is BufferLogger::flush(). It does two things: sends buffered records into needed destinations and resets the buffer array. All sending logic must be implemented in this method.

Tell the Drupal about your logger


Define a service inside your logger_example.services.yml file and tag it with logger name:
services:
  ...
  logger.logger_example:
    class: Drupal\logger_example\Logger\BufferLogger
    arguments: [100]
    tags:
      - { name: logger }
  ...
You can set up buffer size as big/small as you wish. For example, you want to send logs only if the buffer is 100 messages full (or anyway it will flush the buffer on a shutdown). Rebuild Drupal cache and after that, Drupal will log messages through all defined loggers including yours and all buffered records will be sent to the needed destination.

That's all, now you have a simple buffered logger. Of course, it can be improved. For example, you could implement a filter for channels or/and log levels you want to listen to. It's useful when you need to be notified only, let's say, about errors from "system" channel. It's all up to you.

1 comment: