Handling Audit Logging in Node.js
Published: 2024-05-12 16:44:08


Audit logs are records on the system that provide evidence of the sequence of activities that have been done at a given time.They help in monitoring the series of activities been done.

In this article, we will be going through step by step process of handling audit logs in Node.js. 

#### Pre-requisites:

Before proceeding in this article, it is good to have the following:

- Basic knowledge working with [Node.js](https://nodejs.org/).
- Basic knowledge working with [Express.js](https://expressjs.com/).
- [Postman](https://www.postman.com/downloads/) installed on your computer.
- [MongoDB](https://www.mongodb.com/) and [MongoDB Compass](https://www.mongodb.com/products/tools/compass) installed on your computer.

#### Overview:

- [Setting up the application](#setting-up-the-application).
- [Setting up the models](#setting-up-the-models).
- [Setting up the logger middleware](#setting-up-the-logger-middleware).
- [Testing the application](#testing-the-application)

#### Setting up the application:

- Proceed to your preferred working directory.
- From the terminal, initialize an NPM project using the default configurations:

  ```bash
  npm init -y
  ```
- We will be using the below packages:

  - [Express.js](https://www.npmjs.com/package/express): For setting up the web server.
  - [Mongoose](https://www.npmjs.com/package/mongoose): ORM, for connecting to MongoDB.
  - [Dotenv](https://www.npmjs.com/package/dotenv): For loading environmental variables.

  Install the packages by running this command:

  ```bash
  npm run install express mongoose dotenv
  ```
- Once the packages, have been installed, the next step is to set up the entry point file of the application.
- In the project directory, create a file named `app.js` which will be the entry point file.
- In the *app.js* file:

  - Start by importing the necessary packages:
 
    ```js
    const express = require('express');
    const dotenv = require('dotenv');
    const {join} = require('path');
    ```

  - Define the path where *.env* file will be:

    ```js
    dotenv.config({ path: join(__dirname,'.env') });
    ```

  - Initialize the application server:

    ```js
    const app = express();
    ```

  - Accept `JSON` payloads for `POST` requests:

    ```js
    app.use(express.json());
    ```

  - Set up dummy `GET`, `POST`, and `PUT` requests:

    ```js
    // GET
    app.get("/", (req,res) => res.json({
       success:true,
       message:"Get request sent successfully"
    }));

    // POST
    app.post("/", (req,res) => res.json({
       success:true,
       message:"Post request sent successfully"
    }));

    // PUT
    app.put("/:pet", (req,res) => res.json({
       success:true,
       message:"Put request sent successfully"
    }));
    ```
    From the above routes, we are simply returning a `JSON` response. We will use the routes to derive the audit logs. 

  - Start the application server:

    ```js
    app.listen(process.env.PORT,() => {
        console.log("Server started and running on port "+process.env.PORT)
    });
    ```

Since we are already calling an environmental variable, it is about time we create a *.env* file and specify the port.

In the project directory, create a *.env* file. In it, specify the PORT as below:

```bash
PORT=5000
```

The next step is to set up a model that will be holding the audit logs data.

#### Setting up the models

For our case, we will only have one model `AuditTrail`. In this model, we will store the following:
 
- The URL.
- The activity / name of action been done.
- The request params.
- The request query.
- The request payload.
- The returned response.
- Timestamp.

To set up the model, create a directory named `models` on the project root directory. Inside the `models` directory, create a file named `AuditTrail.js`. 

In the *AuditTrail.js*:

 - Start by importing the *Schema*, and *model* from `mongoose`:

   ```js
   const {Schema,model} = require('mongoose');
   ```
 - Define the audit trail schema as below:

    ```js
    const auditTrail = new Schema({
       url:{
          type:Schema.Types.String,
          required:true
       },
       activity:{
          type:Schema.Types.String,
          required:true
       },
       params:{
          type:Schema.Types.String,
          required:true
       },
       query:{
          type:Schema.Types.String,
          required:true
       },
       payload:{
          type:Schema.Types.String,
          required:true
       },
       response:{
          type:Schema.Types.String,
          required:true
       }
    },{
       timestamps:true
    });
    ```
  - Export the model:

    ```js
    module.exports = model('auditTrail',auditTrail);
    ```
With the model setup, the next step is to create a functionality to connect to the database. In the project root directory, create a directory named: *utils*. Inside the *utils* directory, create a file named: *db.js*.

In the *db.js* file:

- Import the *mongoose* package:

  ```js
  const mongoose = require("mongoose");
  ```

- Configure the path to the *.env* file:

  ```js
  const {join} = require("path");
  require("dotenv").config({
    path:join(__dirname,'..','.env')
  });
  ```
- Define a function for connecting to the database:

  ```js
  exports.connectDb = async () => {
    try{
        await mongoose.connect(process.env.DB_URL);
        console.log(">>>>>>>> Db is connected successfully >>>>>>>>>>");
    }catch(error){
        console.log(">>>>>>> an error occurred connecting to db >>>>>>>>>>>");
        console.log(error.message);
     }
  }
  ```

Since we are accessing the *DB_URL* variable from the above function, we will need to edit the *.env* file and specify it:

```bash
DB_URL=mongodb://127.0.0.1:27017/auditTrails
PORT=4000
```

To be able to interact with the model we just created, we need to import the *connectDb* function to the entry point file and ensure the function is called when application is been started.

Edit the *app.js* as follows:

- Import the *connectDb* function:

  ```js
  const {connectDb} = require("./utils/db");
  ```

- Call the function when the application is been started:

  ```js
  app.listen(process.env.PORT,async () => {
    await connectDb();
    console.log("Server started and running on port "+process.env.PORT)
  });
  ```

Now that we have the database connection set up, the next step is to set up a middleware that will be recording audit logs whenever any route is been hit.

#### Setting up the logger middleware

In the project root directory, create a directory named *middlewares*. In it, create a file named: *auditTrail.js*.

In the *auditTrail.js*:

- Import the model:

  ```js
  const AuditTrail = require('../models/AuditTrail');
  ```
- Define the methods in the routes and their actions:

  ```js
  const methodMappers = {
    "GET":"Fetching",
    "POST":"Adding",
    "PUT":"Updating",
    "DELETE":"Deleting"
  }
  ```

- Define the middleware:

  ```js
  exports.logAuditTrails = (req,res,next) => {
    try{
        const originalJson = res.json;
        res.json = async function (body){
            await AuditTrail.create({
                url:req.originalUrl,
                activity:methodMappers[req.method] + ' ' + req.originalUrl.split("/")[req.originalUrl.split("/").length - 1] || "",
                params:JSON.stringify(req.params),
                query:JSON.stringify(req.query),
                payload:JSON.stringify(req.body),
                response:JSON.stringify(body)
            });
            return originalJson.call(this,body);
        }
        next();
    }catch(error){
        console.log(">>>>> an error occurred logging audit trail >>>>>>>>");
        console.log(error.message);
        next();
    }
  }
  ```

  From above, we are basically embedding a function on the *res.json* function. In it, we are fetching the data from the  *request* and *response* body to create the audit trail. After saving the record, we are calling the *next()* so as the original response can be shown to the user.

  In the event that there is an error creating the audit trail, the error is logged and the response returned to the user. This way, the user will interact with the application as normal.

With the middleware set up, we need to make sure that it is applied on all the routes. To do so, we will edit the entry point file *app.js* as follows:

- Import the middleware function:

  ```js
  const {logAuditTrails} = require("./middlewares/auditTrail");
  ``` 
- Before defining the routes, add the following snippet : 

  ```js
  app.use(logAuditTrails);
  ```

With that set up, we are ready to begin testing the application.

#### Testing the application

To start the Node.js server, we will need to add a script for that in the *package.json* file.

Edit your *package.json* by adding the below key value pair on the *scripts* section:

```json
"start":"node app.js"
```

Start the server by running the below command:

```bash
npm run start
```

Once the server is started, open up *Postman* and send the following requests: 

- A *GET* request to *http://localhost:4000/*.
- A *POST* request to *http://localhost:4000/*. Make sure you include some *JSON* payload on it.
- A *PUT* request to *http://localhost:4000/kitty*: Make sure you include some *JSON* payload too.

If all is successful and no error has been logged on the console, it means that the audit trails have been created on the database. Simply open up *MongoDB Compass* on *mongodb://127.0.0.1:27017* and your data will be under the *auditTrails* database.

If an error occurred, kindly revisit the steps.

A take home task alongside this is to create an endpoint for fetching the audit trails from the database. This way, to retrieve your records you won't need to be using MongoDB Compass you will simply be sending a request via Postman.

For your reference, you can access the code from this [Github Repository](https://github.com/mwangiKibui/nodejs-audit-logs)