Terakhir diperbaharui: Nov 14, 2020
Logger
Di production environment ketika sebuah aplikasi tidak berjalan sebagaimana mestinya maka yang pertama dilihat adalah log file.
Proses menulis informasi pada sebuah log file disebut logging.
Informasi yang ditulis pada log file tersebut adalah semua aktifitas yang terjadi di dalam sebuah aplikasi secara terperinci.
Logging sering digunakan untuk alasan troubleshooting dan performance monitoring tetapi tidak terbatas hanya kepada kedua hal tersebut.
Masalah Utama
Masalah utama dari logging adalah informasi yang ditulis di log file adalah informasi yang kurang berarti.
Jika hal ini terjadi maka proses troubleshooting atau mengatasi bugs bisa terhambat, karena developer yang bertanggung jawab harus menghabiskan lebih banyak waktu dalam menganalisa semua informasi yang ada di log file.
Belum lagi ukuran dari log file bisa menjadi sangat besar, padahal banyak informasi yang kurang dibutuhkan ada didalamnya.
Untuk mengatasi masalah ini tentu saja adalah dengan menentukan informasi apa saja yang harus tulis dan bagaimana kita bisa membuat informasi itu lebih mudah untuk dipahami.
Misalnya kita bisa menggunakan standard dari RFC5424 untuk menulis log:
0 Emergency: system is unusable
1 Alert: action must be taken immediately
2 Critical: critical conditions
3 Error: error conditions
4 Warning: warning conditions
5 Notice: normal but significant condition
6 Informational: informational messages
7 Debug: debug-level messages
Menambah Logger
Ada dua cara menambahkan logger pada aplikasi:
- Menggunakan layanan SaaS seperti Loggly, DataDog, LogRocket atau Splunk
- Menggunakan package seperti Winston, morgan, pino atau ELK Stack
Untuk aplikasi sederhana seperti DinoTes kita bisa gunakan Winston.
Kita akan tambahkan logger di sisi server/api, dengan alasan banyak komunikasi terjadi di sisi server.
Seperti Komunikasi antara client(React.js) dengan api(express.js), komunikasi antara api(express.js) dengan database(mongodb) dll.
Sehingga kemungkinan terjadinya error di sisi server cukuplah besar.
Step by step
Clone repository aplikasi dinotes-api.
- Install package Winston
1$ yarn add winston
- Buat sebuah folder bernama logs untuk menyimpan semua log file
Tempat menyimpan log file bisa dimana saja, pada umumnya ada di dalam folder bernama logs di dalam folder aplikasi, atau disimpan di lokasi seperti /var/log (untuk linux).
Struktur folder:
1dinotes-api2 |--middlewares3 |--node_modules4 |--logs5 ...
- Buat sebuah folder bernama utils untuk menyimpan shared function, dalam hal ini shared function kita yang pertama adalah winston logger.
Struktur folder:
1dinotes-api2 |--middlewares3 |--node_modules4 |--logs5 |--utils6 ...
di dalam folder utils buat sebuah file javascript bernama logger.js yang kemudian kita export agar bisa digunakan di semua tempat di dalam aplikasi.
1dinotes-api2 |--middlewares3 |--node_modules4 |--logs5 |--utils6 |--logger.js7 ...
- Inisialisasi Winston
Untuk menggunakan winston kita bisa menggunakan code dibawah ini:
1const winston = require('winston');23exports.logger = winston.createLogger({4 transports: [new winston.transports.Console()]5});
Kita bisa tambahkan beberapa opsi:
- level
Berikut level logging yang digunakan oleh winston:
1const levels = {2 error: 0,3 warn: 1,4 info: 2,5 http: 3,6 verbose: 4,7 debug: 5,8 silly: 69};
Jika kita tambahkan kode berikut maka winston akan log level mulai dari level info ke bawah (termasuk warn, error)
1level: 'info',
- transporter
Transporter digunakan untuk mengatur lokasi penyimpanan log.
Kita akan tulis semua error ke dalam file bernama error.log di dalam folder logs sedangkan selain itu ditulis ke file bernama combined.log.
1new transports.File({ filename: 'logs/error.log', level: 'error' }),2new transports.File({ filename: 'logs/combined.log' }),
- format
Apa saja yang ditulis oleh log?
Kita bisa log informasi seperti timestamp, endpoint, ip address requester dan keterangan singkat tentang apa yang dilakukan, jika itu sebuah error maka kita log detail dari error message.
Oleh karena itu kita gunakan opsi format:
1...2const { combine, timestamp, printf } = format;34const myFormat = printf(({ level, message, timestamp }) => {5 return `${timestamp} ${level}: ${message}`;6});78...9 format: combine(10 timestamp(),11 myFormat12 ),13...
Sehingga code akhir logger.js adalah
utils/logger.js
1const { format, createLogger, transports } = require('winston');2const { combine, timestamp, printf } = format;34const myFormat = printf(({ level, message, timestamp }) => {5 return `${timestamp} ${level}: ${message}`;6});78exports.logger = createLogger({9 level: 'info',10 format: combine(11 timestamp(),12 myFormat13 ),14 transports: [15 new transports.File({ filename: 'logs/error.log', level: 'error' }),16 new transports.File({ filename: 'logs/combined.log' })17 ]18});
- Update handler dan server.js
Logger akan ditambahkan ke setiap handler.
Update file handler.js:
1const { ObjectId } = require('mongodb');2const { logger } = require('./utils/logger');34exports.addNote = async (req, res, next) => {5 const { notesCollection } = req.app.locals;6 const { title } = req.body;78 try {9 if (!title) {10 logger.error(`${req.originalUrl} - ${req.ip} - title is missing `);11 throw new Error('title is missing');12 }13 // Insert data to collection14 await notesCollection.insertOne(req.body);1516 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully saved`);1718 res.status(200).json('Data successfully saved');19 } catch (error) {20 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);21 next(error);22 }23};2425exports.getAllNotes = async (req, res, next) => {26 const { notesCollection } = req.app.locals;2728 try {29 // find all Notes30 const result = await notesCollection.find().sort({_id:-1}).toArray();3132 logger.info(`${req.originalUrl} - ${req.ip} - All notes retrieved`);3334 res.status(200).json(result);35 } catch (error) {36 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);37 next(error);38 }39};4041exports.getNote = async (req, res, next) => {42 const { notesCollection } = req.app.locals;4344 try {45 // find Notes based on id46 const result = await notesCollection.findOne({ _id: ObjectId(req.params.id) });4748 logger.info(`${req.originalUrl} - ${req.ip} - Notes retrieved`);4950 res.status(200).json(result);51 } catch (error) {52 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);53 next(error);54 }55};5657exports.updateNote = async (req, res, next) => {58 const { notesCollection } = req.app.locals;59 const { title, note } = req.body;6061 try {62 if (!title) {63 logger.error(`${req.originalUrl} - ${req.ip} - title is missing `);64 throw new Error('title is missing');65 }66 // update data collection67 await notesCollection.updateOne(68 { _id: ObjectId(req.params.id) },69 { $set: { title, note } }70 );7172 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully updated`);7374 res.status(200).json('Data successfully updated');75 } catch (error) {76 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);77 next(error);78 }79};8081exports.deleteNote = async (req, res, next) => {82 const { notesCollection } = req.app.locals;8384 try {85 // delete data collection86 await notesCollection.deleteOne({ _id: ObjectId(req.params.id) });8788 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully deleted`);8990 res.status(200).json('Data successfully deleted');91 } catch (error) {92 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);93 next(error);94 }95};
server.js
1...2const { logger } = require('./utils/logger');34...56app.listen(port, () => {7 logger.info(`Server listening at http://localhost:${port}`);8});
- Testing
Jalankan api menggunakan perintah yarn start
atau dari Run & Debug -> Node.js
Dengan menggunakan Postman kita bisa kirim GET request:
Selanjutnya kita bisa melihat ada dua file baru di dalam folder logs.
Isi dari combined.log:
Sedangkan error.log masih kosong dan akan terisi jika terjadi error pada aplikasi.
Logging dengan cara menulis informasi ke log file umumnya dilakukan pada production environment.
Sedangkan pada development environment, logging bisa dilakukan dengan lebih simple seperti cukup dengan menggunakan console tanpa menulisnya di sebuah log file.
Sebagai contoh ketika kita menggunakan winston kita bisa tambahkan code berikut ini:
1if (process.env.NODE_ENV !== 'production') {2 logger.add(new winston.transports.Console({3 format: winston.format.simple(),4 }));5}
Jika aplikasi tidak berjalan di production environment winston akan menampilkan log di console.
Pembahasan selanjutnya adalah menambahkan state management library.