0

I have a bit of a dilemma as I need to come up with a good logger that logs what is happening in the app at the same time if there is a Log::error called, it should also notify Devs and Sys admin via slack. It is currently working, but it adds an overhead to the request-response time.

Below is my setting:

//config/logging.php
    'default' => env('LOG_CHANNEL', 'stack'),
    //truncated
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['daily', 'slack'],
        ],

        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 0,
        ],

        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'App',
            'emoji' => ':boom:',
            'level' => 'error',
        ]
    ]
    //truncated

//UserController
public function show(User $user)
{
    //just a sample code, the important part is where the Log facade is called
    try {
        //business logic
    } catch (Exception $e) {
        Log::error(get_class(), [
            'user_id' => $user->id,
            'message' => $e->getMessage()
        ]);
    }  

    return view('user.show', compact($user));
}

It is already working, but for sure we can still improve this to reduce the overhead somehow even though the added time for code above is negligible, but the real code is more complex and has quite a lot of iteration

How can I alter modify the behavior of the 'slack' logger to push it into a queue when it is triggered? I prefer to code it once and forget it rather than remembering that I have to push it to an on-demand logger such as

Log::chanel(['daily', 'slack'])->...

OR

//this is good for more on particular event notification but not not error notification which can happen anywhere
Notification::route('slack', env('LOG_SLACK_WEBHOOK_URL'))->notify(new AlertDevInSlackNotification)`

Note:

  • I tried adding some code into bootstrap/app.php but it is not working
//bootstrap/app.php
$app->configureMonologUsing(function($monolog) use ($app) {
    //some code here. does not work, just getting page not working
});
  • It is like when I call this log level and this channel, I want it to be queued
rfpdl
  • 956
  • 1
  • 11
  • 35

2 Answers2

0

You can do like this.

1.Create Job ex: name as LogSlackQueue.php

public class LogSlackQueue implements ShouldQueue {
     ...
     ...
     public function handle() {
          Log::channel(['daily', 'slack'])->info($your_input);
     }
}

2.Then use as

LogSlackQueue::dispatch($your_input)

If you dont want to do like above, you need to figure it out to make custom provider

ZeroOne
  • 8,996
  • 4
  • 27
  • 45
  • I did mention that that is doable and it will work, just that with the existing code, I just want to **easily** call `Log::error` and it will push the slack notification to queue automatically – rfpdl Feb 11 '20 at 07:55
  • 1
    custom service provider is needed then. currently laravel dont provide queue for log yet – ZeroOne Feb 11 '20 at 08:24
  • Any chance you know how to do that on custom service provider? Can you show me? – rfpdl Feb 11 '20 at 09:47
  • you may refer here https://github.com/hmazter/laravel-log-queue – ZeroOne Feb 11 '20 at 10:29
0

Thanks to @ZeroOne for giving out idea on how to solve it. I wanted it automatic and any existing code having Log::error() will automatically prompt the devs.

Below is my solution.

//CustomSlackServiceProvider.php
try {
    //listen to all events
    Event::listen('*', function($event, $details) {
        //check if the event message logged event which is triggered when we call Log::<level>
        if($event == "Illuminate\Log\Events\MessageLogged") {
            //$details contain all the information we need and it comes in array of object
            foreach($details as $detail) {
                //check if the log level is from error to emergency
                if(in_array($detail->level, ['emergency', 'critical', 'alert', 'error'])) {
                    //trigger the notification
                    Notification::route('slack', env('LOG_SLACK_WEBHOOK_URL'))->notify(new AlertDevInSlackNotification($detail->message, $detail->level, $detail->context));
                }
            }
        }
    });
} catch (Exception $e) {

}

//AlertDevInSlackNotification.php
class AlertDevInSlackNotification extends Notification implements ShouldQueue
{
    use Queueable;

    private $class;
    private $level;
    private $context;

    public function __construct($class, $level, $context)
    {
        $this->class = $class;
        $this->level = strtoupper($level);
        $this->context = $context;

        //prevent congestion in primary queue - make sure this queue exists
        $this->queue = 'alert';
    }

    public function via($notifiable)
    {
        return ['slack'];
    }

    public function toSlack($notifiable)
    {
        return (new SlackMessage)
            ->content($this->level.': '.$this->class)
            ->attachment(function($attachment) {
                $attachment->fields($this->context);
            });
    }

UPDATE:

The code above will work when you trigger Log::error().

But to listen to an event that is being thrown by an error such as syntax error which will cause "Serialization of 'Closure' is not allowed". You can do this instead to improve coverage:

    public function boot()
    {
        try {
            //listen to all events
            Event::listen('*', function($event, $details) {
                //check if the event message logged event which is triggered when we call Log::<level>
                if($event == "Illuminate\Log\Events\MessageLogged") {
                    // dump($event);
                    //$details contain all the information we need and it comes in array of object
                    foreach($details as $detail) {
                        $this->path = '';
                        $this->level = '';
                        $this->context = [];
                        $this->message = '';

                        //check if the log level is from error to emergency
                        if(in_array($detail->level, ['emergency', 'critical', 'alert', 'error'])) {
                            //@todo - exclude: Error while reading line from the server. [tcp://cache:6379] = restart

                            //check if the context has exception and is an instance of exception
                            //This is to prevent: "Serialization of 'Closure' is not allowed" which prevents jobs from being pushed to the queue
                            if(isset($detail->context['exception'])) {
                                if($detail->context['exception'] instanceof Exception) {

                                    $this->level = $detail->level;
                                    //to keep consistency on all the log message, putting the filename as the header
                                    $this->message = $detail->context['exception']->getFile();
                                    $this->context['user'] = auth()->check() ? auth()->user()->id.' - '. auth()->user()->first_name.' '.auth()->user()->last_name : null;
                                    $this->context['message'] = $detail->context['exception']->getMessage();
                                    $this->context['line'] = $detail->context['exception']->getLine();
                                    $this->context['path'] = request()->path();

                                    $this->runNotification();
                                    continue;
                                }
                            }

                            $this->level = $detail->level;
                            $this->context = $detail->context;
                            $this->message = $detail->message;

                            $this->runNotification();
                            continue;
                        }
                    }
                }
            });
        } catch (Exception $e) {

        }
    }

    public function runNotification()
    {
        Notification::route('slack', env('LOG_SLACK_WEBHOOK_URL'))->notify(new AlertDevInSlackNotification($this->message, $this->level, $this->context));
    }
rfpdl
  • 956
  • 1
  • 11
  • 35