How to Implement a Full Stack Logging Solution for a Vue.js and Node.js App
Published: 2023-05-31 10:14:42

_- Rachel Sensenig_


JavaScript developers are familiar with`console.log()`,which writes output to your browser  console. But what if you want to access logging information outside the browser console, say in a persisted data store like a file or database? Persisting logs can be incredibly helpful for understanding your application. It enables you to easily track activity, debug errors, and generally understand what happens and when in your application – all critical for maintaining a healthy, well-functioning app.

As part of the Major League Hacking Fellowship, I was matched with working on Program Equity’s open source app Amplify. For this project, I implemented a full stack logging solution in production to address an intermittent race condition between a back-end operation completing, and a front-end page render. While there are libraries that can handle both back- and front-end logging, I opted to use separate libraries for my front-end and back-end loggers, specifically Winston, a logger for Node.js, for the back-end, and vue-logger-plugin for the front-end.

This article will show you how to connect client and server-side loggers through an API endpoint and persist log messages from your Vue.js front-end in your Node.js back-end.

**Creating a Full Stack Logging Solution**

**Step 1: Creating the API Endpoint**

First, create an API endpoint that will receive logging requests. I opted to use Express and `express. Router()`.Since we’ll be writing to a data store, a creative act, you’ll want to make this a POST route. In the code examples below, this endpoint is `api/event_logger/log`. 

```javascript
// event_logger.js:

const express = require('express')
const router = express.Router()

router.post('/log', async (req, res) => {
  // Destructure request body sent by client
  const { severity, data } = req.body
  
  // Send response back to client
  res.send({severity, data})
})

module.exports = router
```

To test that your endpoint is working as expected, I recommend using Postman, a free developer tool that makes it easy to make API requests to any endpoint with any request body. First, we’ll test whether you can receive and send a log message. Create a JSON object with `{severity, data}` as the “Body” in Postman with a logging level for severity (i.e. “info”) and a test message for data (i.e. “Hello World”). You should see a response of this JSON object when you select “Send” in Postman.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nv669mup4e574tonfjw7.png)





**Step 2: Creating the Backend Logger**

Next, we’ll build a back-end logger. For this case, we’ll use Winston because it is a non-blocking logging library that can log to anything from a plain text file to SASS services. This logger will have two modes, File and Console. Winston refers to these modes as “transports”. File transport will log to a file of your choice – in this case, `error.log` – while Console transport will log to the browser console. We don’t want our end users to see random log messages in the console, so the Console transport will only be enabled in the development environment. You can adjust the format as needed for your project.

```javascript
backend_logger.js:
require('dotenv').config()
const { format, createLogger, transports } = require('winston')
const { timestamp, combine, errors, json, colorize, printf } = format

const logger = createLogger({
  format: combine(timestamp(), errors({ stack: true }), json()),
  // Specify the name of the file to write to here
  transports: [new transports.File({ filename: 'error.log' })]
})

// create log format for when we're in development
const logFormat = printf(({ level, message, timestamp, stack }) => {
  return `${timestamp} ${level}: ${stack || message}`
})

// if we're not in production, then log to the console
if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new transports.Console({
      format: combine(
        // add color depending on log level
        colorize(),
        timestamp({
          format: 'YYYY-MM-DD HH:mm:ss'
        }),
        // include stack-trace
        errors({ stack: true }),
        logFormat
      ),
      defaultMeta: { service: 'user-service' }
    })
  )
}

module.exports = logger
```


In your API endpoint, require the back-end logger that you created and start logging messages to it. In this case, since we’re using Winston, we’ll use `logger.info` to log the message we’ve extracted from the request body. 

```javascript
event_logger.js:
const express = require('express')
const router = express.Router()
const logger = require('../../utilities/backend_logger')

router.post('/log', async (req, res) => {
  // Destructure request body sent by client
  const { severity, data } = req.body
  
  // Send response status of 200 back to client
  res.sendStatus(200)
  
  // send log messages to back-end logger
  logger.info(JSON.stringify({ severity, data }))
})

module.exports = router
```
Same as before, we can use Postman to test that this is working as expected. Send that same POST request from above. In Postman, you should see a response of “OK” because you’re sending back a response status of 200.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/armuopy4317tqm96g6io.png)



If you’re in development, you should also see a message logged to the browser console.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r6ovhysz3w6gvahk7k0i.png)



**Step 3: Creating the Frontend Logger**

I discovered that Winston is difficult to use in a Vue.js front-end due to creating too many polyfills. As a result, you need a separate front-end logger, and a way for it to talk to the back-end.

For the client-side logger, we’ll use `vue-logger-plugin`. I like this library because it allows you to create custom hooks. In this case, we’ll build a custom after hook that leverages another library, axios, to send log messages to your API endpoint.

Axios is a popular JavaScript library that allows you to send HTTP requests from a web browser or Node.js server. It provides an easy way to send asynchronous requests to the server, which means that the client code can continue running while the request is being sent and a response is being awaited. This is important for log messages, which are often sent in the background and shouldn't interrupt the main application logic.

```javascript
frontend_logger.js:
import VueLogger from 'vue-logger-plugin'
import axios from 'axios'

// after hook to send log messages to api endpoint
const ServerLogHook = {
  run: (event) => {
    axios.post('/api/event_logger/log', {
      severity: event.level,
      data: event.argumentArray
    })
  }
}

// define options
const options = {
  enabled: true,
  level: 'debug',
  afterHooks: [ServerLogHook]
}

// export logger with applied options
export default new VueLogger(options)
```

Next, we’ll make our new front-end logger available for use by attaching it to our application. 

```javascript
main.js:
import Vue from 'vue';
import App from './App.vue';

// import front-end logging
import vueLogger from './utilities/frontend_logger'

// for using front-end logger
Vue.use(vueLogger)

new Vue({
  render: (h) => h(App)
}).$mount('#app');
```

After it’s attached,  the `$log` and `$logger` properties will be available globally in your Vue app. An easy way to test is to input `this.$log` or `this.$logger` directly into your Vue instance, so you’ll see these test messages when your Vue app loads.

```javascript
main.js:
new Vue({
  created: function() {
    const testObject = {
      name: 'test',
      value: 'this is a test object'
    }
    // using $log
    this.$log.debug('Test Message', testObject)
    this.$log.info('Test Message', testObject)
    this.$log.warn('Test Message', testObject)
    this.$log.error('Test Message', testObject)
    this.$log.log('Test Message', testObject)
    // using $logger
    this.$logger.debug('Test Message', testObject)
    this.$logger.info('Test Message', testObject)
    this.$logger.warn('Test Message', testObject)
    this.$logger.error('Test Message', testObject)
    this.$logger.log('Test Message', testObject)
  }
})
```

**Step 4: Hooking it all together**

Now that all the pieces are in place, let’s try this out. Fire up  your application in development. You should see the test log messages appear in the console and in the `error.log` file, which we set in the Winston logger. You’re now free to add logging statements anywhere you please in your application on the frontend with `this.$log` and `this.$logger` and on the backend by including and invoking the Winston logger.


**Conclusion**

In summary, you can create a full stack logging solution by creating an API endpoint that passes log messages from a front-end logger to a back-end logger. The back-end logger then saves log messages to a file and optionally prints to the console. For a Vue.js and Node.js application, I used vue-logger-plugin for the front-end and Winston for the back-end. Use Postman to test your API endpoint along the way.

After the full stack logger is set up, you can customize according to your needs. The $log or $logger global properties can be used anywhere in your Vue app, including child components. With Winston, you can create additional custom transports to save information to your storage place of choice, such as a database.

Try this out, and please leave a comment of what you create if you do!