Skip to content

Commit

Permalink
Merge 97d1823 into fc31921
Browse files Browse the repository at this point in the history
  • Loading branch information
SimoneBergonzi committed Jan 12, 2023
2 parents fc31921 + 97d1823 commit ed4a080
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 9 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Unreleased

### Added

- add optional metrics on request duration. The metrics collection is enabled by mean of the variable `ENABLE_HTTP_CLIENT_METRICS` (default: false)

## v5.1.5 - 2022-11-24

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ declare namespace customPlugin {
//
// HTTP CLIENT
//
type HttpClientMetrics = {
disabled: boolean
urlLabel: string
}
interface HttpClientBaseOptions {
headers?: http.IncomingHttpHeaders,
timeout?: number,
Expand All @@ -136,6 +140,7 @@ declare namespace customPlugin {
interface BaseHttpClientResponse {
headers: http.IncomingHttpHeaders
statusCode: number
duration: number
}
interface StreamResponse extends BaseHttpClientResponse {
payload: NodeJS.ReadableStream
Expand All @@ -162,6 +167,7 @@ declare namespace customPlugin {
errorMessageKey?: string;
proxy?: HttpClientProxy;
query?: Record<string, string>;
metrics?: HttpClientMetrics;
}

type RequestBody = any | Buffer | ReadableStream
Expand Down
25 changes: 23 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const CLIENTTYPE_HEADER_KEY = 'CLIENTTYPE_HEADER_KEY'
const BACKOFFICE_HEADER_KEY = 'BACKOFFICE_HEADER_KEY'
const MICROSERVICE_GATEWAY_SERVICE_NAME = 'MICROSERVICE_GATEWAY_SERVICE_NAME'
const ADDITIONAL_HEADERS_TO_PROXY = 'ADDITIONAL_HEADERS_TO_PROXY'
const ENABLE_HTTP_CLIENT_METRICS = 'ENABLE_HTTP_CLIENT_METRICS'

const baseSchema = {
type: 'object',
Expand Down Expand Up @@ -85,6 +86,11 @@ const baseSchema = {
default: '',
description: 'comma separated list of additional headers to proxy',
},
[ENABLE_HTTP_CLIENT_METRICS]: {
type: 'boolean',
default: false,
description: 'flag to enable the httpClient metrics',
},
},
}

Expand Down Expand Up @@ -219,11 +225,11 @@ function getHttpClientFromRequest(url, baseOptions = {}) {
const extraHeaders = getCustomHeaders(extraHeadersKeys, requestHeaders)
const options = getBaseOptionsDecorated(this[ADDITIONAL_HEADERS_TO_PROXY], baseOptions, requestHeaders)
const serviceHeaders = { ...this.getMiaHeaders(), ...extraHeaders }
return new HttpClient(url, serviceHeaders, options)
return new HttpClient(url, serviceHeaders, options, this.httpClientMetrics)
}

function getHttpClient(url, baseOptions = {}) {
return new HttpClient(url, {}, baseOptions)
return new HttpClient(url, {}, baseOptions, this.httpClientMetrics)
}

function getHeadersToProxy({ isMiaHeaderInjected = true } = {}) {
Expand All @@ -241,6 +247,7 @@ function getHeadersToProxy({ isMiaHeaderInjected = true } = {}) {

function decorateFastify(fastify) {
const { config } = fastify
const httpClientMetrics = config[ENABLE_HTTP_CLIENT_METRICS] ? getHttpClientMetrics(fastify) : {}

fastify.decorateRequest(USERID_HEADER_KEY, config[USERID_HEADER_KEY])
fastify.decorateRequest(USER_PROPERTIES_HEADER_KEY, config[USER_PROPERTIES_HEADER_KEY])
Expand All @@ -261,6 +268,7 @@ function decorateFastify(fastify) {
fastify.decorateRequest('getDirectServiceProxy', getDirectlyServiceBuilderFromRequest)
fastify.decorateRequest('getServiceProxy', getServiceBuilderFromRequest)
fastify.decorateRequest('getHttpClient', getHttpClientFromRequest)
fastify.decorateRequest('httpClientMetrics', httpClientMetrics)

fastify.decorate(MICROSERVICE_GATEWAY_SERVICE_NAME, config[MICROSERVICE_GATEWAY_SERVICE_NAME])
fastify.decorate('addRawCustomPlugin', addRawCustomPlugin)
Expand All @@ -270,6 +278,7 @@ function decorateFastify(fastify) {
fastify.decorate('getDirectServiceProxy', getDirectlyServiceBuilderFromService)
fastify.decorate('getServiceProxy', getServiceBuilderFromService)
fastify.decorate('getHttpClient', getHttpClient)
fastify.decorate('httpClientMetrics', httpClientMetrics)
}

async function decorateRequestAndFastifyInstance(fastify, { asyncInitFunction, serviceOptions = {} }) {
Expand Down Expand Up @@ -337,6 +346,18 @@ function initCustomServiceEnvironment(envSchema = defaultSchema) {
}
}

function getHttpClientMetrics(fastify) {
if (fastify.metrics?.client) {
const requestDuration = new fastify.metrics.client.Histogram({
name: 'http_request_duration_milliseconds',
help: 'request duration histogram',
labelNames: ['baseUrl', 'url', 'method', 'statusCode'],
buckets: [5, 10, 50, 100, 500, 1000, 5000, 10000],
})
return { requestDuration }
}
}

module.exports = initCustomServiceEnvironment
module.exports.getDirectServiceProxy = getDirectlyServiceBuilderFromService
module.exports.getServiceProxy = (microserviceGatewayServiceName, baseOptions = {}) => {
Expand Down
32 changes: 31 additions & 1 deletion lib/httpClient.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-statements */
/*
* Copyright 2022 Mia srl
*
Expand All @@ -21,10 +22,11 @@ const httpsClient = require('https')
const Pino = require('pino')

class HttpClient {
constructor(baseUrl, requestHeadersToProxy, baseOptions = {}) {
constructor(baseUrl, requestHeadersToProxy, baseOptions = {}, metrics = {}) {
this.baseURL = baseUrl
this.requestHeadersToProxy = requestHeadersToProxy
this.baseOptions = baseOptions
this.metrics = baseOptions.disableMetrics ? {} : metrics

const httpsAgent = getHttpsAgent(baseOptions)
this.axios = axios.create({
Expand Down Expand Up @@ -87,6 +89,7 @@ class HttpClient {
const headers = getHeaders(options, this.requestHeadersToProxy, this.baseOptions, payload)
const httpsAgent = getHttpsAgent(options)
const errorMessageKey = getErrorMessageKey(options, this.baseOptions)
const metricsOptions = getMetricsOptions(options.metrics, url, this.metrics)
try {
const validateStatus = getValidateStatus(options)
logger.trace({ baseURL: this.baseURL, url, headers, payload }, 'make call')
Expand All @@ -110,6 +113,16 @@ class HttpClient {
duration: response.duration,
}
logger.trace({ url, ...responseBody }, 'response info')
if (metricsOptions.enabled) {
this.metrics.requestDuration.observe({
method: options.method,
url: metricsOptions.urlLabel,
baseUrl: this.baseURL,
statusCode: response.status,
},
response.duration,
)
}
return responseBody
} catch (error) {
if (error.response) {
Expand All @@ -121,6 +134,16 @@ class HttpClient {
responseError.duration = error.duration
// eslint-disable-next-line id-blacklist
logger.error({ statusCode: error.response.status, message: errorMessage }, 'response error')
if (metricsOptions.enabled) {
this.metrics.requestDuration.observe({
method: options.method,
url: metricsOptions.urlLabel,
baseUrl: this.baseURL,
statusCode: error.response.status,
},
error.duration,
)
}
throw responseError
}
const errToReturn = new Error(error.message)
Expand Down Expand Up @@ -217,6 +240,13 @@ function getHeaders(options, miaHeaders, baseOptions, payload) {
}
}

function getMetricsOptions(metricsOptions = {}, url, metrics) {
return {
enabled: Boolean(metrics.requestDuration && metricsOptions.disabled !== true),
urlLabel: metricsOptions.urlLabel ?? url,
}
}

function decorateResponseWithDuration(axiosInstance) {
axiosInstance.interceptors.response.use(
(response) => {
Expand Down
20 changes: 14 additions & 6 deletions tests/getHttpClient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ const MY_AWESOME_SERVICE_PROXY_HTTPS_URL = 'https://my-awesome-service'
const MY_AWESOME_SERVICE_PROXY_HTTP_URL_CUSTOM_PORT = 'http://my-awesome-service:3000'
const MY_AWESOME_SERVICE_PROXY_HTTPS_URL_CUSTOM_PORT = 'https://my-awesome-service:3001'

const fastifyMock = {
httpClientMetrics: {
requestDuration: {
observe: () => { /* no-op*/ },
},
},
}

tap.test('getHttpClient available for testing - complete url passed', async t => {
nock.disableNetConnect()
t.teardown(() => {
nock.enableNetConnect()
})

const RETURN_MESSAGE = 'OK'
const customProxy = getHttpClient(MY_AWESOME_SERVICE_PROXY_HTTP_URL)
const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTP_URL)
const awesomeHttpServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTP_URL}:80`)
.get('/test-endpoint')
.reply(200, {
Expand All @@ -38,7 +46,7 @@ tap.test('getHttpClient available for testing - timeout passed', async t => {
})

const RETURN_MESSAGE = 'OK'
const customProxy = getHttpClient(MY_AWESOME_SERVICE_PROXY_HTTP_URL, {
const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTP_URL, {
timeout: 100,
})
const awesomeHttpServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTP_URL}:80`)
Expand All @@ -65,7 +73,7 @@ tap.test('getHttpClient available for testing - https url', async t => {
})

const RETURN_MESSAGE = 'OK'
const customProxy = getHttpClient(MY_AWESOME_SERVICE_PROXY_HTTPS_URL)
const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTPS_URL)
const awesomeHttpsServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTPS_URL}:443`)
.get('/test-endpoint')
.reply(200, {
Expand All @@ -86,7 +94,7 @@ tap.test('getHttpClient available for testing - custom port 3000 - custom header
})

const RETURN_MESSAGE = 'OK'
const customProxy = getHttpClient(MY_AWESOME_SERVICE_PROXY_HTTP_URL_CUSTOM_PORT,
const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTP_URL_CUSTOM_PORT,
{
headers: {
'test-header': 'test header works',
Expand All @@ -113,7 +121,7 @@ tap.test('getHttpClient available for testing - https url - custom port 3001', a
})

const RETURN_MESSAGE = 'OK'
const customProxy = getHttpClient(MY_AWESOME_SERVICE_PROXY_HTTPS_URL_CUSTOM_PORT)
const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTPS_URL_CUSTOM_PORT)
const awesomeHttpsServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTPS_URL}:3001`)
.get('/test-endpoint')
.reply(200, {
Expand All @@ -130,7 +138,7 @@ tap.test('getHttpClient available for testing - https url - custom port 3001', a
tap.test('getHttpClient throws on invalid url', async t => {
const invalidUrl = 'httpnot-a-complete-url'
try {
getHttpClient(invalidUrl)
getHttpClient.call(fastifyMock, invalidUrl)
} catch (error) {
t.notOk(true, 'The function should not throw anymore if the url is not a valid one, bet return the standard proxy')
}
Expand Down
Loading

0 comments on commit ed4a080

Please sign in to comment.