Log - Target - Email and recurring emails with the same error

I have log - target:

'error' => [
    'class' => 'yii\log\EmailTarget',
    'levels' => ['error', 'warning'],
    'except' => ['yii\web\HttpException:404'],
    'message' => [
        'from' => ['XXXXXXX'],
        'to' => ['XXXXXXX'],
        'subject' => 'XXXXXX - error',
    ],
],

It will send me an email whenever an application error/warning occurs. It’s OK.

The problem is a “long duration” error (for example database down, but it could be anything else).
It sends an email for every visit the website.
With high traffic = hundreds of emails with the same error during one minute.

How prevent recurring emails with the same error?

For production it’s much better to use something like https://sentry.io/welcome/ (they have OpenSource version) or https://rollbar.com/. They’s both recording each error instance with full context, getting you a list of the most repeating errors and sending summary emails.

I prefer a simple solution over implementing another error management system

Simple solution is to take existing solution but if you need to re-invent your own then:

  1. You need to generalize error i.e. strip parameters out of it.
  2. You need to store hash of this stripped down error when sending email.
  3. When deciding if next one should be sent, search for its hash in database. If it’s there then do not send an email.

This works for me now (email every 30 seconds max):

<?php

namespace app\base\log;

use yii\helpers\VarDumper;
use yii\log\Logger;
use yii\log\LogRuntimeException;

class EmailTarget extends \yii\log\EmailTarget
{
    public function formatMessage($message)
    {
        list($text, $level, $category, $timestamp) = $message;
        $level = Logger::getLevelName($level);
        if (!is_string($text)) {
            // exceptions may not be serializable if in the call stack somewhere is a Closure
            if ($text instanceof \Throwable || $text instanceof \Exception) {
                $text = "\n--------------------------\n".$text->getMessage().' in '.$text->getFile().':'.$text->getLine()."\n--------------------------\n";
            } else {
                $text = VarDumper::export($text);
            }
        }

        $prefix = $this->getMessagePrefix($message);
        return "[$category] $text";
    }

    public function export()
    {
        $lockFile = sys_get_temp_dir().'/email_error.lock';
        $lastMessage = @file_get_contents($lockFile);

        if (empty($this->message['subject'])) {
            $this->message['subject'] = 'Application Log';
        }
        $messages = array_map([$this, 'formatMessage'], $this->messages);

        if($messages[0] === $lastMessage && @filemtime($lockFile) > strtotime('-30 seconds')){ return; }
        file_put_contents($lockFile, $messages[0], LOCK_EX);

        $body = wordwrap(implode("\n", $messages), 70);
        $message = $this->composeMessage($body);
        if (!$message->send($this->mailer)) {
            throw new LogRuntimeException('Unable to export log through email!');
        }
    }
}