Giới thiệu

Flask là tuyệt vời, vô cùng linh hoạt để xây dựng một ứng dụng web. Nó nhỏ gọn  hơn rất nhiều so với người anh em Django. Trong suốt một vài dự án tại GMO Z.com vietnam lab center, chúng tôi đã phát triển một số phần liên quan đế sử lý log mà tôi thấy hữu ích và muốn chia sẻ với bạn.

Flask loging

Log mặc định của flask dược hiện thị trên màn hình như sau

# Default settings - meh
[2020-06-12 08:46:44,202] ERROR in app: <Lightsaber noises>
[2020-06-12 08:46:44,202] WARNING in app: General Kenobi
127.0.0.1 - - [12/Jun/2020 08:46:44] "GET / HTTP/1.1" 200 -

Thêm config log có thể set ở app.config . Ví dụ mình đã thêm vào 2 config là LOG_TYPELOG_LEVEL

from flask import Flask, current_app
from flask_logs import LogSetup
import os

app = Flask(__name__)
app.config["LOG_TYPE"] = os.environ.get("LOG_TYPE", "stream")
app.config["LOG_LEVEL"] = os.environ.get("LOG_LEVEL", "INFO")

logs = LogSetup()
logs.init_app(app)

@app.route("/")
def hello_world():
    current_app.logger.info("Hello REM")
    current_app.logger.error("<Lightsaber noises>")
    current_app.logger.warning("General Kenobi")
    return "Hello World!"

if __name__ == "__main__":
    app.run()

Tiếp đó cần phải ghi log ra file để có thể rotation về sau này

from flask import Flask, current_app
from flask_logs import LogSetup
import os

app = Flask(__name__)

app.config["LOG_TYPE"] = os.environ.get("LOG_TYPE", "watched")
app.config["LOG_LEVEL"] = os.environ.get("LOG_LEVEL", "INFO")

# File Logging Setup
app.config['LOG_DIR'] = os.environ.get("LOG_DIR", "./")
app.config['APP_LOG_NAME'] = os.environ.get("APP_LOG_NAME", "app.log")
app.config['WWW_LOG_NAME'] = os.environ.get("WWW_LOG_NAME", "www.log")
logs = LogSetup()
logs.init_app(app)

@app.route("/")
def hello_world():
    current_app.logger.info("Hello REM")
    current_app.logger.error("<Lightsaber noises>")
    current_app.logger.warning("General Kenobi")
    return "Hello World!"

if __name__ == "__main__":
    app.run()

Như ở trên mình đã thêm các config LOG_DIRAPP_LOG_NAMEWWW_LOG_NAME

Class LogSetup của mình như sau

from logging.config import dictConfig


class LogSetup(object):
    def __init__(self, app=None, **kwargs):
        if app is not None:
            self.init_app(app, **kwargs)

    def init_app(self, app):
        log_type = app.config["LOG_TYPE"]
        logging_level = app.config["LOG_LEVEL"]
        if log_type != "stream":
            try:
                log_directory = app.config["LOG_DIR"]
                app_log_file_name = app.config["APP_LOG_NAME"]
                access_log_file_name = app.config["WWW_LOG_NAME"]
            except KeyError as e:
                exit(code="{} is a required parameter for log_type '{}'".format(e, log_type))
            app_log = "/".join([log_directory, app_log_file_name])
            www_log = "/".join([log_directory, access_log_file_name])

        if log_type == "stream":
            logging_policy = "logging.StreamHandler"
        elif log_type == "watched":
            logging_policy = "logging.handlers.WatchedFileHandler"
        else:
            log_max_bytes = app.config["LOG_MAX_BYTES"]
            log_copies = app.config["LOG_COPIES"]
            logging_policy = "logging.handlers.RotatingFileHandler"

        std_format = {
            "formatters": {
                "default": {
                    "format": "[%(asctime)s.%(msecs)03d] %(levelname)s %(name)s:%(funcName)s: %(message)s",
                    "datefmt": "%d/%b/%Y:%H:%M:%S",
                },
                "access": {"format": "%(message)s"},
            }
        }
        std_logger = {
            "loggers": {
                "": {"level": logging_level, "handlers": ["default"], "propagate": True},
                "app.access": {
                    "level": logging_level,
                    "handlers": ["access_logs"],
                    "propagate": False,
                },
                "root": {"level": logging_level, "handlers": ["default"]},
            }
        }
        if log_type == "stream":
            logging_handler = {
                "handlers": {
                    "default": {
                        "level": logging_level,
                        "formatter": "default",
                        "class": logging_policy,
                    },
                    "access_logs": {
                        "level": logging_level,
                        "class": logging_policy,
                        "formatter": "access",
                    },
                }
            }
        elif log_type == "watched":
            logging_handler = {
                "handlers": {
                    "default": {
                        "level": logging_level,
                        "class": logging_policy,
                        "filename": app_log,
                        "formatter": "default",
                        "delay": True,
                    },
                    "access_logs": {
                        "level": logging_level,
                        "class": logging_policy,
                        "filename": www_log,
                        "formatter": "access",
                        "delay": True,
                    },
                }
            }
        else:
            logging_handler = {
                "handlers": {
                    "default": {
                        "level": logging_level,
                        "class": logging_policy,
                        "filename": app_log,
                        "backupCount": log_copies,
                        "maxBytes": log_max_bytes,
                        "formatter": "default",
                        "delay": True,
                    },
                    "access_logs": {
                        "level": logging_level,
                        "class": logging_policy,
                        "filename": www_log,
                        "backupCount": log_copies,
                        "maxBytes": log_max_bytes,
                        "formatter": "access",
                        "delay": True,
                    },
                }
            }

        log_config = {
            "version": 1,
            "formatters": std_format["formatters"],
            "loggers": std_logger["loggers"],
            "handlers": logging_handler["handlers"],
        }
        dictConfig(log_config)

Trong hàm init_app(self, app): sẽ lấy các config đã được set ở trên như là log_type=app.config["LOG_TYPE"]

Sau đó sẽ chỉ định clask Log Handler nào được thực thi tùy theo config

  • logging.StreamHandler
  • logging.handlers.WatchedFileHandler
  • logging.handlers.RotatingFileHandler

logging_handler Sẽ định nghĩa các setting cho từng loại log handler ở trên

StreamHander

https://docs.python.org/3/library/logging.handlers.html

Đây là class log base, class Handler cha chó các class Log handler khác. Sử dụng khi muốn chia log theo level và cần format cho log. Sử dụng class này log sẽ được hiện thị trực tiếp ra console

Các setting cần thiết cở bản như là level và formatter

WatchedFileHandler

Đây là class con của class logging.FileHander . Tuy nhiên nó được mở rộng để ghi log ra file

RotatingFileHandler

Đây là class để ghi log ra file và có các hàm hỗ trợ rotation theo file size.

TimedRotatingFileHandler

Đây là class để ghi log ra file và có các hàm hỗ trợ rotation theo thời gian. Có thể chỉ định thời gian rotation, tuy nhiên với handler này thì thời gian không được set cố định mà chỉ set được một khoảng thời gian.

Ví dụ mình muốn rotation theo từng tiếng và log sẽ chạy từ phút thứ 15 thì đến phút thứ 15 của giờ tiếp theo sẽ được tách ra thành 1 file. Tức là không thể cố định được theo thời gian cụ thể mả chỉ có thể set được khoảng thời gian.

Tạo Log rotation handler mới

Dưới đây mình đã thử viết một class log handle mới và tự ghi ra file mới khi đã hết khoảng thời gian là 1 ngày. Khi ghi ra file mới cần phải close  stream và mở lai stream mới

class TimedFileHandler(logging.FileHandler):

    def __init__(self, filename, encoding='utf8', delay=False):
        """
        Use the specified filename for streamed logging
        """
        # self.current_half_hour = half_hour()
        current_time = int(time.time())
        self.current_date = time.strftime("%Y%m%d", time.localtime(current_time))
        filename = filename + "." + self.current_date
        logging.FileHandler.__init__(self, filename, 'a', encoding, delay)

    def should_rollover(self):
        t = int(time.time())
        new_date = time.strftime("%Y%m%d", time.localtime(t))
        if new_date > self.current_date:
            self.current_date = new_date
            return 1
        return 0

    def do_rollover(self):
        if self.stream:
            self.stream.close()
            self.stream = None
        self.baseFilename = self.baseFilename[:-8] + self.current_date
        self.stream = self._open()

    def emit(self, record):
        try:
            if self.should_rollover():
                self.do_rollover()
            logging.FileHandler.emit(self, record)
        except Exception:
            self.handleError(record)

    def _open(self):
        file_exists = os.path.exists(self.baseFilename)
        stream = logging.FileHandler._open(self)
        if not file_exists:
            stream.write(LOG_HEADER)
        return stream

Kết luật

Nước sơn thì giống UI của hệ thống, còn gỗ tốt hay không thì cái vụ logging này nó cũng quyết định lắm nè! Nếu không coi trọng logging, khi hệ thống bị bug hay bị attack (gọi là gỗ bị sâu) thì, hoặc là chả coi được log hoặc là log có đó mà không xài được dẫn đến hoàn cảnh bế tắc, lâu dần càng ngày gỗ càng mục nát mà! Cuối cùng người đau khổ và phải hấng chịu nhiều nhất vẫn chính là các bạn - người làm ra hệ thống.

Với những hệ thống quy mô trung bình, được thiết kế scale ngang nhiều server chẳng hạn đi nữa, mình nghĩ log cực kỳ quan trọng trong việc sử lý lỗi và phân tích hệ thống. Vì thế rất recommend mọi người cài đặt và cải thiện hệ thống để nó ngày càng “tốt gỗ” hơn nhé.