Home Reference Source

src/middlewares/express.js

// import transform from '../transform'
import compose from 'composable-middleware'
import addRequestId from 'express-request-id'
import bodyParser from 'body-parser'
import HTTP from '../contexts/http'
import { HTTPRequest, HTTPResponse } from '../events'
import log from '../log'
import config from '../config'

/**
 * The express middleware takes care of automatically logging
 * each http event with the appropriate context events attached.
 *
 * This middleware is composed of three separate middlewares:
 * - `addRequestId` automatically attaches a unique uuid to every request
 * - `bodyParser` allows parsing of JSON encoded request bodies
 * - `expressMiddleware` automatically logs http events to timber
 *
 * @param {object} [options] - An object with configuration options
 * @param {object} [options.logger] - A custom logger to log http events to (usually either: console, winston, or bunyan)
 * @param {boolean} [options.capture_request_body] - Whether the http request body data will be captured (off by default)
 * @param {boolean} [options.combine_http_events] - If true, HTTPRequest and HTTPResponse events will be combined in a single log message (off by defaut)
*/
const expressMiddleware = ({ ...options }) => {
  // If a custom logger was provided, use it to log http events
  if (options.logger) {
    config.logger = options.logger
  }
  return compose(
    addRequestId(),
    bodyParser.json(),
    (req, res, next) => {
      // save a reference of the start time so that we can determine
      // the amount of time each http request takes
      req.start_time = new Date().getTime()

      // destructure the request object for ease of use
      const {
        headers: { host, ...headers },
        method,
        id: request_id,
        path,
        protocol: scheme,
        body: reqBody,
        connection,
      } = req

      // determine the ip address of the client
      // https://stackoverflow.com/a/10849772
      const remote_addr = headers['x-forwarded-for'] || connection.remoteAddress

      // send the request body if the capture_request_body flag is true (off by default)
      // and the request body is not empty
      let body = options.capture_request_body && Object.keys(reqBody).length > 0
        ? JSON.stringify(reqBody)
        : undefined

      // create the HTTP context item
      const http = new HTTP({
        method,
        path,
        request_id,
        remote_addr,
      })

      // add the http context information to the metadata object
      const metadata = {
        context: {
          http,
        },
      }

      const http_request = new HTTPRequest({
        direction: 'incoming',
        body,
        host,
        path,
        request_id,
        scheme,
        method,
      })

      // add the http_request event to the metadata object
      metadata.event = { http_request }


      // Override the response end event
      // This event will send the http_client_response event to timber
      // If combine_http_events is true, this will be the only log generated
      const end = res.end
      res.end = (chunk, encoding) => {
        // Emit the original res.end event
        res.end = end
        res.end(chunk, encoding)

        // destructure the response object for ease of use
        const { body: resBody, statusCode: status } = res

        // calculate the duration of the http request
        const time_ms = new Date().getTime() - req.start_time

        // send the response body if the capture_response_body flag is true (off by default)
        body = options.capture_response_body ? JSON.stringify(resBody) : undefined

        const http_response = new HTTPResponse({
          direction: 'outgoing',
          request_id,
          time_ms,
          status,
          body,
        })

        // If we're combining http events, append the request event
        if (options.combine_http_events) {
          http_response.request = http_request
        }

        // add the http_response event to the metadata object
        metadata.event = { http_response }

        const message = options.combine_http_events
          ? `${method} ${host}${path} - ${status} in ${time_ms}ms`
          : http_response.message()

        // log the http response with metadata
        log('info', message, metadata)
      }

      // If we're not combining http events, log the http request
      if (!options.combine_http_events) {
        log('info', http_request.message(), metadata)
      }
      next()
    }
  )
}

export default expressMiddleware