Xin chào tất cả các bạn, mình là Quân, đã bao giờ một ngày đẹp trời, bạn nhận được một yêu cầu từ khách hàng đó là viết cho họ một cái Service bằng Node.js có nhiệm vụ quan sát, lắng nghe một dự án khác nếu server bên dự án đó có vấn đề gì thì bắn thông báo lỗi về cho quản trị viên ngay lập tức chưa? Hôm nay chúng ta sẽ cùng nhau đi giải quyết bài toán thực tế này nhé.
“Bài này nằm trong loạt bài Lập Trình Nodejs từ cơ bản đến nâng cao trên trang blog chính thức trungquandev.com“
Những nội dung có trong bài:
- Tản mạn phân tích bài toán.
- Quan sát, lắng nghe những hành động của một thư mục.
- Quan sát, lắng nghe những hành động của một tệp tin cụ thể.
1. Tản mạn phân tích bài toán
Okay, đề bài thì đã cho như trên kia rồi, bây giờ chúng ta đặt giấy bút ra nghiên cứu một chút xem những đầu việc cần làm là gì nhé:
Đầu tiên, clear rõ ràng luôn cho các bạn đó là việc đọc thông báo lỗi bản chất là quan sát cái file log trong dự án, lắng nghe xem file đó có xuất hiện hoặc thay đổi hay không, rồi lấy thông tin từ file đó ra.
Thứ hai, bài toán yêu cầu một service Node.js chạy riêng độc lập với dự án chính, (mình giả sử trong trường hợp này “cái dự án chính” là PHP – Framework Laravel).
Đối với dự án dùng Laravel nếu có lỗi, nội dung lỗi sẽ được sinh ra và ghi vào các file log theo từng ngày trong thư mục /storage/logs/, mình sẽ target vào một file có tên là laravel.log.
Lưu ý với bất kỳ một dự án hay framework nào khác thì các bạn cũng phải cần hiểu và áp dụng chính xác nơi lưu trữ file log của dự án đó nhé.
Luyên thuyên nhiều quá, vậy tóm gọn lại chúng ta phải làm gì?
Đơn giản là các bạn hãy lướt xuống dưới và đọc tiếp phần 2 + 3 nhé (─‿‿─)
2. Quan sát, lắng nghe những hành động của một thư mục
Đây là trường hợp mà giả sử khách hàng yêu cầu chúng ta giám sát một đường dẫn thư mục (directory), nếu như có một file error.log xuất hiện bên trong thư mục đó chẳng hạn, thì đọc file đó, lấy ra nội dung lỗi sau đó xóa luôn file đó đi.
Quy trình xử lý cứ xoay vòng như vậy, file lỗi xuất hiện, đọc xong xóa rồi nó lại xuất hiện, lại đọc xong xóa tiếp rồi lại xuất hiện…..(cứ thế đi mãi đi mãi không về =)))
Vậy thì làm thế nào để xử lý được cái quy trình trên trong Node.js bây giờ?
Nếu như các bạn search google một hồi thì sẽ nhận được một vài ideas từ stackoverflow như dùng hàm fs.watch, fs.watchFile của module lõi fs trong Node.js. Mình đã thử làm với chúng nó và nó có một số nhược điểm như sau:
- Thường xuyên bị nhận thông báo file thay đổi 2 lần trong khi mình chỉ chỉnh sửa file và lưu đúng một lần. Mình phải dùng vài tips check md5 của file với kiểm tra datetime thì mới xử lý được.
- Đổi tên file cũng bị nhận thông báo khi đang lắng nghe sự kiện update nội dung bên trong file.
- “Does not provide an easy way to recursively watch file trees”: theo mình đang hiểu là không hỗ trợ lắng nghe đệ quy cây thư mục, việc lắng nghe một thư mục là khó khăn.
- Ngoài ra còn một vài lỗi trên hệ điều hành MacOS rồi cả việc sử dụng chúng thì tiêu tốn nhiều CPU nữa….vv
Những thông tin trên kia có những cái thực tế mình làm và nhận thấy, ngoài ra còn những cái khác thì mình lấy ở đâu? Có xác thực hay không?
Câu trả lời nằm ngay sau đây, giới thiệu đến các bạn module rất nổi tiếng có tên là chokidar, ở thời điểm hiện tại mình vào xem thì lượt dowload hàng tuần của nó lên đến hơn 11 triệu, số star trên github cũng rất khủng, hơn 5,5k:
NPM: https://www.npmjs.com/package/chokidar
Github: https://github.com/paulmillr/chokidar
Và trong bài viết hôm nay, mình sẽ hướng dẫn các bạn sử dụng thằng Chokidar này để xử lý bài toán của chúng ta nhé.
– Bắt tay vào code thôi, mình sẽ khởi tạo cấu trúc cho ứng dụng Node.js và project laravel-example để chứa các file logs của chúng ta như sau:
laravel-example
storage
logs
error.log
laravel.log
nodejs-monitoring-files
modules
observe.js
package.json
server.js
Bạn nào chưa rành việc tạo project Node.js thì có thể tham khảo lại bài khởi tạo ứng dụng Node.js trong loạt bài Lập Trình Nodejs từ cơ bản đến nâng cao trên trang blog chính thức trungquandev.com của mình nhé.
“Lưu ý nhé, ở trên kia mình có 2 projects, một cái laravel-example bên trong nó chứa thư mục và các file logs mục đích để test. Và cái còn lại là project Node.js mà tiếp theo đây chúng ta làm việc.”
Các bạn install cho mình 2 packages là chokidar và fs-extra vào dự án bằng lệnh dưới đây: (fs-extra mình sẽ giới thiệu ở một bài khác, các bạn cứ hiểu nó cũng giống module fs thôi nhưng nó hỗ trợ cho chúng ta viết code xử lý bất đồng bộ với cú pháp async – await rất xịn nhé.)
npm i --save chokidar fs-extra
Phiên bản mới nhất của chokidar hiện tại mình sử dụng là “3.0.1“, còn fs-extra là “8.1.0“
Chúng ta sẽ bắt đầu viết code xử lý lắng nghe cho file Observe.js như sau nhé:
/** * Created by trungquandev.com's author on 02/07/2019. * Observe.js */ const chokidar = require("chokidar"); const EventEmitter = require("events").EventEmitter; const fsExtra = require("fs-extra"); let debug = console.log.bind(console); class Observe extends EventEmitter { constructor() { super(); } /** * Function responsible for watching a folder * @param {string} targetFolder */ watchFolder(targetFolder) { try { debug(`[${new Date().toLocaleString()}] Watching for folder changes on: ${targetFolder}`); // initialize watcher let watcher = chokidar.watch(targetFolder, {persistent: true}); // listen when a file has been added watcher.on("add", async (filePath) => { // if the new file's name is exactly error.log if (filePath.includes("error.log")) { debug(`[${new Date().toLocaleString()}] ${filePath} has been added.`); // Read content of new file let fileContent = await fsExtra.readFile(filePath); // emit an event when new file has been added this.emit("new-file-has-been-added", {message: fileContent.toString()}); // remove file error.log await fsExtra.unlink(filePath); debug(`[${new Date().toLocaleString()}] ${filePath} has been removed.`); } }); } catch (error) { debug(error.toString()); } } } module.exports = Observe;
Ở trên mình cũng đã comment cơ bản rõ ràng hết trong các dòng code rồi (nếu mình có viết sai tiếng anh xin phép các bạn comment dưới bài nhắc mình nhé), bây giờ mình sẽ giải thích rõ ràng code từ trên xuống dưới nếu như các bạn đọc code mà chưa hiểu nha:
- File Observe.js ở trên là nơi chúng ta viết các function chức năng cho việc lắng nghe file và thư mục.
- Đầu tiên là nạp các module cần thiết: chokidar, events, fs-extra.
- Cái biến debug các bạn cứ hiểu đơn giản nó cũng không khác gì console.log, mình tạo ra nó để code phía dưới mỗi lần muốn log thông báo ra console cho nó ngắn gọn hơn thôi.
- Mình khởi tạo một class Observe kế thừa EventEmitter với hàm constructor gọi function super(); mục đích là để class Observer của chúng ta có thể emit được sự kiện. Kỹ thuật này mình có giải thích rất rõ ràng trong phần 3 của bài Đào sâu module Events trong Node.js này rồi các bạn có thể tham khảo nếu chưa rõ nha.
- Tiếp đến là function watchFolder, function này chịu trách nhiệm lắng nghe một đường dẫn thư mục được truyền vào (targetFolder)
- Bên trong watchFolder mình khởi tạo biến watcher bằng hàm chokidar.watch, tham số đầu tiên truyền vào là đường dẫn thư mục (targetFolder), tham số thứ 2 là object {persistent: true} mục đích để thông báo cho thằng watcher biết là dù cho các tệp đã được xem rồi thì nó vẫn nên tiếp tục chạy.
- Sau đó thằng watcher này lắng nghe một sự kiện là “add”, có nghĩa là cứ mỗi khi có một file nào đó được thêm vào folder mà nó đang lắng nghe thì code sẽ chạy vào đây.
- Sau khi sự kiện “add” được kích hoạt, mình kiểm tra tiếp filePath nếu tồn tại chuỗi string là error.log, có nghĩa là file mới được thêm vào có tên là error.log
- Đọc nội dung của file error.log bằng fsExtra.readFile()
- Emit sự kiện “new-file-has-been-added” kèm nội dung message là nội dung file lấy được ở trên.
- Xóa file error.log bằng fsExtra.unlink()
- Cuối cùng luôn là bước nhẹ nhàng nhưng không kém phần quan trọng, export cái module Observe ra ngoài để bên server.js có thể sử dụng.
Bây giờ là code cho file server.js
/** * Created by trungquandev.com's author on 02/07/2019. * server.js */ const ObserveClass = require("./modules/Observe"); // Init Observe object let Observe = new ObserveClass(); // Define folder to watching, in real project, you should put it in file config or env let targetFolder = "../laravel-example/storage/logs"; // Listen event new file has been added Observe.on("new-file-has-been-added", (logData) => { // In this step, you can do anything you want, like to push alert message to chatwork, slack...vv // I just print error message to console console.log(logData.message); }); // Start watching folder... Observe.watchFolder(targetFolder);
- Trong file server.js mình nạp class Observe ở trên, sau đó khởi tạo new đối tượng Observe.
- Định nghĩa đường dẫn đến thư mục “targetFolder”, lưu ý trong dự án thực tế các bạn nên đưa biến targetFolder này vào file config hoặc biến môi trường env của dự án nhé.
- Sử dụng Observe.on để lắng nghe khi nào sự kiện “new-file-has-been-added” của chúng ta được kích hoạt thì log nó ra console để nhìn. Vì làm ví dụ nên mình chỉ log ra như vậy, đối với mỗi dự án thì trong này các bạn sẽ viết thêm code xử lý như là bắn thông báo về slack, về chatwork…vv đại loại là làm sao mà người quản lý project có thể biết được là có lỗi nhé.
- Cuối cùng là gọi đến function watchFolder, và truyền vào đường dẫn tới thư mục muốn quan sát.
Bây giờ chạy service Node lên:
node server.js
Nếu trong targetFolder của các bạn đã có file error.log thì nó sẽ log nội dung ra như hình dưới mình demo, và file error.log cũng sẽ bị xóa luôn.
Các bạn có thể tiếp tục tái hiện lại quá trình này bằng cách tạo một file error.log xong copy paste vào thư mục đang watching và xem log nhé.
Vậy là giải quyết xong việc lắng nghe thư mục, tiếp theo trong phần 3 chúng ta sẽ đi xử lý lắng nghe một file cụ thể.
3. Quan sát, lắng nghe những hành động của một tệp tin cụ thể
Trường hợp này khác phần 2 một chút, lúc này khách hàng không muốn chúng ta xóa file error.log đi nữa, mà lại muốn chúng ta quan sát chỉ riêng file đó, nếu như có lỗi => nội dung file error.log được cập nhật, thì lấy ra đúng phần nội dung cập nhật mới nhất để thông báo thôi.
Quy trình xử lý sẽ xoay vòng như thế này, quan sát file nếu nó được cập nhật, lấy phần nội dung mới cập nhật ra rồi lại quan sát tiếp…….
Với yêu cầu như trên, để giải quyết việc đọc nội dung mới cập nhật của file, các bạn cài thêm cho mình module có tên là read-last-lines như sau:
npm i --save read-last-lines
Chúng ta sẽ viết tiếp chức năng lắng nghe file vào trong Observe.js (mình sẽ lược đi những đoạn code đã có trong phần 2 nhé, lúc làm thì các bạn cứ giữ nguyên, chỉ thêm code mới vào thôi)
/** * Created by trungquandev.com's author on 02/07/2019. * Observe.js */ // require module read-last-lines const readLastLines = require("read-last-lines"); class Observe extends EventEmitter { /** * Function responsible for watching a file * @param {string} targetFile */ watchFile(targetFile) { try { debug(`[${new Date().toLocaleString()}] Watching for file changes on: ${targetFile}`); // initialize watcher let watcher = chokidar.watch(targetFile, {persistent: true}); // listen when a file has been added watcher.on("change", async (filePath) => { debug(`[${new Date().toLocaleString()}] ${filePath} has been updated.`); // Get update content of file, in this case is one line let updateContent = await readLastLines.read(filePath, 1); // emit an event when the file has been updated this.emit("file-has-been-updated", {message: updateContent}); }); } catch (error) { debug(error.toString()); } } }
Phần code này cũng đơn giản:
- Lần này thằng watcher lúc khởi tạo sẽ nhận vào tham số là một file cụ thể sau đó lắng nghe sự kiện “change” watcher.on(“change”)
- Chúng ta sẽ lấy nội dung update của file bằng module read-last-line, thông thường khi có lỗi thì file sẽ được cập nhật từng dòng một và lưu lại nên mình truyền vào tham số là 1 ở đoạn code này để lấy ra nội dung dòng cuối cùng của file: readLastLines.read(filePath, 1);
- Sau đó là emit một sự kiện có tên là “file-has-been-updated” cho bên server.js lắng nghe.
Code cho file server.js chúng ta sẽ viết tiếp đoạn dưới đây
/** * Created by trungquandev.com's author on 02/07/2019. * server.js */ // Define file to watching, in real project, you should put it in file config or env let targetFile = "../laravel-example/storage/logs/laravel.log"; // Listen event file has been updated Observe.on("file-has-been-updated", (logData) => { // In this step, you can do anything you want, like to push alert message to chatwork, slack...vv // I just print error message to console console.log(logData.message); }); // Start watching file... Observe.watchFile(targetFile);
Đoạn code trên chắc mình cũng không cần giải thích gì dài dòng thêm nữa nhé, nó tương tự trong phần 2, chỉ khác là chúng ta quan sát cái file laravel.log thay vì cả cái thư mục logs.
Xong, kết quả mỗi khi mình cập nhật dữ liệu vào file có tên là laravel.log:
Mình có để full source code của bài hôm nay ở repo này cho các bạn tham khảo nhé:
https://github.com/trungquandev/nodejs-monitoring-file-folder
Vậy là bài hôm nay mình đã hướng dẫn hoàn thiện cho các bạn về ý tưởng và cách triển khai cho việc quan sát, lắng nghe một file hoặc một folder trong Node.js rồi nhé.
Cảm ơn các bạn đã dành thời gian đọc bài viết.
Xin chào và hẹn gặp lại các bạn ở những bài viết tiếp theo.
Best Regards – Trung Quân – Green Cat
Tham khảo kiến thức:
https://thisdavej.com/how-to-watch-for-files-changes-in-node-js/
https://www.npmjs.com/package/chokidar
https://www.npmjs.com/package/read-last-lines
“Thanks for awesome knowledges.”