- vừa được xem lúc

Thiết kế hệ thống logging

0 0 18

Người đăng: tuananhhedspibk

Theo Viblo Asia

Với những bạn đã và đang lập trình thì việc "check log" trong quá trình debug không còn là một điều xa lạ. Nếu không có log, chúng ta không thể biết được file nào, dòng nào trong file đó đang phát sinh lỗi để có thể sửa chữa kịp thời.

Đó là đối với những chương trình hoặc file đơn lẻ, thế nhưng với những hệ thống phục vụ người dùng trong thực tế thì việc "check log" lại càng trở nên quan trọng hơn vì nó giúp chúng ta:

  • Điều tra, phát hiện ra những lỗi đang xảy ra làm ảnh hưởng đến người dùng cuối.
  • Theo dõi "sức khoẻ" của hệ thống để từ đó có thể phát hiện ra những "dấu hiệu bất thường" trước khi nó kịp phát tác làm ảnh hưởng đến hệ thống.
  • ...

Qua đó chúng ta có thể thấy việc "check log" đóng vai trò rất quan trọng trong quá trình phát triển cũng như vận hành một phần mềm hoặc một hệ thống (còn gọi là monitoring), thế nên việc thiết kế và triển khai một hệ thống log tốt sẽ giúp quá trình monitoring trở nên dễ dàng hơn rất nhiều.

Trong bài viết lần này tôi xin phép được gửi đến bạn đọc những kiến thức thực tế mà tôi đã tiếp thu được về việc xây dựng một hệ thống logging, một phần là để bạn đọc cảm nhận rõ hơn về tầm quan trọng của log khi vận hành hệ thống, một phần là để bạn đọc có được cho mình một tài liệu tham khảo khi muốn xây dựng một hệ thống logging trong thực tế. Rất mong được bạn đọc đón nhận.

Logging Policy

Dưới đây là những câu hỏi chúng ta cần đặt ra trước khi xây dựng một hệ thống log.

  • Why: Log nhằm mục đích gì ?
  • Who: Hệ thống hay module nào tạo ra log (API, APP, ...) ?
  • When: Khi nào, thời điểm nào thì đưa ra log ?
  • Where: Đưa ra log ở đâu (log sẽ được gửi đến Slack, BigQuery, ...) ?
  • What: Log về cái gì ?
  • How: Cách thức đưa ra log là như thế nào ?

Log level

Sau khi đã hiểu rõ được mục đích của mình khi đưa ra log, chúng ta cần "phân cấp" log.

Log level Khái niệm Cách đối ứng Ví dụ
FATAL Level này gây cản trở đến việc vận hành của hệ thống Cần sửa ngay Không thể kết nối tới DB
ERROR Việc thực thi gây ra những lỗi ngoài dự tính Sửa trong thời gian hoạt động của hệ thống Không thể gửi mail
WARN Không phải lỗi nhưng là những vấn đề như: input không như mong muốn hoặc thực thi không như mong muốn Refactor định kì API xoá dữ liệu một cách định kì
INFO Thông báo khi bắt đầu hoặc kết thúc một xử lí, transaction. Cũng có thể là việc đưa ra các thông tin cần thiết khác Không cần phải sửa Đưa ra nội dung của req / res, hoặc khi bắt đầu hoặc kết thúc batch
DEBUG Thông tin liên quan đến trạng thái hoạt động của hệ thống Không đưa ra ở môi trường production Có thể đặt ở các hàm bên trong app
TRACE Thông tin chi tiết ở mức độ cao hơn DEBUG Không đưa ra ở môi trường production

Case

Sau khi đã rõ các cấp độ log, chúng ta cần làm rõ "các loại log" mà mình muốn đưa ra. Trong phần này đối với từng loại log tôi sẽ đi trả lời 6 câu hỏi:

  • Why
  • Who
  • When
  • Where
  • What
  • How

như đã nói chi tiết ở phần Logging Policy phía trên.

System log

  • Why: Những log này sẽ sử dụng để debug khi hệ thống có lỗi.
  • Who: Bản thân hệ thống sẽ sinh ra log.
  • When: Đưa ra log khi phát sinh lỗi.
  • Where:
    • FATAL / ERROR: Thông báo đến một kênh mà dev có thể nhận ra và xử lí kịp thời.
    • WARN / INFO: Đưa ra bên trong hệ thống hoặc các tool quản lí log.
    • DEBUG / TRACE: Đưa ra ở dạng console.log trên môi trường staging.
  • What:
    • FATAL / ERROR: Stacktrace.
    • WARN / INFO / DEBUG/ TRACE: Nội dung muốn thông báo.
  • How:
    • FATAL / ERROR: Đưa ra log thông qua các tool quản lí log hoặc đưa tới Slack, SMS, ... (dưới hình thức push).
    • WARN / INFO / DEBUG / TRACE: Đưa ra log thông qua các tools quản lí log hoặc bên trong hệ thống, ... (dưới hình thức pull).

Access log

  • Why: Đưa ra log để theo dõi việc gửi, nhận request.
  • Who: Bản thân hệ thống hoặc từ phía infrastructure.
  • When: Đưa ra tại thời điểm access (gửi, nhận request)
  • Where: Chủ yếu là log ở level INFO, nên sẽ đưa ra ở dạng pull. Ngoài ra do lượng log có thể nhiều nên tốc độ tìm kiếm log cũng là việc cần phải chú ý.
  • What: Đưa ra ai, bằng cách nào, khi nào access đến hệ thống.
  • How: Tuỳ vào đối tượng và mục tiêu mà có sự khác biệt.

Action log

  • Why: Đưa ra để tiến hành phân tích hành vi của người dùng, qua đó nhằm cải thiện service.
  • Who: Bản thân hệ thống hoặc external tools.
  • When: Khi phát sinh ra actions (hành động).
  • Where: Các tool có khả năng phân tích log (BigQuery, BI, ...).
  • What: Tuỳ vào mục tiêu.
  • How: Tuỳ vào đối tượng và mục tiêu mà có sự khác biệt.

Auth log

  • Why: Đưa ra nhằm mục đích theo dõi quá trình xác thực người dùng.
  • Who: Bản thân hệ thống.
  • When: Khi tiến hành xác thực người dùng.
  • Where: Chủ yếu là level INFO, dưới hình thức pull.
  • What: Đưa ra ai, bằng cách nào, khi nào thì được xác thực.
  • How: Tuỳ vào phương thức xác thực mà có sự khác biệt

Ví dụ minh hoạ

Lí thuyết như vậy là đủ, giờ chúng ta hãy cùng nhau thử triển khai nó trong một project đơn giản. Bạn đọc có thể tham khảo source code tại: https://github.com/tuananhhedspibk/NewAnigram-BE-DDD

Lựa chọn thư viện log

Ở đây tôi sẽ sử dụng thư viện log4js (https://github.com/log4js-node/log4js-node). Lí do rất đơn giản vì bản thân log4js cũng tổ chức hệ thống phân cấp log của mình giống như tư tưởng mà tôi đã trình bày trong phần Log level ở trên.

Triển khai

Bước 1 - Định nghĩa class Logger

Đầu tiên tôi sẽ định nghĩa một class Logger riêng như sau:

class Logger { public default: log4js.Logger; public system: log4js.Logger; public api: log4js.Logger; public access_req: log4js.Logger; public access_res: log4js.Logger; public sql: log4js.Logger; public auth: log4js.Logger; public fatal: log4js.Logger; public error: log4js.Logger; public warn: log4js.Logger; public info: log4js.Logger; public debug: log4js.Logger; public trace: log4js.Logger; constructor() { log4js.configure(loggerConfig); this.system = log4js.getLogger('system'); this.api = log4js.getLogger('api'); this.access_req = log4js.getLogger('access_req'); this.access_res = log4js.getLogger('access_res'); this.sql = log4js.getLogger('sql'); this.auth = log4js.getLogger('auth'); this.fatal = log4js.getLogger('fatal'); this.fatal.level = log4js.levels.FATAL; this.error = log4js.getLogger('error'); this.error.level = log4js.levels.ERROR; this.warn = log4js.getLogger('warn'); this.warn.level = log4js.levels.WARN; this.info = log4js.getLogger('info'); this.info.level = log4js.levels.INFO; this.debug = log4js.getLogger('debug'); this.debug.level = log4js.levels.DEBUG; this.trace = log4js.getLogger('trace'); this.trace.level = log4js.levels.TRACE; }
}

Tôi định nghĩa bên trong class Logger các Log Levels như:

  • fatal
  • error
  • debug
  • ...

ngoài ra còn có các Log Types như:

  • system
  • api
  • access_req
  • access_res
  • ...

Bước 2 - "Áp dụng" Logger vào project

Project lần này tôi sử dụng framework NestJS để triển khai. Tôi sẽ áp dụng Logger vào project của mình dưới hình thức một "Interceptor", cụ thể hơn về "Interceptor" trong NestJS bạn đọc có thể tham khảo bài viết dưới đây của tôi để biết thêm chi tiết:

https://viblo.asia/p/cac-concepts-co-ban-trong-nestjs-aAY4qepK4Pw

Lí do tôi chọn "Interceptor" là hình thức triển khai log là do "Interceptor" có thể "bao ngoài" request stream cũng như response stream đến và đi từ API nên sử dụng Interceptor là cách dễ dàng nhất để có thể bắt được request logresponse log. Tôi định nghĩa LoggerInterceptor class như sau:

export class LoggerInterceptor implements NestInterceptor { intercept( context: ExecutionContext, next: CallHandler<any> ): Observable<any> | Promise<Observable<any>> { // intercept() method sẽ "bao ngoài" request/ response stream /* * Lấy request object từ context * Sau đó cho request object đi qua hàm hiển thị log là "requestLogger" */ const request = context.switchToHttp().getRequest(); requestLogger(request); /* * Lấy response object từ context * Sau đó cho response object đi qua hàm hiển thị log là "responseLogger" & "responseErrorLogger" */ const response = context.switchToHttp().getResponse(); return next.handle().pipe( // 200 - Success Response map((data) => { responseLogger({ requestId: request._id, response, data }); }), // 4xx, 5xx - Error Response tap(null, (exception: HttpException | Error) => { try { responseErrorLogger({ requestId: request._id, exception }); } catch (e) { logger.access_res.error(e); } }) ); }
}

bạn đọc có thể tham khảo chi tiết hơn tại đường dẫn sau: https://github.com/tuananhhedspibk/restful-app/blob/main/libs/interceptor/logger/index.ts

Tôi định nghĩa 3 methods:

  • requestLogger
  • responseLogger
  • responseErrorLogger

lần lượt như sau:

export const requestLogger = (request: Request) => { const { ip, originalUrl, method, params, query, body, headers } = request; // logTemplate sẽ bao gồm: now(thời gian) ip method url request_object const logTemplate = '%s %s %s %s %s'; const now = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS'); const logContent = util.formatWithOptions( { colors: true }, logTemplate, now, ip, method, originalUrl, JSON.stringify({ method, url: originalUrl, userAgent: headers['user-agent'], body: _maskFields(body, 'password'), params, query, }) ); // Gọi đến access_req logger object đã được định nghĩa trước đó logger.access_req.info(logContent);
}; // Hiển thị success response log
export const responseLogger = (input: { requestId: number; response: Response; data: any;
}) => { const { requestId, response, data } = input; const log: ResponseLog = { requestId, statusCode: response.statusCode, data, }; // Gọi đến access_res logger object đã được định nghĩa trước đó logger.access_res.info(JSON.stringify(log));
}; // Hiển thị error response log
export const responseErrorLogger = (input: { requestId: number; exception: HttpException | Error;
}) => { const { requestId, exception } = input; const log: ResponseLog = { requestId, statusCode: exception instanceof HttpException ? exception.getStatus() : null, message: exception?.stack || exception?.message, }; // Gọi đến access_res logger object đã được định nghĩa trước đó logger.access_res.info(JSON.stringify(log)); logger.access_res.error(exception);
};

bạn đọc có thể tham khảo cụ thể hơn tại đường dẫn sau: https://github.com/tuananhhedspibk/restful-app/blob/main/libs/middleware/logger/index.ts

Sau khi đã định nghĩa xong "LoggerInterceptor" cũng các methods bổ trợ, tôi sẽ "apply" interceptor này vào main app của mình như sau:

const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(new LoggerInterceptor());

rất đơn giản vì đây là một tính năng sẵn có của framework NestJS, cụ thể hơn về source code bạn đọc có thể tham khảo tại đây: https://github.com/tuananhhedspibk/restful-app/blob/main/src/main.ts

Vậy thì với fatal hay debug log thì sao ? Các loại log này sẽ được tôi sử dụng trong tầng nghiệp vụ (usecase) hoặc tầng infrastructure với các mục đích:

  • Thông báo các lỗi mang tính "chí mạng" như việc kết nối tới DB bị "ngắt".
  • Có cơ sở để debug nếu user gặp lỗi.

Rất đơn giản thôi, chỉ cần làm như sau là được

logger.fatal.error('Error message');

Bạn đọc có thể tham khảo một ví dụ tại: https://github.com/tuananhhedspibk/restful-app/blob/main/src/users/application/command/signup/handler.ts#L35

Và đây là kết quả:

Đầu tiên là với access request logresponse log (khi không xảy ra lỗi)

Screenshot 2024-05-16 at 23 05 12

bạn đọc có thể thấy các thông tin liên quan đến request như method, body, .. cũng như thông tin liên quan đến response là statusCode đều được hiển thị rõ ràng.

Còn trong trường hợp xảy ra lỗi:

Screenshot 2024-05-16 at 23 05 26

thì loại lỗi, error message cũng đều được hiển thị.

Tiếp theo là fatal log sẽ như sau:

Screenshot 2024-05-16 at 23 09 21

error message và error stack cũng được show ra.

Tổng kết

Vậy là trong bài viết lần này tôi đã chia sẻ được với bạn đọc cách thức thiết kế cũng như triển khai một hệ thống logging cơ bản. Ví dụ minh hoạ lần này không quá phức tạp nhưng tôi hi vọng rằng bạn đọc có thể hiểu được tầm quan trọng cũng như sự cần thiết của việc xây dựng một hệ thống log "chỉnh chu" sẽ giúp ích rất nhiều trong quá trình vận hành một hệ thống thực tế đặc biệt là khi hệ thống gặp lỗi hoặc trục trặc.

Cảm ơn bạn đọc đã đón nhận, xin hẹn gặp lại ở các bài viết tiếp theo. Happy coding.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Triển khai EFK Stack trên Kubernetes

EFK stack on K8S. Giới thiệu. Một hệ thống có thể chạy nhiều dịch vụ hoặc ứng dụng khác nhau, vì vậy việc. theo dõi hệ thống là vô cùng cần thiết.

0 0 65

- vừa được xem lúc

[Spring] - Spring logback config

Trong spring boot có nhiều cách để cấu hình logging. Với mình thì mình thường làm là config trong file aplication.properties hay application.yml hoặc file logback.

0 0 37

- vừa được xem lúc

Ghi Log Messages trong quá trình phát triển hệ thống, ứng dụng phần mềm

Lang thang vào viblo xem mấy thông báo 'bài viết XYZ của bạn đã được upvote' hay 'Trần X, Phạm Y... đã theo dõi bạn' mình tình cờ thấy được thông báo này của Viblo. Và chắc chắn rồi, đầu năm người Việ

0 0 28

- vừa được xem lúc

[K8S] Phần 12 - Logging trên k8s - section1

Lời tựa. Tiếp tục series về Kubernetes, mình sẽ giới thiệu với các bạn về một chủ đề rất quan trọng đó là Logging.

0 0 26

- vừa được xem lúc

[K8S] Phần 14 - Logging trên k8s sử dụng ELK - section3

Lời tựa. Trong 2 bài viết trước mình đã tự đặt ra bài toán logging cho hệ thống cũng như xây dựng xong môi trường gồm các service opensource và tự build.

0 0 42

- vừa được xem lúc

[K8S] Phần 13 - Logging trên k8s sử dụng ELK - section2

Lời tựa. Tiếp tục về chủ đề logging trên Kubernetes, bài viết này sẽ đi vào xây dựng môi trường và thiết kế giải pháp trước khi xây dựng hệ thống logging ELK.

0 0 28