Skip to content

Commit b7a29f2

Browse files
paulmelnikowrepo-ranger[bot]
authored andcommitted
Add a response-time metric (#3948)
* Refactor existing metrics support into MetricHelper This completes the refactor done at #3662 (comment) 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
1 parent 33389e3 commit b7a29f2

File tree

7 files changed

+146
-43
lines changed

7 files changed

+146
-43
lines changed

core/base-service/base-non-memory-caching.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const makeBadge = require('../../gh-badges/lib/make-badge')
44
const BaseService = require('./base')
5+
const { MetricHelper } = require('./metric-helper')
56
const { setCacheHeaders } = require('./cache-headers')
67
const { makeSend } = require('./legacy-result-sender')
78
const coalesceBadge = require('./coalesce-badge')
@@ -24,16 +25,19 @@ const { prepareRoute, namedParamsForMatch } = require('./route')
2425
// configured by the service, the user's request, and the server's default
2526
// cache length.
2627
module.exports = class NonMemoryCachingBaseService extends BaseService {
27-
static register({ camp, requestCounter }, serviceConfig) {
28+
static register({ camp, metricInstance }, serviceConfig) {
2829
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
2930
const { _cacheLength: serviceDefaultCacheLengthSeconds } = this
3031
const { regex, captureNames } = prepareRoute(this.route)
3132

32-
const serviceRequestCounter = this._createServiceRequestCounter({
33-
requestCounter,
33+
const metricHelper = MetricHelper.create({
34+
metricInstance,
35+
ServiceClass: this,
3436
})
3537

3638
camp.route(regex, async (queryParams, match, end, ask) => {
39+
const metricHandle = metricHelper.startRequest()
40+
3741
const namedParams = namedParamsForMatch(captureNames, match, this)
3842
const serviceData = await this.invoke(
3943
{},
@@ -64,7 +68,7 @@ module.exports = class NonMemoryCachingBaseService extends BaseService {
6468

6569
makeSend(format, ask.res, end)(svg)
6670

67-
serviceRequestCounter.inc()
71+
metricHandle.noteResponseSent()
6872
})
6973
}
7074
}

core/base-service/base-static.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ const {
77
setCacheHeadersForStaticResource,
88
} = require('./cache-headers')
99
const { makeSend } = require('./legacy-result-sender')
10+
const { MetricHelper } = require('./metric-helper')
1011
const coalesceBadge = require('./coalesce-badge')
1112
const { prepareRoute, namedParamsForMatch } = require('./route')
1213

1314
module.exports = class BaseStaticService extends BaseService {
14-
static register({ camp, requestCounter }, serviceConfig) {
15+
static register({ camp, metricInstance }, serviceConfig) {
1516
const {
1617
profiling: { makeBadge: shouldProfileMakeBadge },
1718
} = serviceConfig
1819
const { regex, captureNames } = prepareRoute(this.route)
1920

20-
const serviceRequestCounter = this._createServiceRequestCounter({
21-
requestCounter,
21+
const metricHelper = MetricHelper.create({
22+
metricInstance,
23+
ServiceClass: this,
2224
})
2325

2426
camp.route(regex, async (queryParams, match, end, ask) => {
@@ -29,6 +31,8 @@ module.exports = class BaseStaticService extends BaseService {
2931
return
3032
}
3133

34+
const metricHandle = metricHelper.startRequest()
35+
3236
const namedParams = namedParamsForMatch(captureNames, match, this)
3337
const serviceData = await this.invoke(
3438
{},
@@ -60,7 +64,7 @@ module.exports = class BaseStaticService extends BaseService {
6064

6165
makeSend(format, ask.res, end)(svg)
6266

63-
serviceRequestCounter.inc()
67+
metricHandle.noteResponseSent()
6468
})
6569
}
6670
}

core/base-service/base.js

+8-16
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
* @module
44
*/
55

6-
const decamelize = require('decamelize')
76
// See available emoji at http://emoji.muan.co/
87
const emojic = require('emojic')
98
const Joi = require('@hapi/joi')
109
const log = require('../server/log')
1110
const { AuthHelper } = require('./auth-helper')
11+
const { MetricHelper } = require('./metric-helper')
1212
const { assertValidCategory } = require('./categories')
1313
const checkErrorResponse = require('./check-error-response')
1414
const coalesceBadge = require('./coalesce-badge')
@@ -391,34 +391,26 @@ class BaseService {
391391
return serviceData
392392
}
393393

394-
static _createServiceRequestCounter({ requestCounter }) {
395-
if (requestCounter) {
396-
const { category, serviceFamily, name } = this
397-
const service = decamelize(name)
398-
return requestCounter.labels(category, serviceFamily, service)
399-
} else {
400-
// When metrics are disabled, return a mock counter.
401-
return { inc: () => {} }
402-
}
403-
}
404-
405394
static register(
406-
{ camp, handleRequest, githubApiProvider, requestCounter },
395+
{ camp, handleRequest, githubApiProvider, metricInstance },
407396
serviceConfig
408397
) {
409398
const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
410399
const { regex, captureNames } = prepareRoute(this.route)
411400
const queryParams = getQueryParamNames(this.route)
412401

413-
const serviceRequestCounter = this._createServiceRequestCounter({
414-
requestCounter,
402+
const metricHelper = MetricHelper.create({
403+
metricInstance,
404+
ServiceClass: this,
415405
})
416406

417407
camp.route(
418408
regex,
419409
handleRequest(cacheHeaderConfig, {
420410
queryParams,
421411
handler: async (queryParams, match, sendBadge, request) => {
412+
const metricHandle = metricHelper.startRequest()
413+
422414
const namedParams = namedParamsForMatch(captureNames, match, this)
423415
const serviceData = await this.invoke(
424416
{
@@ -441,7 +433,7 @@ class BaseService {
441433
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
442434
sendBadge(format, badgeData)
443435

444-
serviceRequestCounter.inc()
436+
metricHandle.noteResponseSent()
445437
},
446438
cacheLength: this._cacheLength,
447439
fetchLimitBytes,

core/base-service/metric-helper.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict'
2+
3+
const { performance } = require('perf_hooks')
4+
5+
class MetricHelper {
6+
constructor({ metricInstance }, { category, serviceFamily, name }) {
7+
if (metricInstance) {
8+
this.metricInstance = metricInstance
9+
this.serviceRequestCounter = metricInstance.createNumRequestCounter({
10+
category,
11+
serviceFamily,
12+
name,
13+
})
14+
} else {
15+
this.metricInstance = undefined
16+
this.serviceRequestCounter = undefined
17+
}
18+
}
19+
20+
static create({ metricInstance, ServiceClass }) {
21+
const { category, serviceFamily, name } = ServiceClass
22+
return new this({ metricInstance }, { category, serviceFamily, name })
23+
}
24+
25+
startRequest() {
26+
const { metricInstance, serviceRequestCounter } = this
27+
28+
const requestStartTime = performance.now()
29+
30+
return {
31+
noteResponseSent() {
32+
if (metricInstance) {
33+
const elapsedTime = performance.now() - requestStartTime
34+
metricInstance.noteResponseTime(elapsedTime)
35+
}
36+
37+
if (serviceRequestCounter) {
38+
serviceRequestCounter.inc()
39+
}
40+
},
41+
}
42+
}
43+
}
44+
45+
module.exports = { MetricHelper }

core/base-service/redirector.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
setCacheHeadersForStaticResource,
1111
} = require('./cache-headers')
1212
const { isValidCategory } = require('./categories')
13+
const { MetricHelper } = require('./metric-helper')
1314
const { isValidRoute, prepareRoute, namedParamsForMatch } = require('./route')
1415
const trace = require('./trace')
1516

@@ -62,14 +63,15 @@ module.exports = function redirector(attrs) {
6263
return route
6364
}
6465

65-
static register({ camp, requestCounter }, { rasterUrl }) {
66+
static register({ camp, metricInstance }, { rasterUrl }) {
6667
const { regex, captureNames } = prepareRoute({
6768
...this.route,
6869
withPng: Boolean(rasterUrl),
6970
})
7071

71-
const serviceRequestCounter = this._createServiceRequestCounter({
72-
requestCounter,
72+
const metricHelper = MetricHelper.create({
73+
metricInstance,
74+
ServiceClass: this,
7375
})
7476

7577
camp.route(regex, async (queryParams, match, end, ask) => {
@@ -80,6 +82,8 @@ module.exports = function redirector(attrs) {
8082
return
8183
}
8284

85+
const metricHandle = metricHelper.startRequest()
86+
8387
const namedParams = namedParamsForMatch(captureNames, match, this)
8488
trace.logTrace(
8589
'inbound',
@@ -121,7 +125,7 @@ module.exports = function redirector(attrs) {
121125

122126
ask.res.end()
123127

124-
serviceRequestCounter.inc()
128+
metricHandle.noteResponseSent()
125129
})
126130
}
127131
}

core/server/prometheus-metrics.js

+61-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,59 @@
11
'use strict'
22

3+
const decamelize = require('decamelize')
34
const prometheus = require('prom-client')
45

56
module.exports = class PrometheusMetrics {
67
constructor() {
78
this.register = new prometheus.Registry()
8-
this.requestCounter = new prometheus.Counter({
9-
name: 'service_requests_total',
10-
help: 'Total service requests',
11-
labelNames: ['category', 'family', 'service'],
12-
registers: [this.register],
13-
})
9+
this.counters = {
10+
numRequests: new prometheus.Counter({
11+
name: 'service_requests_total',
12+
help: 'Total service requests',
13+
labelNames: ['category', 'family', 'service'],
14+
registers: [this.register],
15+
}),
16+
responseTime: new prometheus.Histogram({
17+
name: 'service_response_millis',
18+
help: 'Service response time in milliseconds',
19+
// 250 ms increments up to 2 seconds, then 500 ms increments up to 8
20+
// seconds, then 1 second increments up to 15 seconds.
21+
buckets: [
22+
250,
23+
500,
24+
750,
25+
1000,
26+
1250,
27+
1500,
28+
1750,
29+
2000,
30+
2250,
31+
2500,
32+
2750,
33+
3000,
34+
3250,
35+
3500,
36+
3750,
37+
4000,
38+
4500,
39+
5000,
40+
5500,
41+
6000,
42+
6500,
43+
7000,
44+
7500,
45+
8000,
46+
9000,
47+
10000,
48+
11000,
49+
12000,
50+
13000,
51+
14000,
52+
15000,
53+
],
54+
registers: [this.register],
55+
}),
56+
}
1457
}
1558

1659
async initialize(server) {
@@ -30,4 +73,16 @@ module.exports = class PrometheusMetrics {
3073
this.interval = undefined
3174
}
3275
}
76+
77+
/**
78+
* @returns {object} `{ inc() {} }`.
79+
*/
80+
createNumRequestCounter({ category, serviceFamily, name }) {
81+
const service = decamelize(name)
82+
return this.counters.numRequests.labels(category, serviceFamily, service)
83+
}
84+
85+
noteResponseTime(responseTime) {
86+
return this.counters.responseTime.observe(responseTime)
87+
}
3388
}

core/server/server.js

+8-9
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class Server {
148148
private: privateConfig,
149149
})
150150
if (publicConfig.metrics.prometheus.enabled) {
151-
this.metrics = new PrometheusMetrics()
151+
this.metricInstance = new PrometheusMetrics()
152152
}
153153
}
154154

@@ -263,13 +263,12 @@ class Server {
263263
* load each service and register a Scoutcamp route for each service.
264264
*/
265265
registerServices() {
266-
const { config, camp } = this
266+
const { config, camp, metricInstance } = this
267267
const { apiProvider: githubApiProvider } = this.githubConstellation
268-
const { requestCounter } = this.metrics || {}
269268

270269
loadServiceClasses().forEach(serviceClass =>
271270
serviceClass.register(
272-
{ camp, handleRequest, githubApiProvider, requestCounter },
271+
{ camp, handleRequest, githubApiProvider, metricInstance },
273272
{
274273
handleInternalErrors: config.public.handleInternalErrors,
275274
cacheHeaders: config.public.cacheHeaders,
@@ -309,10 +308,10 @@ class Server {
309308

310309
this.cleanupMonitor = sysMonitor.setRoutes({ rateLimit }, camp)
311310

312-
const { githubConstellation, metrics } = this
311+
const { githubConstellation, metricInstance } = this
313312
githubConstellation.initialize(camp)
314-
if (metrics) {
315-
metrics.initialize(camp)
313+
if (metricInstance) {
314+
metricInstance.initialize(camp)
316315
}
317316

318317
const { apiProvider: githubApiProvider } = this.githubConstellation
@@ -355,8 +354,8 @@ class Server {
355354
this.githubConstellation = undefined
356355
}
357356

358-
if (this.metrics) {
359-
this.metrics.stop()
357+
if (this.metricInstance) {
358+
this.metricInstance.stop()
360359
}
361360
}
362361
}

0 commit comments

Comments
 (0)