Flask có sẵn hệ thống signals — các “móc” phát ra ở từng mốc của vòng đời request: khi request bắt đầu, khi response hoàn tất, khi có exception chưa được xử lý. Chỉ cần gắn handler một lần ở nơi khởi tạo app, bạn sẽ log được mọi lỗi 500 và đo thời gian xử lý của mọi request mà không phải chạm vào bất kỳ view nào.

Vấn đề

Muốn biết request nào chậm hay lỗi 500 ở đâu, cách thường thấy là chèn code đo thời gian và try/except vào từng view. Với app chỉ vài route thì ổn, nhưng khi có vài chục view, cách này lặp lại khắp nơi, dễ sót view mới và làm view phình to vì code ngoài nghiệp vụ. Tệ hơn, lỗi 500 chỉ lộ ra khi user báo, vì không có chỗ nào tập trung ghi lại exception kèm đường dẫn gây lỗi.

Giải pháp

Flask phát signals qua thư viện blinker — Flask 3.x đã khai báo blinker là dependency nên thường có sẵn, nếu chưa có thì chạy pip install blinker. Ba signals hữu ích nhất: request_started (request bắt đầu), request_finished (kèm response trước khi trả về client) và got_request_exception (có exception chưa được xử lý). Gắn handler bằng .connect(handler, app) ngay sau khi tạo app, hoặc trong app factory:

import logging
import time
from flask import Flask, g, request
from flask.signals import (
got_request_exception,
request_finished,
request_started,
)
app = Flask(__name__)
logger = logging.getLogger(__name__)
def _bat_dau(sender, **extra):
g.started_at = time.monotonic()
def _ket_thuc(sender, response, **extra):
elapsed = time.monotonic() - g.pop("started_at", time.monotonic())
logger.info("%s %s -> %s (%.3fs)",
request.method, request.path,
response.status_code, elapsed)
def _bat_loi(sender, exception, **extra):
logger.exception("Lỗi 500 tại %s %s",
request.method, request.path)
request_started.connect(_bat_dau, app)
request_finished.connect(_ket_thuc, app)
got_request_exception.connect(_bat_loi, app)

g lưu thời điểm bắt đầu vì nó chỉ sống đúng một request, và request_finished luôn chạy kể cả khi view lỗi, nên thời gian đo được là toàn bộ vòng đời request. Mỗi handler nhận sender là app và **extra chứa dữ liệu riêng của từng signal: response ở request_finished, exception ở got_request_exception.

Lưu ý: nếu blinker chưa được cài, Flask thay signals bằng bản “giả” — handler vẫn đăng ký bình thường nhưng không bao giờ chạy, và không có lỗi nào báo ra. Hãy kiểm tra request_started.receivers có rỗng không sau khi connect, hoặc chạy pip show blinker trước khi debug.

Áp dụng vào dự án của bạn

Gắn ba handler này trong create_app() để mọi blueprint đều được theo dõi, rồi đẩy log ra file bằng RotatingFileHandler thay vì để ra console. Khi log đã có method, path, status code và thời gian, bạn chỉ cần grep “500” hoặc sắp xếp theo thời gian là thấy ngay endpoint nào chậm, lỗi nào lặp lại nhiều nhất — đủ để quyết định tối ưu chỗ nào trước mà không cần thêm công cụ APM. Sau này cần gửi cảnh báo về Telegram hay Sentry, chỉ cần thêm một dòng trong _bat_loi, không phải sửa lại view nào cả.

Bài viết liên quan

Câu hỏi thường gặp

Signals khác gì before_request và after_request?

before_request và after_request cũng đo được thời gian request, nhưng chúng chạy trong chuỗi xử lý request của app. Signals tách bạch hơn: handler có thể đặt ở module khác, và got_request_exception bắt được exception mà cặp before/after không thấy. Hai cách bổ sung cho nhau.

Có cần cài thêm gì để dùng signals không?

Cần blinker. Flask 3.x liệt kê blinker là dependency nên thường đã có sẵn; nếu bạn cài Flask với --no-deps hoặc dùng môi trường tối giản, hãy chạy pip install blinker rồi kiểm tra bằng pip show blinker.

abort(404) có kích hoạt got_request_exception không?

Không. abort() ném ra HTTPException và Flask xử lý nó thành response bình thường, nên signal chỉ phát khi exception thật sự không được xử lý — tức lỗi 500. Muốn theo dõi cả 404, hãy nhìn log của request_finished, vì nó ghi mọi status code.

Handler signals có làm chậm request không?

Không đáng kể nếu handler chỉ ghi log. Nhưng đừng làm việc nặng trong handler — gọi API ngoài hay ghi database — vì request_finished chạy trước khi response trả về client, nên việc nặng cộng thẳng vào thời gian phản hồi. Việc nặng nên đẩy sang hàng đợi hoặc thread nền.

💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.

Nguồn tham khảo