Search by

    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:

    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.

    1. Install package Winston
    1$ yarn add winston
    1. 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-api
    2 |--middlewares
    3 |--node_modules
    4 |--logs
    5 ...
    1. Buat sebuah folder bernama utils untuk menyimpan shared function, dalam hal ini shared function kita yang pertama adalah winston logger.

    Struktur folder:

    1dinotes-api
    2 |--middlewares
    3 |--node_modules
    4 |--logs
    5 |--utils
    6 ...

    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-api
    2 |--middlewares
    3 |--node_modules
    4 |--logs
    5 |--utils
    6 |--logger.js
    7 ...
    1. Inisialisasi Winston

    Untuk menggunakan winston kita bisa menggunakan code dibawah ini:

    1const winston = require('winston');
    2
    3exports.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: 6
    9};

    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;
    3
    4const myFormat = printf(({ level, message, timestamp }) => {
    5 return `${timestamp} ${level}: ${message}`;
    6});
    7
    8...
    9 format: combine(
    10 timestamp(),
    11 myFormat
    12 ),
    13...

    Sehingga code akhir logger.js adalah

    utils/logger.js

    1const { format, createLogger, transports } = require('winston');
    2const { combine, timestamp, printf } = format;
    3
    4const myFormat = printf(({ level, message, timestamp }) => {
    5 return `${timestamp} ${level}: ${message}`;
    6});
    7
    8exports.logger = createLogger({
    9 level: 'info',
    10 format: combine(
    11 timestamp(),
    12 myFormat
    13 ),
    14 transports: [
    15 new transports.File({ filename: 'logs/error.log', level: 'error' }),
    16 new transports.File({ filename: 'logs/combined.log' })
    17 ]
    18});
    1. Update handler dan server.js

    Logger akan ditambahkan ke setiap handler.

    Update file handler.js:

    1const { ObjectId } = require('mongodb');
    2const { logger } = require('./utils/logger');
    3
    4exports.addNote = async (req, res, next) => {
    5 const { notesCollection } = req.app.locals;
    6 const { title } = req.body;
    7
    8 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 collection
    14 await notesCollection.insertOne(req.body);
    15
    16 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully saved`);
    17
    18 res.status(200).json('Data successfully saved');
    19 } catch (error) {
    20 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);
    21 next(error);
    22 }
    23};
    24
    25exports.getAllNotes = async (req, res, next) => {
    26 const { notesCollection } = req.app.locals;
    27
    28 try {
    29 // find all Notes
    30 const result = await notesCollection.find().sort({_id:-1}).toArray();
    31
    32 logger.info(`${req.originalUrl} - ${req.ip} - All notes retrieved`);
    33
    34 res.status(200).json(result);
    35 } catch (error) {
    36 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);
    37 next(error);
    38 }
    39};
    40
    41exports.getNote = async (req, res, next) => {
    42 const { notesCollection } = req.app.locals;
    43
    44 try {
    45 // find Notes based on id
    46 const result = await notesCollection.findOne({ _id: ObjectId(req.params.id) });
    47
    48 logger.info(`${req.originalUrl} - ${req.ip} - Notes retrieved`);
    49
    50 res.status(200).json(result);
    51 } catch (error) {
    52 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);
    53 next(error);
    54 }
    55};
    56
    57exports.updateNote = async (req, res, next) => {
    58 const { notesCollection } = req.app.locals;
    59 const { title, note } = req.body;
    60
    61 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 collection
    67 await notesCollection.updateOne(
    68 { _id: ObjectId(req.params.id) },
    69 { $set: { title, note } }
    70 );
    71
    72 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully updated`);
    73
    74 res.status(200).json('Data successfully updated');
    75 } catch (error) {
    76 logger.error(`${req.originalUrl} - ${req.ip} - ${error} `);
    77 next(error);
    78 }
    79};
    80
    81exports.deleteNote = async (req, res, next) => {
    82 const { notesCollection } = req.app.locals;
    83
    84 try {
    85 // delete data collection
    86 await notesCollection.deleteOne({ _id: ObjectId(req.params.id) });
    87
    88 logger.info(`${req.originalUrl} - ${req.ip} - Data successfully deleted`);
    89
    90 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');
    3
    4...
    5
    6app.listen(port, () => {
    7 logger.info(`Server listening at http://localhost:${port}`);
    8});
    1. Testing

    Jalankan api menggunakan perintah yarn start atau dari Run & Debug -> Node.js

    Dengan menggunakan Postman kita bisa kirim GET request:

    postman

    Selanjutnya kita bisa melihat ada dua file baru di dalam folder logs.

    Isi dari combined.log:

    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.