Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/logger and api logger #69

Merged
merged 10 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions bin/www
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const app = require('../server');
const debug = require('debug')('isomercms:server');
const http = require('http');

// Import logger
const logger = require('../logger/logger');

/**
* Get port from environment and store in Express.
*/
Expand Down Expand Up @@ -65,11 +68,11 @@ function onError(error) {
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
logger.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
logger.error(bind + ' is already in use');
process.exit(1);
break;
default:
Expand All @@ -87,5 +90,5 @@ function onListening() {
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
console.log(`isomerCMS app listening on port ${port}`)
logger.info(`isomerCMS app listening on port ${port}`)
}
31 changes: 15 additions & 16 deletions classes/Config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const axios = require('axios');
const validateStatus = require('../utils/axios-utils')

// Import logger
const logger = require('../logger/logger');

// Import error
const { NotFoundError } = require('../errors/NotFoundError')

Expand Down Expand Up @@ -42,25 +45,21 @@ class Config {
}

async update(newContent, sha) {
try {
const endpoint = `https://api.github.com/repos/${GITHUB_ORG_NAME}/${this.siteName}/contents/_config.yml`
const endpoint = `https://api.github.com/repos/${GITHUB_ORG_NAME}/${this.siteName}/contents/_config.yml`

const params = {
"message": 'Edit config',
"content": newContent,
"branch": BRANCH_REF,
"sha": sha
}
const params = {
"message": 'Edit config',
"content": newContent,
"branch": BRANCH_REF,
"sha": sha
}

await axios.put(endpoint, params, {
headers: {
Authorization: `token ${this.accessToken}`,
"Content-Type": "application/json"
}
})
} catch (err) {
console.log(err)
}
headers: {
Authorization: `token ${this.accessToken}`,
"Content-Type": "application/json"
}
})
}
}

Expand Down
122 changes: 57 additions & 65 deletions classes/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,75 +16,67 @@ class Settings {
}

async get() {
try {
// retrieve _config.yml and footer.yml
const configResp = new Config(this.accessToken, this.siteName)

const IsomerDataFile = new File(this.accessToken, this.siteName)
const dataType = new DataType()
IsomerDataFile.setFileType(dataType)

const fileRetrievalArr = [configResp.read(), IsomerDataFile.read(FOOTER_PATH)]

const fileContentsArr = await Bluebird.map(fileRetrievalArr, async (fileOp) => {
const { content, sha } = await fileOp
return { content, sha}
})

// convert data to object form
const configContent = fileContentsArr[0].content
const footerContent = fileContentsArr[1].content

const configReadableContent = yaml.safeLoad(Base64.decode(configContent));
const footerReadableContent = yaml.safeLoad(Base64.decode(footerContent));

// retrieve only the relevant config and index fields
const configFieldsRequired = {
url: configReadableContent.url,
title: configReadableContent.title,
favicon: configReadableContent.favicon,
resources_name: configReadableContent.resources_name,
colors: configReadableContent.colors,
}

// retrieve footer sha since we are sending the footer object wholesale
const footerSha = fileContentsArr[1].sha

return ({ configFieldsRequired, footerContent: footerReadableContent, footerSha })
} catch (err) {
console.log(err)
// retrieve _config.yml and footer.yml
const configResp = new Config(this.accessToken, this.siteName)

const IsomerDataFile = new File(this.accessToken, this.siteName)
const dataType = new DataType()
IsomerDataFile.setFileType(dataType)

const fileRetrievalArr = [configResp.read(), IsomerDataFile.read(FOOTER_PATH)]

const fileContentsArr = await Bluebird.map(fileRetrievalArr, async (fileOp) => {
const { content, sha } = await fileOp
return { content, sha}
})

// convert data to object form
const configContent = fileContentsArr[0].content
const footerContent = fileContentsArr[1].content

const configReadableContent = yaml.safeLoad(Base64.decode(configContent));
const footerReadableContent = yaml.safeLoad(Base64.decode(footerContent));

// retrieve only the relevant config and index fields
const configFieldsRequired = {
url: configReadableContent.url,
title: configReadableContent.title,
favicon: configReadableContent.favicon,
resources_name: configReadableContent.resources_name,
colors: configReadableContent.colors,
}

// retrieve footer sha since we are sending the footer object wholesale
const footerSha = fileContentsArr[1].sha

return ({ configFieldsRequired, footerContent: footerReadableContent, footerSha })
}

async post(payload) {
try {
// setup
const configResp = new Config(this.accessToken, this.siteName)
const config = await configResp.read()
const IsomerDataFile = new File(this.accessToken, this.siteName)
const dataType = new DataType()
IsomerDataFile.setFileType(dataType)

// extract data
const {
footerSettings,
configSettings,
footerSha,
} = payload

// update config object
const configContent = yaml.safeLoad(Base64.decode(config.content));
Object.keys(configSettings).forEach((setting) => (configContent[setting] = configSettings[setting]));

// update files
const newConfigContent = Base64.encode(yaml.safeDump(configContent))
const newFooterContent = Base64.encode(yaml.safeDump(footerSettings))
await configResp.update(newConfigContent, config.sha)
await IsomerDataFile.update(FOOTER_PATH, newFooterContent, footerSha)
return
} catch (err) {
console.log(err)
}
// setup
const configResp = new Config(this.accessToken, this.siteName)
const config = await configResp.read()
const IsomerDataFile = new File(this.accessToken, this.siteName)
const dataType = new DataType()
IsomerDataFile.setFileType(dataType)

// extract data
const {
footerSettings,
configSettings,
footerSha,
} = payload

// update config object
const configContent = yaml.safeLoad(Base64.decode(config.content));
Object.keys(configSettings).forEach((setting) => (configContent[setting] = configSettings[setting]));

// update files
const newConfigContent = Base64.encode(yaml.safeDump(configContent))
const newFooterContent = Base64.encode(yaml.safeDump(footerSettings))
await configResp.update(newConfigContent, config.sha)
await IsomerDataFile.update(FOOTER_PATH, newFooterContent, footerSha)
return
}
}

Expand Down
101 changes: 101 additions & 0 deletions logger/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Imports
const Bluebird = require('bluebird')
const moment = require('moment-timezone')

// Env vars
const { NODE_ENV } = process.env

// AWS
const AWS = require('aws-sdk')

const AWS_REGION_NAME = 'ap-southeast-1'
AWS.config.update({ region: AWS_REGION_NAME })
const awsMetadata = new AWS.MetadataService()
const metadataRequest = Bluebird.promisify(awsMetadata.request.bind(awsMetadata))

// Logging tools
const winston = require('winston')
const WinstonCloudWatch = require('winston-cloudwatch')

// Constants
const LOG_GROUP_NAME = `${process.env.AWS_BACKEND_EB_ENV_NAME}/nodejs.log`
const IS_PROD_ENV = NODE_ENV !== 'LOCAL_DEV' && NODE_ENV !== 'DEV'

// Retrieve EC2 instance since that is the cloudwatch log stream name
async function getEc2InstanceId () {
let id
try {
id = await metadataRequest('/latest/meta-data/instance-id').timeout(1000)
} catch (error) {
// eslint-disable-next-line no-console
console.log(timestampGenerator(), 'Error getting ec2 instance id. This script is probably not running on ec2')
throw error
}
return id
}

function timestampGenerator () {
return moment().tz('Asia/Singapore')
.format('YYYY-MM-DD HH:mm:ss')
}
class CloudWatchLogger {
constructor () {
this._logger = winston.createLogger()
}

async init () {
if (IS_PROD_ENV) {
try {
// attempt to log directly to cloudwatch
const logGroupName = LOG_GROUP_NAME
const logStreamName = await getEc2InstanceId()
const awsRegion = AWS_REGION_NAME

const cloudwatchConfig = {
logGroupName,
logStreamName,
awsRegion,
stderrLevels: ['error'],
format: winston.format.simple(),
handleExceptions: true,
}

this._logger.add(new WinstonCloudWatch(cloudwatchConfig))
} catch (err) {
console.error(`${timestampGenerator()} ${err.message}`)
console.error(`${timestampGenerator()} Failed to initiate CloudWatch logger`)
}
}
}

// this method is used to log non-error messages, replacing console.log
async info (logMessage) {
// eslint-disable-next-line no-console
console.log(`${timestampGenerator()} ${logMessage}`)

if (IS_PROD_ENV) {
try {
await this._logger.info(`${timestampGenerator()} ${logMessage}`)
} catch (err) {
console.error(`${timestampGenerator()} ${err.message}`)
}
}
}

// this method is used to log error messages and write to stderr, replacing console.error
async error (errMessage) {
console.error(`${timestampGenerator()} ${errMessage}`)

if (IS_PROD_ENV) {
try {
await this._logger.error(`${timestampGenerator()} ${errMessage}`)
} catch (err) {
console.error(`${timestampGenerator()} ${err.message}`)
}
}
}
}

const logger = new CloudWatchLogger()

module.exports = logger
29 changes: 29 additions & 0 deletions middleware/apiLogger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Imports
const express = require('express')

// Logger
const logger = require('../logger/Logger')

const apiLogger = express.Router()

apiLogger.use((req, res, next) => {
function isObjEmpty (obj) {
return Object.keys(obj).length === 0
}

// Get IP address
const ipAddress = req.headers['x-forwarded-for']

// Get user GitHub id
let userId
if (req.userId) userId = req.userId

let logMessage = `User ${userId} from IP address ${ipAddress ? `(IP: ${ipAddress})` : undefined } called ${req.method} on ${req.path}`
if (!isObjEmpty(req.query)) {
logMessage += ` with query ${JSON.stringify(req.query)}`
}
logger.info(logMessage)
return next()
})

module.exports = { apiLogger }
8 changes: 6 additions & 2 deletions middleware/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
const express = require('express')
const jwtUtils = require('../utils/jwt-utils')

// Import logger
const logger = require('../logger/logger')

// Import errors
const { AuthError } = require('../errors/AuthError')
const { verify } = require('jsonwebtoken')
Expand All @@ -17,10 +20,11 @@ function noVerify (req, res, next) {
const verifyJwt = (req, res, next) => {
try {
const { isomercms } = req.cookies
const { access_token } = jwtUtils.verifyToken(isomercms)
const { access_token, user_id } = jwtUtils.verifyToken(isomercms)
req.accessToken = access_token
req.userId = user_id
} catch (err) {
console.error('Authentication error')
logger.error('Authentication error')
if (err.name === 'TokenExpiredError') {
throw new AuthError('JWT token has expired')
}
Expand Down
6 changes: 5 additions & 1 deletion middleware/errorHandler.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Import dependencies
const { serializeError } = require('serialize-error')

// Import logger
const logger = require('../logger/logger');

function errorHandler (err, req, res, next) {
console.log(`${new Date()}: ${JSON.stringify(serializeError(err))}`)
logger.info(`${new Date()}: ${JSON.stringify(serializeError(err))}`)

// set locals, only providing error in development
res.locals.message = err.message;
Expand Down
Loading