Thông báo lỗi realtime tới Slack trong dự án Laravel

425

I. Bài toán thực tế

Chuyện kể rằng anh Quang làm ở công ty nọ được giao nhiệm vụ quản lý một dự án từ lâu không đã không được maintain. Sau khi nhận nhiệm vụ, công việc đầu tiền là anh check file error log của dự án để xem tình hình bug biếc thế nào, thì anh thấy rằng file log đã nặng tới gần 10Gb, nội dung của file thì toàn những lỗi bị lặp đi lặp lại. Dẫu biết rằng code thì không thể không có lỗi, thế nhưng khi có lỗi thì cần phải được khắc phục ngay chứ, tại sao lại để ra nông nỗi này – anh Quang ngán ngẩm than thở, nhưng rồi cũng phải thông cảm vì dự án từ lâu đã không còn ai ngó ngàng tới chứ đừng nói tới việc check file log thường xuyên.

II. Tìm giải pháp

Để khắc phục tình trạng không nhận được exception kịp thời như vậy, anh liền tìm cách tích hợp cơ chế gửi thông báo realtime tới một channel Slack mỗi khi dự án có exception xảy ra (do slack cung cấp API để làm điều này). Nhìn lại, thì dự án đang tiếp nhận được xây dựng trên Laravel 5.5, và may mắn thay là Laravel 5.5 có tích hợp sẵn cơ chế gửi thông báo tới Slack – anh Quang sẽ bớt vất vả hơn phần nào.

Ý tưởng là anh Quang sẽ tạo ra một cặp event – listener lần lượt là HasExceptionEventHasExceptionListener. Mỗi khi có exception xảy ra, anh Quang cho phát ra sự kiện HasExceptionEvent để HasExceptionListener có thể xử lý, cụ thể là trong HasExceptionListener anh Quang sẽ gửi notification mô tả chi tiết về exception đó tới channel slack mà anh đã cấu hình từ trước.

III. Các bước thực hiện

3.1 Đăng ký một workspace trên Slack

Bước này mình không được anh Quang hướng dẫn, nên chi tiết về cách tạo Slack workspace bạn tham khảo tại chính trang chủ slack.com nhé. Chắc cũng không khó lắm.

3.2 Đăng ký sử dụng incoming webhooks của Slack

Slack có một khái niệm là Incoming Webhooks – theo như Slack đề cập thì nó là cách để bạn có thể gửi tin nhắn từ một ứng dụng bên thứ ba tới channel của Slack. Về cơ bản thì Incoming Webhooks giống như một API endpoint.

Để đăng ký sử dụng Incoming Webhooks, bạn cần phải tạo một Slack app bằng cách:

Bước 1: Truy cập vào https://api.slack.com/incoming-webhooks

Bước 2: Tìm nút “Create your Slack app” và nhấn vào

Bước 3: Điền các thông tin cần thiết sau đó nhấn vào Create App. Các thông tin bao gồm tên app và app đó được phép truy cập vào workspace nào. Như anh Quang thì anh ý tạo app có tên là Notification và được phép truy cập vào workspace Phambinh.net.

Bước 4: Sau khi nhấn Create App. Nếu thành công, bạn sẽ được chuyển hướng tới một trang mới. Tại đây, bạn hãy tìm tới mục Incomming Webhooks và nhấp vào.

Bước 5: Bật Incomming Webhooks và nhấn vào nút Add New Webhook to Workspace để tạo một Incomming Webhook mới.

Bước 6: Xác nhận cho phép app của bạn được phép gửi tin nhắn vào channel nào trong workspace đã chọn. Sau đó nhấn vào Allow. Như minh là mình cho gửi vào channel general.

Bước 7: Hoàn tất các bước tạo Incomming Webhook, bạn sẽ được chuyển hướng vào trang mới. Ở đây bạn sẽ thấy thông tin về Webhook mà bạn vừa tạo, hãy nhấp vào nút Copy để copy URL của webhook.

Hãy nhớ đừng để lộ URL này, nếu không ai cũng có thể gửi tin nhắn tới channel của bạn đấy.

3.3 Tạo một Notification để thông báo Exception

Anh Quang chạy command sau để tạo ra một notification class, có tên là HasExceptionNotification

php artisan make:notification HasExceptionNotification

Sau khi chạy command trên HasExceptionNotification sẽ được tạo ra tại App\Notifications\HasExceptionNotification.

Mở App\Notifications\HasExceptionNotification xóa hết nội dung và thay thế bằng

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\SlackMessage;
use App\Events\HasExceptionEvent;

class HasExceptionNotification extends Notification
{
    use Queueable;

    protected $event;

    public function __construct(HasExceptionEvent $event)
    {
        $this->event = $event;
    }

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

    public function toSlack($notifiable)
    {
        $notification = (new SlackMessage)
            ->error()
            ->content($this->event->exception->getMessage())
            ->attachment(function ($attachment) {
                $attachment->title(get_class($this->event->exception))
                    ->content('File: ' . $this->event->exception->getFile() . ' on line' . $this->event->exception->getLine());
            });

        if ($this->event->metas and is_array($this->event->metas)) {
            $notification->attachment(function ($attachment) {
                $contents = [];
                foreach ($this->event->metas as $name => $value) {
                    $contents[] = "$name: $value";
                }
                $attachment->title('Metas')->content(implode("\n", $contents));
            });
        }

        return $notification;
    }
}

3.4 Tạo HasExceptionEvent và HasExceptionListener

Mở App\Providers\EventServiceProvider để đăng ký HasExceptionEventHasExceptionListener như sau

<?php
// app/Providers/EventServiceProvider.php

// ...
protected $listen = [
    //...
    'App\Events\HasExceptionEvent' => [
        'App\Listeners\HasExceptionListener'
    ]
];
//...

Chạy command sau để generate event và listenner

php artisan event:generate

Sau khi chạy command trên, HasExceptionEventHasExceptionListener sẽ lần lượt được tạo ra tại App\Events\HasExceptionEventApp\Listenners\HasExceptionListener

Mở App\Events\HasExceptionEvent, xóa hết nội dung và thay thế bằng

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Exception;

class HasExceptionEvent
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $exception;
    public $metas;

    public function __construct(Exception $exception, $metas = [])
    {
        $this->exception = $exception;
        $this->metas = $metas;
    }
}

Mở App\Listenners\HasExceptionListener, xóa hết nội dung và thay thế bằng

<?php

namespace App\Listeners;

use App\Events\HasExceptionEvent;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Notifications\HasExceptionNotification;
use Notification;
use Exception;

class HasExceptionListener
{
    public function handle(HasExceptionEvent $event)
    {
        $notify = new HasExceptionNotification($event);
        $slackWebHookUrl = ''; // paste your webhook slack url here
        Notification::route('slack', $slackWebHookUrl)->notify($notify);
    }
}

3.5 Phát sự kiện HasExceptionEvent khi có exception xảy ra

Laravel quản lý việc throw exception tại App\Exceptions\Handler, có nghĩa là tất cả exception của dự án Laravel trước khi được throw đều phải đi qua lớp Handler này, cụ thể là đi qua method report(). Biết được vậy, anh Quang tìm liền tìm tới method report() trong lớp Handler và sửa lại như sau

<?php
// use App\Events\HasExceptionEvent;
// ...
public function report(Exception $exception)
{
    parent::report($exception);

    if ($this->shouldReport($exception)) {
        event(new HasExceptionEvent($exception));
    }
}
//...

Vậy là mỗi khi dự án có exception, là sự kiện HasExceptionEvent sẽ được phát ra.

IV. Chạy thử

Muốn thử nghiệm thì rất đơn giản, chỉ cần cố tình tạo ra một exception nào đấy là xong, cái gì chứ việc tạo ra exception thì mình vẫn làm hằng ngày mà – anh Quang nghĩ. Anh mở file routes/web.php và xóa đi một dấu chấm phẩy. Rồi tải lại trang web, thì

Bùm… exception đã xảy ra. Nhưng khác với mọi lần, đây là cái exception hạnh phúc nhất mà anh từng gặp bởi vì nó do anh cố tình tạo ra.

Nhảy qua Slack để kiểm tra, thì anh thấy có một tin nhắn với nội dung thế này

Vậy là việc tích hợp gửi thông báo khi có exception xảy ra đã thành công – anh Quang kêu lên sung sướng. Sau đó, anh Quang còn cài app Slack cho smartphone và cả chiếc macbook anh đang sử dụng, để đảm bảo là anh không bỏ qua exception nào.

V. Lời kết

Anh cũng chia sẻ thêm với mình rằng nhờ có cơ chế đó mà anh và team đã vá hết các exception trong vòng một tháng. Vì vậy mà hệ thống hoạt động trơn tru, số lượng người dùng tăng đột biến, sếp thấy thế nên cũng đã tăng lương cho anh.

Bạn thấy không, chỉ với một công việc đơn giản như vậy mà hệ thống đã được cải thiện đáng kể. Nếu như bạn đang làm việc với một dự án mà chưa được tích hợp cơ chế nào như thế này, thì hãy tích hợp ngay đi, bởi biết đâu bạn cũng sẽ được tăng lương như anh Quang thì sao.

Chúc bạn may mắn.