Một API Để Quản Lý Tất Cả Logs Trong Dự Án Lớn
Giới Thiệu: Sự Khó Khăn Của Các API Phân Tán
Trong các dự án phần mềm có nhiều đội ngũ và chu kỳ phát triển dài, vấn đề phân tán của các API thường xuyên xảy ra. Mỗi đội, hoặc thậm chí mỗi lập trình viên, có thể chọn các thư viện hoặc cách tiếp cận khác nhau cho những nhiệm vụ chung như logging, xử lý lỗi, yêu cầu HTTP, v.v. Điều này không chỉ dẫn đến sự không đồng nhất, mà còn làm tăng độ phức tạp trong việc bảo trì, gây khó khăn trong việc đào tạo thành viên mới và cuối cùng, làm chậm tiến độ phát triển.
Hãy tưởng tượng một kịch bản trong đó việc ghi log ở frontend sử dụng console.log hoặc một thư viện cụ thể, trong khi backend lại sử dụng một thư viện khác, có thể với các cấp độ và định dạng khác nhau. Nếu bạn cần tập trung log, hoặc thay đổi cách triển khai bên dưới, bạn sẽ phải đối mặt với một nhiệm vụ khổng lồ.
Giải pháp nằm ở sự thống nhất. Tạo ra một "Một API Để Quản Lý Tất Cả" cho một số chức năng quan trọng có thể biến đổi cách mà các đội tương tác với hệ thống.
Trường Hợp Nghiên Cứu: Thống Nhất Hệ Thống Logs
Hệ thống logs là một ứng cử viên hoàn hảo cho sự thống nhất. Nó xuyên suốt mọi phần của ứng dụng của chúng ta, cho dù là ở phía khách (trình duyệt, React Native) hay ở phía server (Node.js).
Vấn Đề Khi Không Có Sự Thống Nhất
- Không Đồng Nhất:
console.log,console.warn,debug,pino,winston,log4js. Mỗi loại có cú pháp và khả năng riêng. - Bảo Trì: Nếu chúng ta thay đổi hệ thống logs (ví dụ, từ file system sang cloud service), chúng ta phải sửa đổi mã ở nhiều nơi và nhiều nền tảng.
- Đào Tạo: Các lập trình viên mới phải học nhiều công cụ ghi log khác nhau.
- Tập Trung: Khó khăn trong việc hợp nhất và phân tích logs từ nhiều nguồn với định dạng khác nhau.
Giải Pháp: Một Giao Diện Thống Nhất
Chúng ta có thể tạo ra một API ghi log đơn giản và độc lập với cách triển khai. Mục tiêu là các lập trình viên chỉ cần học một giao diện duy nhất.
typescript
// src/infrastructure/log/index.ts
export type LogLevel = "info" | "warn" | "error";
export interface LoggerInterface
extends Record<LogLevel, (message: string) => void> {
info(message: string, context?: Record<string, unknown>): void;
warn(message: string, context?: Record<string, unknown>): void;
error(
message: string,
error?: Error,
context?: Record<string, unknown>
): void;
}
export const log: LoggerInterface =
typeof document !== "undefined"
? (await import("./client")).BrowserLogger
: (await import("./server")).ServerLogger;
Lưu ý: Các môi trường như Bun, Cloudflare, Deno định nghĩa một đối tượng window tương tự như trong trình duyệt, mặc dù không chạy trong trình duyệt web. Điều này giúp duy trì tính tương thích với mã JavaScript.
Với giao diện này, bất kỳ phần nào của mã chúng ta, cho dù ở phía khách hay phía server, chỉ cần nhập log và sử dụng các phương thức của nó.
Sức Mạnh Của Các Bundler: Các Triển Khai Cụ Thể Cho Mỗi Môi Trường
Sự đẹp đẽ của kiến trúc này được tối đa hóa với các bundler hiện đại như Turbopack và Vite. Những công cụ này cho phép chúng ta có nhiều triển khai cho cùng một giao diện, và bundler sẽ đóng gói chỉ mã liên quan cho môi trường cuối cùng (khách hay server).
Điều này đạt được nhờ các kỹ thuật như Tree Shaking hay loại bỏ mã chết. Khi bundler xử lý mã cho khách, nó nhận ra rằng triển khai của logger cho server không bao giờ được sử dụng và do đó, loại bỏ hoàn toàn khỏi bundle cuối cùng, dẫn đến một gói nhỏ hơn và tối ưu hơn. Điều tương tự cũng xảy ra ở phía server, nơi mà triển khai của khách bị loại bỏ.
Triển Khai Cho Trình Duyệt (Khách):
typescript
// src/infrastructure/log/browserLogger.ts
"client only";
import type { LoggerInterface, LogLevel } from "./";
const formatMessage = (
level: LogLevel,
message: string,
context?: Record<string, unknown>
) =>
`[${level.toUpperCase()}] ${message} ${context ? JSON.stringify(context) : ""}`;
export const BrowserLogger: LoggerInterface = {
info(message: string, context?: Record<string, unknown>): void {
console.info(formatMessage("info", message, context));
// Triển khai Sentry/Datadog/Bugsnag
},
warn(message: string, context?: Record<string, unknown>): void {
console.warn(formatMessage("warn", message, context));
// Triển khai Sentry/Datadog/Bugsnag
},
error(
message: string,
error?: Error,
context?: Record<string, unknown>
): void {
console.error(formatMessage("error", message, context), error);
// Triển khai Sentry/Datadog/Bugsnag
},
};
Triển Khai Cho Server (Node.js):
Tại đây, chúng ta có thể sử dụng các thư viện mạnh mẽ như pino hoặc winston.
typescript
// src/infrastructure/log/server.ts
"server only";
import type { LoggerInterface } from "./log";
import pino from "pino";
const pinoLogger = pino({
level: process.env.LOG_LEVEL || "info",
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
});
export const ServerLogger: LoggerInterface = {
info(message: string, context?: Record<string, unknown>): void {
pinoLogger.info(context, message);
},
warn(message: string, context?: Record<string, unknown>): void {
pinoLogger.warn(context, message);
},
error(
message: string,
error?: Error,
context?: Record<string, unknown>
): void {
pinoLogger.error({ err: error, ...context }, message);
},
};
Lợi Ích Rõ Ràng Của Sự Thống Nhất
- Tính Nhất Quán: Mã nguồn có hình thức và hành vi tương tự trên toàn bộ dự án.
- Bảo Trì Đơn Giản Hơn: Các thay đổi trong việc triển khai một API được thực hiện ở một nơi duy nhất.
- Dễ Dàng Kiểm Tra: Các giao diện rõ ràng giúp việc tạo mocks và kiểm tra đơn vị dễ dàng hơn.
- Giảm Đường Cong Học Tập: Các lập trình viên mới chỉ cần học API thống nhất, không phải nhiều công cụ khác nhau.
- Khả Năng Tái Sử Dụng Mã: Logic nghiệp vụ có thể độc lập với nền tảng.
- Tách Biệt: Mã sử dụng API được tách biệt khỏi việc triển khai cụ thể của nó.
Kết Luận
Áp dụng mô hình "Một API Để Quản Lý Tất Cả" cho các chức năng xuyên suốt là một khoản đầu tư mang lại lợi ích lâu dài trong các dự án phức tạp. Nó giảm ma sát, cải thiện chất lượng mã và, nhờ vào khả năng tối ưu của các bundler như Vite và Webpack/Turbopack, cho phép chúng ta duy trì mã sạch và thống nhất mà không làm giảm hiệu suất. Bắt đầu với hệ thống logs là một bước đầu tiên tuyệt vời để chứng minh giá trị của chiến lược này.