- Rachel Sensenig
JavaScript developers are familiar withconsole.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
.
// 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.
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.
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.
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.
If you’re in development, you should also see a message logged to the browser console.
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.
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.
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.
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!
Top comments (1)
Implementing a full-stack logging solution for a Vue.js and Node.js app is crucial for effective error monitoring and debugging. To achieve this, you can hire a skilled Node.js developer who specializes in backend development and has experience with logging frameworks like Winston or Bunyan.
A competent Node.js developer will be able to integrate logging libraries into your Node.js backend, configure them to capture relevant log data, and define appropriate log levels for different types of messages. They can also set up centralized logging by sending logs to a dedicated logging service or storing them in a database for easy access and analysis.
Additionally, a capable Node.js developer will collaborate with your Vue.js frontend team to ensure that client-side errors and events are logged as well. They can leverage Vue.js error handling mechanisms and frontend logging libraries like LogRocket or Sentry to capture and transmit relevant log data from the frontend to the backend.
By hiring a skilled Node.js developer, you can establish a robust full-stack logging solution that facilitates efficient debugging, error tracking, and performance optimization for your Vue.js and Node.js application.