+1

Series Docker & ELK Stack cho Laravel #2: Bơm dữ liệu vào Elasticsearch và Tư duy Structured Logging

Ở Bài 1, chúng ta đã xây xong "căn cứ địa" với Elasticsearch và Kibana chạy êm ru trong Docker. Nhưng một hệ thống Dashboard xịn sò sẽ vô nghĩa nếu không có dữ liệu. Hôm nay, nhiệm vụ của chúng ta là "bơm" hàng triệu dòng log từ hệ thống Laravel vào thẳng bụng của Elasticsearch.

Tuy nhiên, kết nối Laravel với Elasticsearch không đơn giản chỉ là cài một package rồi gọi lệnh. Trong môi trường chịu tải cao như hệ thống vé AFC Metro, nếu bạn bắt API chờ Elasticsearch lưu log xong rồi mới phản hồi cho người dùng, toàn bộ server sẽ sụp đổ vì nghẽn I/O (Input/Output Blocking).

Chúng ta sẽ tiếp cận việc này theo tư duy Structured Logging (Ghi log có cấu trúc) và né tránh tử huyệt hiệu năng chuẩn Senior.

1. Khai báo Driver giao tiếp

Đầu tiên, hệ thống Laravel cần bộ từ điển để biết cách nói chuyện với Elasticsearch thông qua API. Thư viện cốt lõi nhất mà bạn cần kéo về là Elasticsearch PHP Client:

composer require elastic/elasticsearch

(Lưu ý: Đảm bảo version của thư viện PHP client khớp với version của Elasticsearch image trong Docker - ở bài trước chúng ta dùng v8.x).

2. Thiết lập Kênh Log (Log Channel) chuyên dụng

Laravel quản lý log thông qua config/logging.php. Chúng ta không ghi đè lên file laravel.log cũ, mà sẽ tạo ra một kênh mới độc lập.

Hãy mở config/logging.php và thêm channel elasticsearch vào mảng channels:

'elasticsearch' => [
            'driver' => 'custom',
            'via' => App\Logging\ElasticsearchLogger::class,
            'level' => env('LOG_LEVEL', 'debug'),
        ],

Tiếp theo, tạo Class ElasticsearchLogger để khởi tạo kết nối. Đây là nơi bạn cấu hình Client để trỏ vào IP của container Elasticsearch đã dựng ở Bài 1:

php artisan make:class Logging/ElasticsearchLogger
namespace App\Logging;

use Elastic\Elasticsearch\ClientBuilder;
use Monolog\Logger;
use Monolog\Handler\ElasticsearchHandler;
use Monolog\Formatter\ElasticsearchFormatter;

class ElasticsearchLogger
{
    /**
     * Tạo một Monolog instance truyền dữ liệu tới Elasticsearch.
     */
    public function __invoke(array $config): Logger
    {
        // Khởi tạo Client trỏ về Container ES chạy ở port 9200
        $client = ClientBuilder::create()
            ->setHosts(['http://127.0.0.1:9200']) // Nếu chạy Docker cùng máy. Nếu khác mạng thì điền IP nội bộ.
            ->build();

        // Định nghĩa cấu hình Handler
        $options = [
            'index' => 'afc-system-logs-' . date('Y-m'), // Phân mảnh Index theo tháng
            'type'  => '_doc',
        ];

        $handler = new ElasticsearchHandler($client, $options);
        
        // Dùng Formatter chuẩn của Monolog để ép kiểu JSON tối ưu cho ES
        $handler->setFormatter(new ElasticsearchFormatter($options['index'], $options['type']));

        return new Logger('elasticsearch', [$handler]);
    }
}

3. Tư duy Structured Logging (Chuẩn hóa cấu trúc)

Lỗi nặng nhất của người mới khi dùng Kibana là ghi log theo kiểu "kể chuyện":

// ❌ Rất tệ khi dùng với Kibana
\Log::channel('elasticsearch')->info("Khách hàng Nguyễn Văn A vừa quẹt thẻ E1F4A tại nhà ga S01");

Nếu bạn ghi thế này, Kibana chỉ nhận được một chuỗi text dài. Khi sếp yêu cầu: "Hãy đếm xem hôm nay ga S01 có bao nhiêu lượt quẹt thẻ?", bạn sẽ phải dùng Regex để tách chuỗi cực kỳ khổ sở, và Elasticsearch tìm kiếm text toàn văn (Full-text) sẽ ăn sạch CPU.

Chuẩn Senior: Log là Dữ liệu (Data), không phải Văn bản. Mọi giá trị cần đếm, lọc, hoặc nhóm (Group By) phải được đẩy vào biến context dưới dạng Key-Value rõ ràng:

// ✅ Chuẩn Enterprise
\Log::channel('elasticsearch')->info("Khách hàng quẹt thẻ thành công", [
    'station_id' => 'S01',
    'card_uid'   => 'E1F4A5002B',
    'action'     => 'tap_in',
    'status'     => 'success'
]);

Nhờ cách viết này, khi dữ liệu vào Kibana, nó sẽ tự động nhận diện station_id là một cột riêng biệt, cho phép bạn chỉ mất 2 giây để vẽ biểu đồ hình tròn (Pie chart) so sánh lưu lượng giữa các nhà ga.

4. Tử huyệt hiệu năng: Giải pháp Queue & Filebeat

Đoạn code ở mục 2 sẽ hoạt động hoàn hảo, nhưng nó mang một rủi ro ngầm: Tính đồng bộ (Synchronous).

Mỗi khi hàm \Log::info() được gọi, PHP sẽ phải mở một HTTP Request đến Elasticsearch (port 9200), chờ ES báo "OK" rồi mới chạy tiếp. Nếu lượng giao dịch tăng vọt lên 1000 requests/giây, hoặc container ES bị chậm đi 200ms, toàn bộ API của Laravel sẽ bị ngâm (treo) thêm 200ms tương ứng!

Để giải quyết vấn đề này, các hệ thống lớn thường chọn một trong hai phương án:

  1. Phương án Code (Dùng Queue): Đừng gọi trực tiếp Log vào Elasticsearch. Hãy đẩy dữ liệu Log vào một Job (ví dụ: PushLogToElasticsearchJob), đưa nó vào Redis Queue, và cho các Worker chạy ngầm (Asynchronous) túc tắc nạp dữ liệu vào ES mà không làm chậm API.
  2. Phương án Hạ tầng (Dùng Filebeat): Trả file config/logging.php về việc ghi log ra một file storage/logs/kibana.json. Sau đó, cài đặt một công cụ siêu nhẹ của Elastic là Filebeat chạy ngầm trên Server. Filebeat sẽ đóng vai trò "người vận chuyển", liên tục đọc file kibana.json và bắn vào Elasticsearch. (Đây là phương án tối ưu nhất, Zero-blocking cho PHP).

Tại thời điểm này, dù dùng trực tiếp hay dùng Queue, dữ liệu chuẩn cấu trúc JSON của chúng ta đã nằm ngay ngắn bên trong Elasticsearch.

Ở Bài 3, bạn muốn chúng ta mở giao diện Kibana lên để bắt tay vào việc vẽ Dashboard và các biểu đồ Real-time (Data Views, Lens), hay bạn muốn đi sâu hơn vào việc cấu hình kiến trúc Filebeat để tối ưu hóa hiệu năng tới mức tuyệt đối?


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí