Skip to content
Permalink
Browse files

feat: add request logger to log http requests

  • Loading branch information...
thetutlage committed Jun 3, 2019
1 parent 4add2ed commit 61ea07297bafdba211aa363fb346fa0eed07225e
@@ -16,6 +16,7 @@
/// <reference path="./http-context.ts" />
/// <reference path="./logger.ts" />
/// <reference path="./middleware-store.ts" />
/// <reference path="./request-logger.ts" />
/// <reference path="./request.ts" />
/// <reference path="./response.ts" />
/// <reference path="./route.ts" />
@@ -0,0 +1,15 @@
/*
* @adonisjs/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare module '@ioc:Adonis/Core/RequestLogger' {
export type RequestLoggerConfigContract = {
logRequests: boolean,
requestLogData?: () => { [key: string]: any },
}
}
@@ -10,6 +10,7 @@
import * as proxyAddr from 'proxy-addr'
import { RequestConfigContract } from '@ioc:Adonis/Core/Request'
import { ResponseConfigContract } from '@ioc:Adonis/Core/Response'
import { RequestLoggerConfigContract } from '@ioc:Adonis/Core/RequestLogger'
import { LoggerConfigContract } from '@ioc:Adonis/Core/Logger'
import Env from '@ioc:Adonis/Core/Env'

@@ -36,7 +37,33 @@ export const appKey: string = Env.getOrFail('APP_KEY') as string
| the config properties to make keep server secure.
|
*/
export const http: RequestConfigContract & ResponseConfigContract = {
export const http: RequestConfigContract & ResponseConfigContract & RequestLoggerConfigContract = {
/*
|--------------------------------------------------------------------------
| Log HTTP requests
|--------------------------------------------------------------------------
|
| Set the value to true, to automatically log every HTTP requests. It is
| okay to log requests in production too.
|
*/
logRequests: true,

/*
|--------------------------------------------------------------------------
| Request log data
|--------------------------------------------------------------------------
|
| Optional, custom function to log custom data with every HTTP request
| log
|
*/
// requestLogData: () => {
// return {
// foo: 'bar',
// }
// }

/*
|--------------------------------------------------------------------------
| Allow method spoofing
@@ -94,9 +121,48 @@ export const http: RequestConfigContract & ResponseConfigContract = {
|--------------------------------------------------------------------------
*/
export const logger: LoggerConfigContract = {
name: 'adonis-app',
messageKey: 'msg',
/*
|--------------------------------------------------------------------------
| Application name
|--------------------------------------------------------------------------
|
| The name of the application you want to add to the log. It is recommended
| to always have app name in every log line.
|
| The `APP_NAME` environment variable is set by reading `appName` from
| `.adonisrc.json` file.
|
*/
name: Env.get('APP_NAME') as string,

/*
|--------------------------------------------------------------------------
| Toggle logger
|--------------------------------------------------------------------------
|
| Enable or disable logger application wide
|
*/
enabled: true,
level: 'trace',

/*
|--------------------------------------------------------------------------
| Logging level
|--------------------------------------------------------------------------
|
| The level from which you want the logger to flush logs.
|
*/
level: 'info',

/*
|--------------------------------------------------------------------------
| Pretty print
|--------------------------------------------------------------------------
|
| It is highly advised not to use `prettyPrint` in production, since it
| can have huge impact on performance
|
*/
prettyPrint: Env.get('NODE_ENV') === 'development',
}

Some generated files are not rendered by default. Learn more.

@@ -87,6 +87,8 @@
"@poppinss/response": "^1.0.8",
"@poppinss/utils": "^1.0.3",
"find-package-json": "^1.2.0",
"on-finished": "^2.3.0",
"pretty-ms": "^5.0.0",
"simple-encryptor": "^3.0.0",
"youch": "^2.0.10",
"youch-terminal": "^1.0.0"
@@ -21,6 +21,7 @@ import { Cors } from '../src/Middleware/Cors'

import { HttpExceptionHandler } from '../src/HttpExceptionHandler'
import { envLoader } from '../src/envLoader'
import { RequestLogger } from '../src/HttpHooks/RequestLogger'

/**
* The application provider that sticks all core components
@@ -152,4 +153,21 @@ export default class AppProvider {
this.$registerEmitter()
this.$registerCorsMiddleware()
}

public boot () {
const logRequests = this.$container.use('Adonis/Core/Config').get('app.http.logRequests', false)
if (!logRequests) {
return
}

/**
* Create a single instance of the logger and hook it as a `before` server hook.
*/
const requestLogData = this.$container.use('Adonis/Core/Config').get('app.http.requestLogData')
const logger = new RequestLogger({ logRequests, requestLogData })

this.$container.with(['Adonis/Core/Server'], (Server) => {
Server.before(logger.onRequest.bind(logger))
})
}
}
@@ -0,0 +1,76 @@
/*
* @adonisjs/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import * as prettyMs from 'pretty-ms'
import { ServerResponse } from 'http'
import * as onFinished from 'on-finished'
import { LoggerContract } from '@ioc:Adonis/Core/Logger'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { RequestLoggerConfigContract } from '@ioc:Adonis/Core/RequestLogger'

/**
* Logs every HTTP request by hooking into HTTP server `before`
* hook.
*/
export class RequestLogger {
constructor (private _config: RequestLoggerConfigContract) {
}

/**
* Returns request handling duration
*/
private _getDuration (startedAt: [number, number]) {
const diff = process.hrtime(startedAt)
return prettyMs(((diff[0] * 1e9) + diff[1]) / 1e6)
}

/**
* Returns log level based upon the response status code
*/
private _getLogLevel (statusCode: number): Extract<keyof LoggerContract, 'info' | 'error' | 'warn'> {
if (statusCode < 400) {
return 'info'
}

if (statusCode >= 400 && statusCode < 500) {
return 'warn'
}

return 'error'
}

/**
* Hooks into `Server.before` and logs every HTTP request.
*/
public async onRequest (ctx: HttpContextContract) {
const start = process.hrtime()
const url = ctx.request.url(true)
const method = ctx.request.method()

/**
* Hook into on finish
*/
onFinished(ctx.response.response, (error: any, res: ServerResponse) => {
const duration = this._getDuration(start)
const logLevel = this._getLogLevel(res.statusCode)
const message = error ? error.message : 'http request'
const payload = { url, method, duration }

try {
if (typeof (this._config.requestLogData) === 'function') {
Object.assign(payload, this._config.requestLogData())
}

ctx.logger[logLevel](payload, message)
} catch (error) {
ctx.logger.fatal(error, `Error in request logger`)
}
})
}
}
@@ -0,0 +1,76 @@
/*
* @adonisjs/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/// <reference path="../adonis-typings/index.ts" />

import * as test from 'japa'
import { FakeLogger } from '@poppinss/logger'
import { HttpContext } from '@poppinss/http-server'
import * as supertest from 'supertest'
import { createServer } from 'http'
import { RequestLogger } from '../src/HttpHooks/RequestLogger'

test.group('Request logger', () => {
test('log http responses with 200 status', async (assert) => {
const logger = new FakeLogger({ enabled: true, level: 'debug', name: 'adonis-app' })

const server = createServer((req, res) => {
const requestLogger = new RequestLogger({ logRequests: true })
const ctx = HttpContext.create('/', {}, req, res)
ctx.logger = logger
requestLogger.onRequest(ctx)
ctx.response.send('')
ctx.response.finish()
})

await supertest(server).get('/')

assert.equal(logger.logs[0].method, 'GET')
assert.equal(logger.logs[0].msg, 'http request')
assert.equal(logger.logs[0].level, logger.levels.values.info)
})

test('log http responses with 400 status', async (assert) => {
const logger = new FakeLogger({ enabled: true, level: 'debug', name: 'adonis-app' })

const server = createServer((req, res) => {
const requestLogger = new RequestLogger({ logRequests: true })
const ctx = HttpContext.create('/', {}, req, res)
ctx.logger = logger
requestLogger.onRequest(ctx)
ctx.response.status(400).send('')
ctx.response.finish()
})

await supertest(server).get('/')

assert.equal(logger.logs[0].method, 'GET')
assert.equal(logger.logs[0].msg, 'http request')
assert.equal(logger.logs[0].level, logger.levels.values.warn)
})

test('log http responses with 500 status', async (assert) => {
const logger = new FakeLogger({ enabled: true, level: 'debug', name: 'adonis-app' })

const server = createServer((req, res) => {
const requestLogger = new RequestLogger({ logRequests: true })
const ctx = HttpContext.create('/', {}, req, res)
ctx.logger = logger
requestLogger.onRequest(ctx)
ctx.response.status(503).send(new Error('a'))
ctx.response.finish()
})

await supertest(server).get('/')

assert.equal(logger.logs[0].method, 'GET')
assert.equal(logger.logs[0].msg, 'http request')
assert.equal(logger.logs[0].level, logger.levels.values.error)
})
})

0 comments on commit 61ea072

Please sign in to comment.
You can’t perform that action at this time.