Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support prometheus metrics #314

Merged
merged 19 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions bin/configurable-http-proxy
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,7 @@ 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("--disable-metrics", "Disable metrics endpoint")
.option("--log-level <loglevel>", "Log level (debug, info, warn, error)", "info")
.option(
"--timeout <n>",
Expand Down Expand Up @@ -266,14 +264,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);
}
// prometheus options
options.disableMetrics = args.disableMetrics;

// certs need to be provided for https redirection
if (!options.ssl && options.redirectPort) {
Expand Down
69 changes: 36 additions & 33 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 (options.disableMetrics) {
this.metrics = new metrics.MockMetrics();
} 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.Metrics();
}

if (this.options.defaultTarget) {
Expand Down Expand Up @@ -235,7 +224,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 +254,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 +330,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,13 +348,14 @@ 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();
});
}

deleteRoutes(req, res, path) {
// DELETE removes an existing route

var that = this;
return this._routes.get(path).then((result) => {
var p, code;
if (result) {
Expand All @@ -378,19 +368,19 @@ class ConfigurableProxy extends EventEmitter {
return p.then(() => {
res.writeHead(code);
res.end();
this.statsd.increment("api.route.delete", 1);
that.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 +391,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 +401,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 +420,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 +502,7 @@ class ConfigurableProxy extends EventEmitter {
this._handleProxyErrorDefault(code, kind, req, res);
return;
}
if (res.writableEnded) return; // response already done
minrk marked this conversation as resolved.
Show resolved Hide resolved
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 +593,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,12 +613,25 @@ class ConfigurableProxy extends EventEmitter {
}
}

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

handleApiRequest(req, res) {
// Handle a request to the REST API
this.statsd.increment("requests.api", 1);
if (!this.options.disableMetrics) {
var p = this.handleMetrics(req, res);
if (p) {
return p;
}
}
if (res) {
var that = this;
res.on("finish", () => {
this.logResponse(req, res);
that.metrics.requestsApiCount.labels(res.statusCode).inc();
that.logResponse(req, res);
});
}
var args = [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 });
minrk marked this conversation as resolved.
Show resolved Hide resolved
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,
};
Loading