Add a response-time metric (#3948)
* Refactor existing metrics support into MetricHelper This completes the refactor done at https://github.com/badges/shields/pull/3662#issuecomment-509011229 in anticipation of adding more metrics support, such as response size of an upstream service, or response time. * Clean up * Renames * Add response time metrics This adds around 30 new metrics to cover response times at a fairly granular level. We may be able to shrink the number of buckets with time, though I think using 30 metrics is probably okay given that I think may become our most important metric. * Fix
This commit is contained in:
committed by
repo-ranger[bot]
parent
33389e352d
commit
b7a29f20ef
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const makeBadge = require('../../gh-badges/lib/make-badge')
|
const makeBadge = require('../../gh-badges/lib/make-badge')
|
||||||
const BaseService = require('./base')
|
const BaseService = require('./base')
|
||||||
|
const { MetricHelper } = require('./metric-helper')
|
||||||
const { setCacheHeaders } = require('./cache-headers')
|
const { setCacheHeaders } = require('./cache-headers')
|
||||||
const { makeSend } = require('./legacy-result-sender')
|
const { makeSend } = require('./legacy-result-sender')
|
||||||
const coalesceBadge = require('./coalesce-badge')
|
const coalesceBadge = require('./coalesce-badge')
|
||||||
@@ -24,16 +25,19 @@ const { prepareRoute, namedParamsForMatch } = require('./route')
|
|||||||
// configured by the service, the user's request, and the server's default
|
// configured by the service, the user's request, and the server's default
|
||||||
// cache length.
|
// cache length.
|
||||||
module.exports = class NonMemoryCachingBaseService extends BaseService {
|
module.exports = class NonMemoryCachingBaseService extends BaseService {
|
||||||
static register({ camp, requestCounter }, serviceConfig) {
|
static register({ camp, metricInstance }, serviceConfig) {
|
||||||
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
||||||
const { _cacheLength: serviceDefaultCacheLengthSeconds } = this
|
const { _cacheLength: serviceDefaultCacheLengthSeconds } = this
|
||||||
const { regex, captureNames } = prepareRoute(this.route)
|
const { regex, captureNames } = prepareRoute(this.route)
|
||||||
|
|
||||||
const serviceRequestCounter = this._createServiceRequestCounter({
|
const metricHelper = MetricHelper.create({
|
||||||
requestCounter,
|
metricInstance,
|
||||||
|
ServiceClass: this,
|
||||||
})
|
})
|
||||||
|
|
||||||
camp.route(regex, async (queryParams, match, end, ask) => {
|
camp.route(regex, async (queryParams, match, end, ask) => {
|
||||||
|
const metricHandle = metricHelper.startRequest()
|
||||||
|
|
||||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||||
const serviceData = await this.invoke(
|
const serviceData = await this.invoke(
|
||||||
{},
|
{},
|
||||||
@@ -64,7 +68,7 @@ module.exports = class NonMemoryCachingBaseService extends BaseService {
|
|||||||
|
|
||||||
makeSend(format, ask.res, end)(svg)
|
makeSend(format, ask.res, end)(svg)
|
||||||
|
|
||||||
serviceRequestCounter.inc()
|
metricHandle.noteResponseSent()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,20 @@ const {
|
|||||||
setCacheHeadersForStaticResource,
|
setCacheHeadersForStaticResource,
|
||||||
} = require('./cache-headers')
|
} = require('./cache-headers')
|
||||||
const { makeSend } = require('./legacy-result-sender')
|
const { makeSend } = require('./legacy-result-sender')
|
||||||
|
const { MetricHelper } = require('./metric-helper')
|
||||||
const coalesceBadge = require('./coalesce-badge')
|
const coalesceBadge = require('./coalesce-badge')
|
||||||
const { prepareRoute, namedParamsForMatch } = require('./route')
|
const { prepareRoute, namedParamsForMatch } = require('./route')
|
||||||
|
|
||||||
module.exports = class BaseStaticService extends BaseService {
|
module.exports = class BaseStaticService extends BaseService {
|
||||||
static register({ camp, requestCounter }, serviceConfig) {
|
static register({ camp, metricInstance }, serviceConfig) {
|
||||||
const {
|
const {
|
||||||
profiling: { makeBadge: shouldProfileMakeBadge },
|
profiling: { makeBadge: shouldProfileMakeBadge },
|
||||||
} = serviceConfig
|
} = serviceConfig
|
||||||
const { regex, captureNames } = prepareRoute(this.route)
|
const { regex, captureNames } = prepareRoute(this.route)
|
||||||
|
|
||||||
const serviceRequestCounter = this._createServiceRequestCounter({
|
const metricHelper = MetricHelper.create({
|
||||||
requestCounter,
|
metricInstance,
|
||||||
|
ServiceClass: this,
|
||||||
})
|
})
|
||||||
|
|
||||||
camp.route(regex, async (queryParams, match, end, ask) => {
|
camp.route(regex, async (queryParams, match, end, ask) => {
|
||||||
@@ -29,6 +31,8 @@ module.exports = class BaseStaticService extends BaseService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metricHandle = metricHelper.startRequest()
|
||||||
|
|
||||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||||
const serviceData = await this.invoke(
|
const serviceData = await this.invoke(
|
||||||
{},
|
{},
|
||||||
@@ -60,7 +64,7 @@ module.exports = class BaseStaticService extends BaseService {
|
|||||||
|
|
||||||
makeSend(format, ask.res, end)(svg)
|
makeSend(format, ask.res, end)(svg)
|
||||||
|
|
||||||
serviceRequestCounter.inc()
|
metricHandle.noteResponseSent()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const decamelize = require('decamelize')
|
|
||||||
// See available emoji at http://emoji.muan.co/
|
// See available emoji at http://emoji.muan.co/
|
||||||
const emojic = require('emojic')
|
const emojic = require('emojic')
|
||||||
const Joi = require('@hapi/joi')
|
const Joi = require('@hapi/joi')
|
||||||
const log = require('../server/log')
|
const log = require('../server/log')
|
||||||
const { AuthHelper } = require('./auth-helper')
|
const { AuthHelper } = require('./auth-helper')
|
||||||
|
const { MetricHelper } = require('./metric-helper')
|
||||||
const { assertValidCategory } = require('./categories')
|
const { assertValidCategory } = require('./categories')
|
||||||
const checkErrorResponse = require('./check-error-response')
|
const checkErrorResponse = require('./check-error-response')
|
||||||
const coalesceBadge = require('./coalesce-badge')
|
const coalesceBadge = require('./coalesce-badge')
|
||||||
@@ -391,27 +391,17 @@ class BaseService {
|
|||||||
return serviceData
|
return serviceData
|
||||||
}
|
}
|
||||||
|
|
||||||
static _createServiceRequestCounter({ requestCounter }) {
|
|
||||||
if (requestCounter) {
|
|
||||||
const { category, serviceFamily, name } = this
|
|
||||||
const service = decamelize(name)
|
|
||||||
return requestCounter.labels(category, serviceFamily, service)
|
|
||||||
} else {
|
|
||||||
// When metrics are disabled, return a mock counter.
|
|
||||||
return { inc: () => {} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static register(
|
static register(
|
||||||
{ camp, handleRequest, githubApiProvider, requestCounter },
|
{ camp, handleRequest, githubApiProvider, metricInstance },
|
||||||
serviceConfig
|
serviceConfig
|
||||||
) {
|
) {
|
||||||
const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
|
const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
|
||||||
const { regex, captureNames } = prepareRoute(this.route)
|
const { regex, captureNames } = prepareRoute(this.route)
|
||||||
const queryParams = getQueryParamNames(this.route)
|
const queryParams = getQueryParamNames(this.route)
|
||||||
|
|
||||||
const serviceRequestCounter = this._createServiceRequestCounter({
|
const metricHelper = MetricHelper.create({
|
||||||
requestCounter,
|
metricInstance,
|
||||||
|
ServiceClass: this,
|
||||||
})
|
})
|
||||||
|
|
||||||
camp.route(
|
camp.route(
|
||||||
@@ -419,6 +409,8 @@ class BaseService {
|
|||||||
handleRequest(cacheHeaderConfig, {
|
handleRequest(cacheHeaderConfig, {
|
||||||
queryParams,
|
queryParams,
|
||||||
handler: async (queryParams, match, sendBadge, request) => {
|
handler: async (queryParams, match, sendBadge, request) => {
|
||||||
|
const metricHandle = metricHelper.startRequest()
|
||||||
|
|
||||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||||
const serviceData = await this.invoke(
|
const serviceData = await this.invoke(
|
||||||
{
|
{
|
||||||
@@ -441,7 +433,7 @@ class BaseService {
|
|||||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||||
sendBadge(format, badgeData)
|
sendBadge(format, badgeData)
|
||||||
|
|
||||||
serviceRequestCounter.inc()
|
metricHandle.noteResponseSent()
|
||||||
},
|
},
|
||||||
cacheLength: this._cacheLength,
|
cacheLength: this._cacheLength,
|
||||||
fetchLimitBytes,
|
fetchLimitBytes,
|
||||||
|
|||||||
45
core/base-service/metric-helper.js
Normal file
45
core/base-service/metric-helper.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { performance } = require('perf_hooks')
|
||||||
|
|
||||||
|
class MetricHelper {
|
||||||
|
constructor({ metricInstance }, { category, serviceFamily, name }) {
|
||||||
|
if (metricInstance) {
|
||||||
|
this.metricInstance = metricInstance
|
||||||
|
this.serviceRequestCounter = metricInstance.createNumRequestCounter({
|
||||||
|
category,
|
||||||
|
serviceFamily,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.metricInstance = undefined
|
||||||
|
this.serviceRequestCounter = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static create({ metricInstance, ServiceClass }) {
|
||||||
|
const { category, serviceFamily, name } = ServiceClass
|
||||||
|
return new this({ metricInstance }, { category, serviceFamily, name })
|
||||||
|
}
|
||||||
|
|
||||||
|
startRequest() {
|
||||||
|
const { metricInstance, serviceRequestCounter } = this
|
||||||
|
|
||||||
|
const requestStartTime = performance.now()
|
||||||
|
|
||||||
|
return {
|
||||||
|
noteResponseSent() {
|
||||||
|
if (metricInstance) {
|
||||||
|
const elapsedTime = performance.now() - requestStartTime
|
||||||
|
metricInstance.noteResponseTime(elapsedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceRequestCounter) {
|
||||||
|
serviceRequestCounter.inc()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { MetricHelper }
|
||||||
@@ -10,6 +10,7 @@ const {
|
|||||||
setCacheHeadersForStaticResource,
|
setCacheHeadersForStaticResource,
|
||||||
} = require('./cache-headers')
|
} = require('./cache-headers')
|
||||||
const { isValidCategory } = require('./categories')
|
const { isValidCategory } = require('./categories')
|
||||||
|
const { MetricHelper } = require('./metric-helper')
|
||||||
const { isValidRoute, prepareRoute, namedParamsForMatch } = require('./route')
|
const { isValidRoute, prepareRoute, namedParamsForMatch } = require('./route')
|
||||||
const trace = require('./trace')
|
const trace = require('./trace')
|
||||||
|
|
||||||
@@ -62,14 +63,15 @@ module.exports = function redirector(attrs) {
|
|||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
static register({ camp, requestCounter }, { rasterUrl }) {
|
static register({ camp, metricInstance }, { rasterUrl }) {
|
||||||
const { regex, captureNames } = prepareRoute({
|
const { regex, captureNames } = prepareRoute({
|
||||||
...this.route,
|
...this.route,
|
||||||
withPng: Boolean(rasterUrl),
|
withPng: Boolean(rasterUrl),
|
||||||
})
|
})
|
||||||
|
|
||||||
const serviceRequestCounter = this._createServiceRequestCounter({
|
const metricHelper = MetricHelper.create({
|
||||||
requestCounter,
|
metricInstance,
|
||||||
|
ServiceClass: this,
|
||||||
})
|
})
|
||||||
|
|
||||||
camp.route(regex, async (queryParams, match, end, ask) => {
|
camp.route(regex, async (queryParams, match, end, ask) => {
|
||||||
@@ -80,6 +82,8 @@ module.exports = function redirector(attrs) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metricHandle = metricHelper.startRequest()
|
||||||
|
|
||||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||||
trace.logTrace(
|
trace.logTrace(
|
||||||
'inbound',
|
'inbound',
|
||||||
@@ -121,7 +125,7 @@ module.exports = function redirector(attrs) {
|
|||||||
|
|
||||||
ask.res.end()
|
ask.res.end()
|
||||||
|
|
||||||
serviceRequestCounter.inc()
|
metricHandle.noteResponseSent()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,59 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
const decamelize = require('decamelize')
|
||||||
const prometheus = require('prom-client')
|
const prometheus = require('prom-client')
|
||||||
|
|
||||||
module.exports = class PrometheusMetrics {
|
module.exports = class PrometheusMetrics {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.register = new prometheus.Registry()
|
this.register = new prometheus.Registry()
|
||||||
this.requestCounter = new prometheus.Counter({
|
this.counters = {
|
||||||
name: 'service_requests_total',
|
numRequests: new prometheus.Counter({
|
||||||
help: 'Total service requests',
|
name: 'service_requests_total',
|
||||||
labelNames: ['category', 'family', 'service'],
|
help: 'Total service requests',
|
||||||
registers: [this.register],
|
labelNames: ['category', 'family', 'service'],
|
||||||
})
|
registers: [this.register],
|
||||||
|
}),
|
||||||
|
responseTime: new prometheus.Histogram({
|
||||||
|
name: 'service_response_millis',
|
||||||
|
help: 'Service response time in milliseconds',
|
||||||
|
// 250 ms increments up to 2 seconds, then 500 ms increments up to 8
|
||||||
|
// seconds, then 1 second increments up to 15 seconds.
|
||||||
|
buckets: [
|
||||||
|
250,
|
||||||
|
500,
|
||||||
|
750,
|
||||||
|
1000,
|
||||||
|
1250,
|
||||||
|
1500,
|
||||||
|
1750,
|
||||||
|
2000,
|
||||||
|
2250,
|
||||||
|
2500,
|
||||||
|
2750,
|
||||||
|
3000,
|
||||||
|
3250,
|
||||||
|
3500,
|
||||||
|
3750,
|
||||||
|
4000,
|
||||||
|
4500,
|
||||||
|
5000,
|
||||||
|
5500,
|
||||||
|
6000,
|
||||||
|
6500,
|
||||||
|
7000,
|
||||||
|
7500,
|
||||||
|
8000,
|
||||||
|
9000,
|
||||||
|
10000,
|
||||||
|
11000,
|
||||||
|
12000,
|
||||||
|
13000,
|
||||||
|
14000,
|
||||||
|
15000,
|
||||||
|
],
|
||||||
|
registers: [this.register],
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(server) {
|
async initialize(server) {
|
||||||
@@ -30,4 +73,16 @@ module.exports = class PrometheusMetrics {
|
|||||||
this.interval = undefined
|
this.interval = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {object} `{ inc() {} }`.
|
||||||
|
*/
|
||||||
|
createNumRequestCounter({ category, serviceFamily, name }) {
|
||||||
|
const service = decamelize(name)
|
||||||
|
return this.counters.numRequests.labels(category, serviceFamily, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
noteResponseTime(responseTime) {
|
||||||
|
return this.counters.responseTime.observe(responseTime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class Server {
|
|||||||
private: privateConfig,
|
private: privateConfig,
|
||||||
})
|
})
|
||||||
if (publicConfig.metrics.prometheus.enabled) {
|
if (publicConfig.metrics.prometheus.enabled) {
|
||||||
this.metrics = new PrometheusMetrics()
|
this.metricInstance = new PrometheusMetrics()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,13 +263,12 @@ class Server {
|
|||||||
* load each service and register a Scoutcamp route for each service.
|
* load each service and register a Scoutcamp route for each service.
|
||||||
*/
|
*/
|
||||||
registerServices() {
|
registerServices() {
|
||||||
const { config, camp } = this
|
const { config, camp, metricInstance } = this
|
||||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||||
const { requestCounter } = this.metrics || {}
|
|
||||||
|
|
||||||
loadServiceClasses().forEach(serviceClass =>
|
loadServiceClasses().forEach(serviceClass =>
|
||||||
serviceClass.register(
|
serviceClass.register(
|
||||||
{ camp, handleRequest, githubApiProvider, requestCounter },
|
{ camp, handleRequest, githubApiProvider, metricInstance },
|
||||||
{
|
{
|
||||||
handleInternalErrors: config.public.handleInternalErrors,
|
handleInternalErrors: config.public.handleInternalErrors,
|
||||||
cacheHeaders: config.public.cacheHeaders,
|
cacheHeaders: config.public.cacheHeaders,
|
||||||
@@ -309,10 +308,10 @@ class Server {
|
|||||||
|
|
||||||
this.cleanupMonitor = sysMonitor.setRoutes({ rateLimit }, camp)
|
this.cleanupMonitor = sysMonitor.setRoutes({ rateLimit }, camp)
|
||||||
|
|
||||||
const { githubConstellation, metrics } = this
|
const { githubConstellation, metricInstance } = this
|
||||||
githubConstellation.initialize(camp)
|
githubConstellation.initialize(camp)
|
||||||
if (metrics) {
|
if (metricInstance) {
|
||||||
metrics.initialize(camp)
|
metricInstance.initialize(camp)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||||
@@ -355,8 +354,8 @@ class Server {
|
|||||||
this.githubConstellation = undefined
|
this.githubConstellation = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.metrics) {
|
if (this.metricInstance) {
|
||||||
this.metrics.stop()
|
this.metricInstance.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user