0

I want that all errors that are logged with Monolog in my Laravel 9 application are sent to a predefined email address. Using SwiftMailer with Monolog has been deprecated (see this PR). Instead, Symfony Mailer should be used.

How do I use Monolog with Symfony Mailer on Laravel 9?

askuri
  • 345
  • 3
  • 10

3 Answers3

1

Laravel doesn't support Monolog with Symfony Mailer natively as of June 2022. However, using the monolog driver in the logging config allows to configure this manually.

First, you need to add symfony/mailer as a dependency: composer require symfony/mailer

In logging.php, you can create a new channel that looks like this:

    'mail' => [ // create a channel with the identifier `mail`
        'driver' => 'monolog', // use this driver to use Handlers that are not included in Laravel
        'handler' => Monolog\Handler\SymfonyMailerHandler::class,
        'level' => 'error',
        'with' => [ // initialize the Handler with these options
            // Configure the Mailer to use SMTP and provide it with the credentials for the SMTP server.
            // You may also use other protocols. For more info: https://symfony.com/doc/current/mailer.html#transport-setup
            // In this case, I build the DSN from the environment variables which Laravel includes by default.
            'mailer' => new Symfony\Component\Mailer\Mailer(Symfony\Component\Mailer\Transport::fromDsn(
                'smtp://'.urlencode(env('MAIL_USERNAME')).':'.urlencode(env('MAIL_PASSWORD')).'@'.env('MAIL_HOST').':'.env('MAIL_PORT'))),
            'email' => fn ($content, $records) => (new Email())
                ->subject('error occurred')
                ->from('from@example.com')
                ->to('to@example.com'),
        ],
    ],

Note that we create the Mailer manually, although Laravel should be doing the same internally somewhere. However, while the config is read, the Mailer is not yet instantiated. I believe it doesn't get any prettier until Laravel supports this natively.

Now you can configure Laravel to use the mail log channel. For example, you could set the default log channel to mail in logging.php.

When tinkering with the mail log channel, make sure to disable the mail channel during tests. I disabled logging completely in phpunit.xml:

    <server name="LOG_CHANNEL" value="'null'"/>
askuri
  • 345
  • 3
  • 10
  • Thanks for your answer, check out my improvements below. Let me know what you think – Michael Christensen Jan 04 '23 at 03:01
  • Good improvement @MichaelChristensen, thank you. I agree it's cleaner to create a separate handler for this purpose. I haven't tested it myself though, so I'll leave my answer here. – askuri Jan 04 '23 at 18:12
1

The answer of @askuri is good enough to make it works. Thank you!

If someone reading this wants to send rich content with the email, tries the following implementation. Basically uses the SymfonyMailerHandler and a Symfony\Component\Mime\Email built from and configured with html content and sender/to attributes. (here are the docs)

config/logging.php

        'alert-smtp' => [
            'driver' => 'monolog',
            'handler' => Monolog\Handler\SymfonyMailerHandler::class,
            'level' => 'emergency',
            'with' => [
                'mailer' => new Symfony\Component\Mailer\Mailer(Symfony\Component\Mailer\Transport::fromDsn(
                    'smtp://' . urlencode(env('APPLICATION_MAIL_USERNAME')) . ':' . urlencode(env('APPLICATION_MAIL_PASSWORD')) . '@' . env('APPLICATION_MAIL_HOST') . ':' . env('APPLICATION_MAIL_PORT'))),
                'email' => fn ($content, $records) => (new \App\Mail\ErrorAlert($records[0]['level_name'], $records[0]['message']))->build(),
            ],
        ],

app/Mail/ErrorAlert.php

<?php

namespace App\Mail;

use Symfony\Component\Mime\Email;

class ErrorAlert
{

    private string $content;
    private string $levelName;

    /**
     * Create a new Email instance.
     *
     * @return void
     */
    public function __construct(string $levelName, string $content)
    {
        $this->levelName = $levelName;
        $this->content = $content;
    }

    /**
     * Build the Email.
     *
     * @return Email
     */
    public function build(): Email
    {
        return (new Email())
            ->subject(trans('error-alert.subject', ['levelName' => $this->levelName,'app_name'=>config('app.name')]))
            ->html(view('emails.error-alert', [
                'levelName' => $this->levelName,
                'content' => $this->content,
            ])->render())
            ->from(config('mail.username'))
            ->to(config('a_custom_config.recipient'));
    }
}

Hope it helps!

se09deluca
  • 61
  • 7
1

I want to thank @askuri for his answer above. In terms of making it all cleaner, I think this is my best attempt.

MailLogHandler.php

use Monolog\Logger;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Monolog\Handler\SymfonyMailerHandler;

class MailLogHandler extends SymfonyMailerHandler
{
    public function __construct($to = ['admin@example.com'], $subject = '[Log Error] - Emergent Situation', $from = ['emergency-logger@example.com'], $level = Logger::ERROR, bool $bubble = true)
    {
        $mailer = new Mailer(Transport::fromDsn(
            'smtp://' . urlencode(config('mail.mailers.smtp.username')) . ':' .
            urlencode(config('mail.mailers.smtp.password')) . '@' . 
config('mail.mailers.smtp.host') .
            ':' . config('mail.mailers.smtp.port')
        ));

        $email = (new Email())
            ->subject($subject)
            ->from(...$from)
            ->to(...$to);

        parent::__construct($mailer, $email, $level, $bubble);
    }
}

This class extends SymfonyMailerhandler and provides all of the same utility as in @askuri's answer. However, this relies on values taken from the mail.php config file rather than direct env() calls.

logging.php

...
'mail' => [
    'driver'  => 'monolog',
    'handler' => MailLogHandler::class,
    'level'   => 'emergency',
    'with' => [
        'to' => [
            'admin1@example.com',
            'admin2@example.com',
        ],
    ],
],
...

Above, you can see a much cleaner logging.php config file. This has the advantage of being cacheable under artisan config:cache. Additionally, you can configure it ('with' => []) to a specific set of users to send to, the subject line, from, etc.