Compare commits

...

1 Commits

Author SHA1 Message Date
alexandre merle 38df40a520 feature: S3C-4073: backport probe server
Backporting probe server which will be use to setting health server
check and metrics gathering
2021-04-27 14:14:40 +02:00
2 changed files with 334 additions and 0 deletions

View File

@ -0,0 +1,99 @@
const httpServer = require('../http/server');
const werelogs = require('werelogs');
const errors = require('../../errors');
const ZenkoMetrics = require('../../metrics/ZenkoMetrics');
function sendError(res, log, error, optMessage) {
res.writeHead(error.code);
let message;
if (optMessage) {
message = optMessage;
} else {
message = error.description || '';
}
log.debug('sending back error response', { httpCode: error.code,
errorType: error.message,
error: message });
res.end(`${JSON.stringify({ errorType: error.message,
errorMessage: message })}\n`);
}
function sendSuccess(res, log, msg) {
res.writeHead(200);
log.debug('replying with success');
const message = msg || 'OK';
res.end(message);
}
function checkStub(log) { // eslint-disable-line
return true;
}
class HealthProbeServer extends httpServer {
constructor(params) {
const logging = new werelogs.Logger('HealthProbeServer');
super(params.port, logging);
this.logging = logging;
this._metrics = params.metrics || ZenkoMetrics;
this.setBindAddress(params.bindAddress || 'localhost');
// hooking our request processing function by calling the
// parent's method for that
this.onRequest(this._onRequest);
this._reqHandlers = {
'/_/health/liveness': this._onLiveness.bind(this),
'/_/health/readiness': this._onReadiness.bind(this),
'/_/monitoring/metrics': this._onMetrics.bind(this),
'/_/metrics': this._onMetrics.bind(this),
};
this._livenessCheck = params.livenessCheck || checkStub;
this._readinessCheck = params.readinessCheck || checkStub;
}
onLiveCheck(f) {
this._livenessCheck = f;
}
onReadyCheck(f) {
this._readinessCheck = f;
}
_onRequest(req, res) {
const log = this.logging.newRequestLogger();
log.debug('request received', { method: req.method,
url: req.url });
if (req.method !== 'GET') {
sendError(res, log, errors.MethodNotAllowed);
}
if (req.url in this._reqHandlers) {
this._reqHandlers[req.url](req, res, log);
} else {
sendError(res, log, errors.InvalidURI);
}
}
_onLiveness(req, res, log) {
if (this._livenessCheck(log)) {
sendSuccess(res, log);
} else {
sendError(res, log, errors.ServiceUnavailable);
}
}
_onReadiness(req, res, log) {
if (this._readinessCheck(log)) {
sendSuccess(res, log);
} else {
sendError(res, log, errors.ServiceUnavailable);
}
}
// expose metrics to Prometheus
_onMetrics(req, res) {
res.writeHead(200, {
'Content-Type': this._metrics.asPrometheusContentType(),
});
res.end(this._metrics.asPrometheus());
}
}
module.exports = HealthProbeServer;

View File

@ -0,0 +1,235 @@
const assert = require('assert');
const HealthProbeServer =
require('../../../../lib/network/probe/HealthProbeServer');
const http = require('http');
function makeRequest(meth, uri) {
const params = {
hostname: 'localhost',
port: 4042,
method: meth,
path: uri,
};
const req = http.request(params);
req.setNoDelay(true);
return req;
}
const healthcheckEndpoints = [
'/_/health/liveness',
'/_/health/readiness',
];
const badHealthcheckEndpoints = [
'/_/health/liveness_thisiswrong',
'/_/health/readiness_thisiswrong',
];
describe('network.probe.HealthProbeServer', () => {
describe('service is "up"', () => {
let server;
function setup(done) {
server = new HealthProbeServer({ port: 4042 });
server._cbOnListening = done;
server.start();
}
before(done => {
setup(done);
});
after(done => {
server.stop();
done();
});
healthcheckEndpoints.forEach(ep => {
it('should perform a GET and ' +
'return 200 OK', done => {
makeRequest('GET', ep)
.on('response', res => {
assert(res.statusCode === 200);
done();
})
.on('error', err => {
assert.ifError(err);
done();
}).end();
});
});
});
describe('service is "down"', () => {
let server;
function setup(done) {
function falseStub() {
return false;
}
server = new HealthProbeServer({
port: 4042,
livenessCheck: falseStub,
readinessCheck: falseStub,
});
server.start();
done();
}
before(done => {
setup(done);
});
after(done => {
server.stop();
done();
});
healthcheckEndpoints.forEach(ep => {
it('should perform a GET and ' +
'return 503 ServiceUnavailable', done => {
makeRequest('GET', ep)
.on('response', res => {
assert(res.statusCode === 503);
done();
})
.on('error', err => {
assert.ifError(err);
done();
}).end();
});
});
});
describe('Invalid Methods', () => {
let server;
function setup(done) {
server = new HealthProbeServer({
port: 4042,
});
server.start();
done();
}
before(done => {
setup(done);
});
after(done => {
server.stop();
done();
});
healthcheckEndpoints.forEach(ep => {
it('should perform a POST and ' +
'return 405 MethodNotAllowed', done => {
makeRequest('POST', ep)
.on('response', res => {
assert(res.statusCode === 405);
done();
})
.on('error', err => {
assert.ifError(err);
done();
}).end();
});
});
});
describe('Invalid URI', () => {
let server;
function setup(done) {
server = new HealthProbeServer({
port: 4042,
});
server.start();
done();
}
before(done => {
setup(done);
});
after(done => {
server.stop();
done();
});
badHealthcheckEndpoints.forEach(ep => {
it('should perform a GET and ' +
'return 400 InvalidURI', done => {
makeRequest('GET', ep)
.on('response', res => {
assert(res.statusCode === 400);
done();
})
.on('error', err => {
assert.ifError(err);
done();
}).end();
});
});
});
describe('metrics route', () => {
let server;
function setup(done) {
server = new HealthProbeServer({ port: 4042 });
server._cbOnListening = done;
server.start();
}
before(done => {
setup(done);
});
after(done => {
server.stop();
done();
});
it('should expose metrics 1/2', done => {
makeRequest('GET', '/_/monitoring/metrics')
.on('response', res => {
assert(res.statusCode === 200);
const respBufs = [];
res.on('data', data => {
respBufs.push(data);
});
res.on('end', () => {
const respContents = respBufs.join('');
assert(respContents.length > 0);
done();
});
res.on('error', err => {
assert.ifError(err);
done();
});
})
.on('error', err => {
assert.ifError(err);
done();
}).end();
});
it('should expose metrics 2/2', done => {
makeRequest('GET', '/_/metrics')
.on('response', res => {
assert(res.statusCode === 200);
const respBufs = [];
res.on('data', data => {
respBufs.push(data);
});
res.on('end', () => {
const respContents = respBufs.join('');
assert(respContents.length > 0);
done();
});
res.on('error', err => {
assert.ifError(err);
done();
});
})
.on('error', err => {
assert.ifError(err);
done();
}).end();
});
});
});