Skip to content

Commit 9196b53

Browse files
authored
Merge pull request #314 from dtaniwaki/support-prometheus
Support prometheus metrics
2 parents 9769a8a + fff977e commit 9196b53

6 files changed

+187
-63
lines changed

bin/configurable-http-proxy

+12-11
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,8 @@ cli
8787
)
8888
.option("--insecure", "Disable SSL cert verification")
8989
.option("--host-routing", "Use host routing (host as first level of path)")
90-
.option("--statsd-host <host>", "Host to send statsd statistics to")
91-
.option("--statsd-port <port>", "Port to send statsd statistics to", parseInt)
92-
.option("--statsd-prefix <prefix>", "Prefix to use for statsd statistics")
90+
.option("--metrics-ip <ip>", "IP for metrics server", "0.0.0.0")
91+
.option("--metrics-port <n>", "Port of metrics server. Defaults to no metrics server")
9392
.option("--log-level <loglevel>", "Log level (debug, info, warn, error)", "info")
9493
.option(
9594
"--timeout <n>",
@@ -266,14 +265,8 @@ options.headers = args.customHeader;
266265
options.timeout = args.timeout;
267266
options.proxyTimeout = args.proxyTimeout;
268267

269-
// statsd options
270-
if (args.statsdHost) {
271-
var lynx = require("lynx");
272-
options.statsd = new lynx(args.statsdHost, args.statsdPort || 8125, {
273-
scope: args.statsdPrefix || "chp",
274-
});
275-
log.info("Sending metrics to statsd at " + args.statsdHost + ":" + args.statsdPort || 8125);
276-
}
268+
// metrics options
269+
options.enableMetrics = !!args.metricsPort;
277270

278271
// certs need to be provided for https redirection
279272
if (!options.ssl && options.redirectPort) {
@@ -326,9 +319,14 @@ if (args.ip === "*") {
326319
listen.ip = args.ip;
327320
listen.apiIp = args.apiIp || "localhost";
328321
listen.apiPort = args.apiPort || listen.port + 1;
322+
listen.metricsIp = args.metricsIp || "0.0.0.0";
323+
listen.metricsPort = args.metricsPort;
329324

330325
proxy.proxyServer.listen(listen.port, listen.ip);
331326
proxy.apiServer.listen(listen.apiPort, listen.apiIp);
327+
if (listen.metricsPort) {
328+
proxy.metricsServer.listen(listen.metricsPort, listen.metricsIp);
329+
}
332330

333331
log.info(
334332
"Proxying %s://%s:%s to %s",
@@ -343,6 +341,9 @@ log.info(
343341
listen.apiIp || "*",
344342
listen.apiPort
345343
);
344+
if (listen.metricsPort) {
345+
log.info("Serve metrics at %s://%s:%s/metrics", "http", listen.metricsIp, listen.metricsPort);
346+
}
346347

347348
if (args.pidFile) {
348349
log.info("Writing pid %s to %s", process.pid, args.pidFile);

lib/configproxy.js

+34-32
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ var http = require("http"),
1818
util = require("util"),
1919
URL = require("url"),
2020
defaultLogger = require("./log").defaultLogger,
21-
querystring = require("querystring");
21+
querystring = require("querystring"),
22+
metrics = require("./metrics");
2223

2324
function bound(that, method) {
2425
// bind a method, to ensure `this=that` when it is called
@@ -159,23 +160,11 @@ class ConfigurableProxy extends EventEmitter {
159160
this.errorTarget = this.errorTarget + "/"; // ensure trailing /
160161
}
161162
this.errorPath = options.errorPath || path.join(__dirname, "error");
162-
if (options.statsd) {
163-
this.statsd = options.statsd;
163+
164+
if (this.options.enableMetrics) {
165+
this.metrics = new metrics.Metrics();
164166
} else {
165-
// Mock the statsd object, rather than pepper the codebase with
166-
// null checks. FIXME: Maybe use a JS Proxy object (if available?)
167-
this.statsd = {
168-
increment: function () {},
169-
decrement: function () {},
170-
timing: function () {},
171-
gauge: function () {},
172-
set: function () {},
173-
createTimer: function () {
174-
return {
175-
stop: function () {},
176-
};
177-
},
178-
};
167+
this.metrics = new metrics.MockMetrics();
179168
}
180169

181170
if (this.options.defaultTarget) {
@@ -224,6 +213,12 @@ class ConfigurableProxy extends EventEmitter {
224213
this.apiServer = http.createServer(apiCallback);
225214
}
226215

216+
// handle metrics
217+
if (this.options.enableMetrics) {
218+
var metricsCallback = logErrors(that.handleMetrics);
219+
this.metricsServer = http.createServer(metricsCallback);
220+
}
221+
227222
// proxy requests separately
228223
var proxyCallback = logErrors(this.handleProxyWeb);
229224
if (this.options.ssl) {
@@ -235,7 +230,7 @@ class ConfigurableProxy extends EventEmitter {
235230
this.proxyServer.on("upgrade", bound(this, this.handleProxyWs));
236231

237232
this.proxy.on("proxyRes", function (proxyRes, req, res) {
238-
that.statsd.increment("requests." + proxyRes.statusCode, 1);
233+
that.metrics.requestsProxyCount.labels(proxyRes.statusCode).inc();
239234
});
240235
}
241236

@@ -265,8 +260,8 @@ class ConfigurableProxy extends EventEmitter {
265260
var that = this;
266261

267262
return this._routes.add(path, data).then(() => {
268-
this.updateLastActivity(path);
269-
this.log.info("Route added %s -> %s", path, data.target);
263+
that.updateLastActivity(path);
264+
that.log.info("Route added %s -> %s", path, data.target);
270265
});
271266
}
272267

@@ -341,7 +336,7 @@ class ConfigurableProxy extends EventEmitter {
341336

342337
res.write(JSON.stringify(results));
343338
res.end();
344-
that.statsd.increment("api.route.get", 1);
339+
that.metrics.apiRouteGetCount.inc();
345340
});
346341
}
347342

@@ -359,7 +354,7 @@ class ConfigurableProxy extends EventEmitter {
359354
return this.addRoute(path, data).then(function () {
360355
res.writeHead(201);
361356
res.end();
362-
that.statsd.increment("api.route.add", 1);
357+
that.metrics.apiRouteAddCount.inc();
363358
});
364359
}
365360

@@ -378,19 +373,19 @@ class ConfigurableProxy extends EventEmitter {
378373
return p.then(() => {
379374
res.writeHead(code);
380375
res.end();
381-
this.statsd.increment("api.route.delete", 1);
376+
this.metrics.apiRouteDeleteCount.inc();
382377
});
383378
});
384379
}
385380

386381
targetForReq(req) {
387-
var timer = this.statsd.createTimer("find_target_for_req");
382+
var metricsTimerEnd = this.metrics.findTargetForReqSummary.startTimer();
388383
// return proxy target for a given url path
389384
var basePath = this.hostRouting ? "/" + parseHost(req) : "";
390385
var path = basePath + decodeURIComponent(URL.parse(req.url).pathname);
391386

392387
return this._routes.getTarget(path).then(function (route) {
393-
timer.stop();
388+
metricsTimerEnd();
394389
if (route) {
395390
return {
396391
prefix: route.prefix,
@@ -401,7 +396,7 @@ class ConfigurableProxy extends EventEmitter {
401396
}
402397

403398
updateLastActivity(prefix) {
404-
var timer = this.statsd.createTimer("last_activity_updating");
399+
var metricsTimerEnd = this.metrics.lastActivityUpdatingSummary.startTimer();
405400
var routes = this._routes;
406401

407402
return routes
@@ -411,7 +406,7 @@ class ConfigurableProxy extends EventEmitter {
411406
return routes.update(prefix, { last_activity: new Date() });
412407
}
413408
})
414-
.then(timer.stop);
409+
.then(metricsTimerEnd);
415410
}
416411

417412
_handleProxyErrorDefault(code, kind, req, res) {
@@ -430,7 +425,7 @@ class ConfigurableProxy extends EventEmitter {
430425
// /404?url=%2Fuser%2Ffoo
431426

432427
var errMsg = "";
433-
this.statsd.increment("requests." + code, 1);
428+
this.metrics.requestsProxyCount.labels(code).inc();
434429
if (e) {
435430
// avoid stack traces on known not-our-problem errors:
436431
// ECONNREFUSED, EHOSTUNREACH (backend isn't there)
@@ -512,7 +507,7 @@ class ConfigurableProxy extends EventEmitter {
512507
this._handleProxyErrorDefault(code, kind, req, res);
513508
return;
514509
}
515-
if (res.writableEnded) return; // response already done
510+
if (!res.writable) return; // response already done
516511
if (res.writeHead) res.writeHead(code, { "Content-Type": "text/html" });
517512
if (res.write) res.write(data);
518513
if (res.end) res.end();
@@ -603,15 +598,15 @@ class ConfigurableProxy extends EventEmitter {
603598

604599
handleProxyWs(req, socket, head) {
605600
// Proxy a websocket request
606-
this.statsd.increment("requests.ws", 1);
601+
this.metrics.requestsWsCount.inc();
607602
return this.handleProxy("ws", req, socket, head);
608603
}
609604

610605
handleProxyWeb(req, res) {
611606
this.handleHealthCheck(req, res);
612607
if (res.finished) return;
613608
// Proxy a web request
614-
this.statsd.increment("requests.web", 1);
609+
this.metrics.requestsWebCount.inc();
615610
return this.handleProxy("web", req, res);
616611
}
617612

@@ -623,11 +618,18 @@ class ConfigurableProxy extends EventEmitter {
623618
}
624619
}
625620

621+
handleMetrics(req, res) {
622+
if (req.url === "/metrics") {
623+
return this.metrics.render(res);
624+
}
625+
fail(req, res, 404);
626+
}
627+
626628
handleApiRequest(req, res) {
627629
// Handle a request to the REST API
628-
this.statsd.increment("requests.api", 1);
629630
if (res) {
630631
res.on("finish", () => {
632+
this.metrics.requestsApiCount.labels(res.statusCode).inc();
631633
this.logResponse(req, res);
632634
});
633635
}

lib/metrics.js

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"use strict";
2+
3+
var client = require("prom-client");
4+
5+
class Metrics {
6+
constructor() {
7+
this.register = new client.Registry();
8+
client.collectDefaultMetrics({ register: this.register });
9+
10+
this.apiRouteGetCount = new client.Counter({
11+
name: "api_route_get",
12+
help: "Count of API route get requests",
13+
registers: [this.register],
14+
});
15+
16+
this.apiRouteAddCount = new client.Counter({
17+
name: "api_route_add",
18+
help: "Count of API route add requests",
19+
registers: [this.register],
20+
});
21+
22+
this.apiRouteDeleteCount = new client.Counter({
23+
name: "api_route_delete",
24+
help: "Count of API route delete requests",
25+
registers: [this.register],
26+
});
27+
28+
this.findTargetForReqSummary = new client.Summary({
29+
name: "find_target_for_req",
30+
help: "Summary of find target requests",
31+
registers: [this.register],
32+
});
33+
34+
this.lastActivityUpdatingSummary = new client.Summary({
35+
name: "last_activity_updating",
36+
help: "Summary of last activity updating requests",
37+
registers: [this.register],
38+
});
39+
40+
this.requestsWsCount = new client.Counter({
41+
name: "requests_ws",
42+
help: "Count of websocket requests",
43+
registers: [this.register],
44+
});
45+
46+
this.requestsWebCount = new client.Counter({
47+
name: "requests_web",
48+
help: "Count of web requests",
49+
registers: [this.register],
50+
});
51+
52+
this.requestsProxyCount = new client.Counter({
53+
name: "requests_proxy",
54+
help: "Count of proxy requests",
55+
labelNames: ["status"],
56+
registers: [this.register],
57+
});
58+
59+
this.requestsApiCount = new client.Counter({
60+
name: "requests_api",
61+
help: "Count of API requests",
62+
labelNames: ["status"],
63+
registers: [this.register],
64+
});
65+
}
66+
67+
render(res) {
68+
return this.register.metrics().then((s) => {
69+
res.writeHead(200, { "Content-Type": this.register.contentType });
70+
res.write(s);
71+
res.end();
72+
});
73+
}
74+
}
75+
76+
class MockMetrics {
77+
constructor() {
78+
return new Proxy(this, {
79+
get(target, name) {
80+
const mockCounter = new Proxy(
81+
{},
82+
{
83+
get(target, name) {
84+
if (name == "inc") {
85+
return () => {};
86+
}
87+
if (name == "startTimer") {
88+
return () => {
89+
return () => {};
90+
};
91+
}
92+
if (name == "labels") {
93+
return () => {
94+
return mockCounter;
95+
};
96+
}
97+
},
98+
}
99+
);
100+
return mockCounter;
101+
},
102+
});
103+
}
104+
105+
render(res) {
106+
return Promise.resolve();
107+
}
108+
}
109+
110+
module.exports = {
111+
Metrics,
112+
MockMetrics,
113+
};

lib/testutil.js

+6
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,18 @@ exports.setupProxy = function (port, options, paths) {
128128

129129
servers.push(proxy.apiServer);
130130
servers.push(proxy.proxyServer);
131+
if (options.enableMetrics) {
132+
servers.push(proxy.metricsServer);
133+
}
131134
proxy.apiServer.on("listening", onlisten);
132135
proxy.proxyServer.on("listening", onlisten);
133136

134137
addTargets(proxy, paths || ["/"], port + 2).then(function () {
135138
proxy.proxyServer.listen(port, ip);
136139
proxy.apiServer.listen(port + 1, ip);
140+
if (options.enableMetrics) {
141+
proxy.metricsServer.listen(port + 3, ip);
142+
}
137143
});
138144
return p;
139145
};

0 commit comments

Comments
 (0)