Skip to content

Commit

Permalink
Merge pull request #314 from dtaniwaki/support-prometheus
Browse files Browse the repository at this point in the history
Support prometheus metrics
  • Loading branch information
minrk committed May 26, 2021
2 parents 9769a8a + fff977e commit 9196b53
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 63 deletions.
23 changes: 12 additions & 11 deletions bin/configurable-http-proxy
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@ cli
)
.option("--insecure", "Disable SSL cert verification")
.option("--host-routing", "Use host routing (host as first level of path)")
.option("--statsd-host <host>", "Host to send statsd statistics to")
.option("--statsd-port <port>", "Port to send statsd statistics to", parseInt)
.option("--statsd-prefix <prefix>", "Prefix to use for statsd statistics")
.option("--metrics-ip <ip>", "IP for metrics server", "0.0.0.0")
.option("--metrics-port <n>", "Port of metrics server. Defaults to no metrics server")
.option("--log-level <loglevel>", "Log level (debug, info, warn, error)", "info")
.option(
"--timeout <n>",
Expand Down Expand Up @@ -266,14 +265,8 @@ options.headers = args.customHeader;
options.timeout = args.timeout;
options.proxyTimeout = args.proxyTimeout;

// statsd options
if (args.statsdHost) {
var lynx = require("lynx");
options.statsd = new lynx(args.statsdHost, args.statsdPort || 8125, {
scope: args.statsdPrefix || "chp",
});
log.info("Sending metrics to statsd at " + args.statsdHost + ":" + args.statsdPort || 8125);
}
// metrics options
options.enableMetrics = !!args.metricsPort;

// certs need to be provided for https redirection
if (!options.ssl && options.redirectPort) {
Expand Down Expand Up @@ -326,9 +319,14 @@ if (args.ip === "*") {
listen.ip = args.ip;
listen.apiIp = args.apiIp || "localhost";
listen.apiPort = args.apiPort || listen.port + 1;
listen.metricsIp = args.metricsIp || "0.0.0.0";
listen.metricsPort = args.metricsPort;

proxy.proxyServer.listen(listen.port, listen.ip);
proxy.apiServer.listen(listen.apiPort, listen.apiIp);
if (listen.metricsPort) {
proxy.metricsServer.listen(listen.metricsPort, listen.metricsIp);
}

log.info(
"Proxying %s://%s:%s to %s",
Expand All @@ -343,6 +341,9 @@ log.info(
listen.apiIp || "*",
listen.apiPort
);
if (listen.metricsPort) {
log.info("Serve metrics at %s://%s:%s/metrics", "http", listen.metricsIp, listen.metricsPort);
}

if (args.pidFile) {
log.info("Writing pid %s to %s", process.pid, args.pidFile);
Expand Down
66 changes: 34 additions & 32 deletions lib/configproxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ var http = require("http"),
util = require("util"),
URL = require("url"),
defaultLogger = require("./log").defaultLogger,
querystring = require("querystring");
querystring = require("querystring"),
metrics = require("./metrics");

function bound(that, method) {
// bind a method, to ensure `this=that` when it is called
Expand Down Expand Up @@ -159,23 +160,11 @@ class ConfigurableProxy extends EventEmitter {
this.errorTarget = this.errorTarget + "/"; // ensure trailing /
}
this.errorPath = options.errorPath || path.join(__dirname, "error");
if (options.statsd) {
this.statsd = options.statsd;

if (this.options.enableMetrics) {
this.metrics = new metrics.Metrics();
} else {
// Mock the statsd object, rather than pepper the codebase with
// null checks. FIXME: Maybe use a JS Proxy object (if available?)
this.statsd = {
increment: function () {},
decrement: function () {},
timing: function () {},
gauge: function () {},
set: function () {},
createTimer: function () {
return {
stop: function () {},
};
},
};
this.metrics = new metrics.MockMetrics();
}

if (this.options.defaultTarget) {
Expand Down Expand Up @@ -224,6 +213,12 @@ class ConfigurableProxy extends EventEmitter {
this.apiServer = http.createServer(apiCallback);
}

// handle metrics
if (this.options.enableMetrics) {
var metricsCallback = logErrors(that.handleMetrics);
this.metricsServer = http.createServer(metricsCallback);
}

// proxy requests separately
var proxyCallback = logErrors(this.handleProxyWeb);
if (this.options.ssl) {
Expand All @@ -235,7 +230,7 @@ class ConfigurableProxy extends EventEmitter {
this.proxyServer.on("upgrade", bound(this, this.handleProxyWs));

this.proxy.on("proxyRes", function (proxyRes, req, res) {
that.statsd.increment("requests." + proxyRes.statusCode, 1);
that.metrics.requestsProxyCount.labels(proxyRes.statusCode).inc();
});
}

Expand Down Expand Up @@ -265,8 +260,8 @@ class ConfigurableProxy extends EventEmitter {
var that = this;

return this._routes.add(path, data).then(() => {
this.updateLastActivity(path);
this.log.info("Route added %s -> %s", path, data.target);
that.updateLastActivity(path);
that.log.info("Route added %s -> %s", path, data.target);
});
}

Expand Down Expand Up @@ -341,7 +336,7 @@ class ConfigurableProxy extends EventEmitter {

res.write(JSON.stringify(results));
res.end();
that.statsd.increment("api.route.get", 1);
that.metrics.apiRouteGetCount.inc();
});
}

Expand All @@ -359,7 +354,7 @@ class ConfigurableProxy extends EventEmitter {
return this.addRoute(path, data).then(function () {
res.writeHead(201);
res.end();
that.statsd.increment("api.route.add", 1);
that.metrics.apiRouteAddCount.inc();
});
}

Expand All @@ -378,19 +373,19 @@ class ConfigurableProxy extends EventEmitter {
return p.then(() => {
res.writeHead(code);
res.end();
this.statsd.increment("api.route.delete", 1);
this.metrics.apiRouteDeleteCount.inc();
});
});
}

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

return this._routes.getTarget(path).then(function (route) {
timer.stop();
metricsTimerEnd();
if (route) {
return {
prefix: route.prefix,
Expand All @@ -401,7 +396,7 @@ class ConfigurableProxy extends EventEmitter {
}

updateLastActivity(prefix) {
var timer = this.statsd.createTimer("last_activity_updating");
var metricsTimerEnd = this.metrics.lastActivityUpdatingSummary.startTimer();
var routes = this._routes;

return routes
Expand All @@ -411,7 +406,7 @@ class ConfigurableProxy extends EventEmitter {
return routes.update(prefix, { last_activity: new Date() });
}
})
.then(timer.stop);
.then(metricsTimerEnd);
}

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

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

handleProxyWs(req, socket, head) {
// Proxy a websocket request
this.statsd.increment("requests.ws", 1);
this.metrics.requestsWsCount.inc();
return this.handleProxy("ws", req, socket, head);
}

handleProxyWeb(req, res) {
this.handleHealthCheck(req, res);
if (res.finished) return;
// Proxy a web request
this.statsd.increment("requests.web", 1);
this.metrics.requestsWebCount.inc();
return this.handleProxy("web", req, res);
}

Expand All @@ -623,11 +618,18 @@ class ConfigurableProxy extends EventEmitter {
}
}

handleMetrics(req, res) {
if (req.url === "/metrics") {
return this.metrics.render(res);
}
fail(req, res, 404);
}

handleApiRequest(req, res) {
// Handle a request to the REST API
this.statsd.increment("requests.api", 1);
if (res) {
res.on("finish", () => {
this.metrics.requestsApiCount.labels(res.statusCode).inc();
this.logResponse(req, res);
});
}
Expand Down
113 changes: 113 additions & 0 deletions lib/metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use strict";

var client = require("prom-client");

class Metrics {
constructor() {
this.register = new client.Registry();
client.collectDefaultMetrics({ register: this.register });

this.apiRouteGetCount = new client.Counter({
name: "api_route_get",
help: "Count of API route get requests",
registers: [this.register],
});

this.apiRouteAddCount = new client.Counter({
name: "api_route_add",
help: "Count of API route add requests",
registers: [this.register],
});

this.apiRouteDeleteCount = new client.Counter({
name: "api_route_delete",
help: "Count of API route delete requests",
registers: [this.register],
});

this.findTargetForReqSummary = new client.Summary({
name: "find_target_for_req",
help: "Summary of find target requests",
registers: [this.register],
});

this.lastActivityUpdatingSummary = new client.Summary({
name: "last_activity_updating",
help: "Summary of last activity updating requests",
registers: [this.register],
});

this.requestsWsCount = new client.Counter({
name: "requests_ws",
help: "Count of websocket requests",
registers: [this.register],
});

this.requestsWebCount = new client.Counter({
name: "requests_web",
help: "Count of web requests",
registers: [this.register],
});

this.requestsProxyCount = new client.Counter({
name: "requests_proxy",
help: "Count of proxy requests",
labelNames: ["status"],
registers: [this.register],
});

this.requestsApiCount = new client.Counter({
name: "requests_api",
help: "Count of API requests",
labelNames: ["status"],
registers: [this.register],
});
}

render(res) {
return this.register.metrics().then((s) => {
res.writeHead(200, { "Content-Type": this.register.contentType });
res.write(s);
res.end();
});
}
}

class MockMetrics {
constructor() {
return new Proxy(this, {
get(target, name) {
const mockCounter = new Proxy(
{},
{
get(target, name) {
if (name == "inc") {
return () => {};
}
if (name == "startTimer") {
return () => {
return () => {};
};
}
if (name == "labels") {
return () => {
return mockCounter;
};
}
},
}
);
return mockCounter;
},
});
}

render(res) {
return Promise.resolve();
}
}

module.exports = {
Metrics,
MockMetrics,
};
6 changes: 6 additions & 0 deletions lib/testutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,18 @@ exports.setupProxy = function (port, options, paths) {

servers.push(proxy.apiServer);
servers.push(proxy.proxyServer);
if (options.enableMetrics) {
servers.push(proxy.metricsServer);
}
proxy.apiServer.on("listening", onlisten);
proxy.proxyServer.on("listening", onlisten);

addTargets(proxy, paths || ["/"], port + 2).then(function () {
proxy.proxyServer.listen(port, ip);
proxy.apiServer.listen(port + 1, ip);
if (options.enableMetrics) {
proxy.metricsServer.listen(port + 3, ip);
}
});
return p;
};
Expand Down
Loading

0 comments on commit 9196b53

Please sign in to comment.