diff --git a/NEWS.md b/NEWS.md index b08ff5c5..0fd4061e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,8 @@ -1.21.3 -- 2014-mm-dd +1.22.0 -- 2014-mm-dd -------------------- +New features: + - Health check endpoint 1.21.2 -- 2014-12-15 -------------------- @@ -19,6 +21,7 @@ Bugfixes: - Closes fd for log files on `kill -HUP` (#230) + 1.21.0 -- 2014-10-24 -------------------- diff --git a/config/environments/development.js.example b/config/environments/development.js.example index 86aea6ce..0fa65f1c 100644 --- a/config/environments/development.js.example +++ b/config/environments/development.js.example @@ -151,6 +151,14 @@ var config = { // X-Tiler-Profile header containing elapsed timing for various // steps taken for producing the response. ,useProfiler:true + // Settings for the health check available at /health + ,health: { + enabled: false, + username: 'localhost', + z: 0, + x: 0, + y: 0 + } }; module.exports = config; diff --git a/config/environments/production.js.example b/config/environments/production.js.example index 8d20f27a..efc73d2c 100644 --- a/config/environments/production.js.example +++ b/config/environments/production.js.example @@ -160,6 +160,14 @@ var config = { handler: 'inline' } } + // Settings for the health check available at /health + ,health: { + enabled: true, + username: 'localhost', + z: 0, + x: 0, + y: 0 + } }; module.exports = config; diff --git a/config/environments/staging.js.example b/config/environments/staging.js.example index 8b1ba7a8..6da02e15 100644 --- a/config/environments/staging.js.example +++ b/config/environments/staging.js.example @@ -160,6 +160,14 @@ var config = { handler: 'inline' } } + // Settings for the health check available at /health + ,health: { + enabled: false, + username: 'localhost', + z: 0, + x: 0, + y: 0 + } }; module.exports = config; diff --git a/config/environments/test.js.example b/config/environments/test.js.example index e2882331..7fcc9c7d 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -147,6 +147,14 @@ var config = { // X-Tiler-Profile header containing elapsed timing for various // steps taken for producing the response. ,useProfiler:true + // Settings for the health check available at /health + ,health: { + enabled: false, + username: 'localhost', + z: 0, + x: 0, + y: 0 + } }; module.exports = config; diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index 216d8682..63dc4081 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -5,6 +5,7 @@ var _ = require('underscore') , TemplateMaps = require('./template_maps.js') , Cache = require('./cache_validator') , os = require('os') + , HealthCheck = require('./monitoring/health_check') ; if ( ! process.env['PGAPPNAME'] ) @@ -665,6 +666,31 @@ var CartodbWindshaft = function(serverOptions) { // ---- Template maps interface ends @} + var healthCheck = new HealthCheck(cartoData, Windshaft.tilelive); + ws.get('/health', function(req, res) { + var healthConfig = global.environment.health || {}; + + if (!!healthConfig.enabled) { + var startTime = Date.now(); + healthCheck.check(healthConfig, function(err, result) { + var ok = !err; + var response = { + enabled: true, + ok: ok, + elapsed: Date.now() - startTime, + result: result + }; + if (err) { + response.err = err.message; + } + res.send(response, ok ? 200 : 503); + + }); + } else { + res.send({enabled: false, ok: true}, 200); + } + }); + return ws; }; diff --git a/lib/cartodb/monitoring/health_check.js b/lib/cartodb/monitoring/health_check.js new file mode 100644 index 00000000..873cd19d --- /dev/null +++ b/lib/cartodb/monitoring/health_check.js @@ -0,0 +1,90 @@ +var _ = require('underscore'), + dot = require('dot'), + fs = require('fs'), + path = require('path'), + Step = require('step'); + +function HealthCheck(metadataBackend, tilelive) { + this.metadataBackend = metadataBackend; + this.tilelive = tilelive; +} + +module.exports = HealthCheck; + + +var mapnikOptions = { + query: { + metatile: 1, + poolSize: 4, + bufferSize: 64 + }, + protocol: 'mapnik:', + slashes: true, + xml: null +}; + +var xmlTemplate = dot.template(fs.readFileSync(path.resolve(__dirname, 'map-config.xml'), 'utf-8')); + +HealthCheck.prototype.check = function(config, callback) { + + var self = this, + startTime, + result = { + redis: { + ok: false + }, + mapnik: { + ok: false + }, + tile: { + ok: false + } + }; + mapnikXmlParams = config; + + Step( + function getDBParams() { + startTime = Date.now(); + self.metadataBackend.getAllUserDBParams(config.username, this); + }, + function loadMapnik(err, dbParams) { + if (err) { + throw err; + } + result.redis = { + ok: !err, + elapsed: Date.now() - startTime, + size: Object.keys(dbParams).length + }; + mapnikOptions.xml = xmlTemplate(mapnikXmlParams); + + startTime = Date.now(); + self.tilelive.load(mapnikOptions, this); + }, + function getTile(err, source) { + if (err) { + throw err; + } + + result.mapnik = { + ok: !err, + elapsed: Date.now() - startTime + }; + + startTime = Date.now(); + source.getTile(config.z, config.x, config.y, this); + }, + function handleTile(err, tile) { + result.tile = { + ok: !err + }; + + if (tile) { + result.tile.elapsed = Date.now() - startTime; + result.tile.size = tile.length; + } + + callback(err, result); + } + ); +}; diff --git a/lib/cartodb/monitoring/map-config.xml b/lib/cartodb/monitoring/map-config.xml new file mode 100644 index 00000000..91a271e1 --- /dev/null +++ b/lib/cartodb/monitoring/map-config.xml @@ -0,0 +1,4 @@ + + diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e9f2e030..760a1f96 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -96,7 +96,7 @@ }, "inherits": { "version": "2.0.1", - "from": "inherits@2" + "from": "inherits@~2.0.1" } } } @@ -149,6 +149,7 @@ "rollbar": { "version": "0.3.13", "from": "rollbar@~0.3.13", + "resolved": "https://registry.npmjs.org/rollbar/-/rollbar-0.3.13.tgz", "dependencies": { "node-uuid": { "version": "1.4.2", @@ -302,21 +303,23 @@ "from": "tunnel-agent@~0.3.0" }, "http-signature": { - "version": "0.10.0", + "version": "0.10.1", "from": "http-signature@~0.10.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", "dependencies": { "assert-plus": { - "version": "0.1.2", - "from": "assert-plus@0.1.2" + "version": "0.1.5", + "from": "assert-plus@^0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" }, "asn1": { "version": "0.1.11", "from": "asn1@0.1.11" }, "ctype": { - "version": "0.5.2", - "from": "ctype@0.5.2", - "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.2.tgz" + "version": "0.5.3", + "from": "ctype@0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz" } } }, @@ -1615,9 +1618,9 @@ "resolved": "https://registry.npmjs.org/connect/-/connect-1.9.2.tgz", "dependencies": { "formidable": { - "version": "1.0.15", + "version": "1.0.16", "from": "formidable@1.0.x", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.15.tgz" + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.16.tgz" } } }, @@ -1681,7 +1684,7 @@ }, "sphericalmercator": { "version": "1.0.3", - "from": "sphericalmercator@~1.0.1", + "from": "sphericalmercator@~1.0.2", "resolved": "https://registry.npmjs.org/sphericalmercator/-/sphericalmercator-1.0.3.tgz" } } diff --git a/package.json b/package.json index 59c704a2..ef054cc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "name": "windshaft-cartodb", + "version": "1.22.0", "version": "1.21.3", "description": "A map tile server for CartoDB", "keywords": [ diff --git a/test/acceptance/health_check.js b/test/acceptance/health_check.js new file mode 100644 index 00000000..af8c361e --- /dev/null +++ b/test/acceptance/health_check.js @@ -0,0 +1,72 @@ +var helper = require(__dirname + '/../support/test_helper'); + +var assert = require('../support/assert'); +var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft'); +var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); +var server = new CartodbWindshaft(serverOptions); + +suite('health checks', function () { + + beforeEach(function (done) { + global.environment.health = { + enabled: true, + username: 'localhost', + z: 0, + x: 0, + y: 0 + }; + done(); + }); + + var healthCheckRequest = { + url: '/health', + method: 'GET', + headers: { + host: 'localhost' + } + }; + + test('returns 200 and ok=true with enabled configuration', function (done) { + assert.response(server, + healthCheckRequest, + { + status: 200 + }, + function (res, err) { + console.log(res.body); + assert.ok(!err); + + var parsed = JSON.parse(res.body); + + assert.ok(parsed.enabled); + assert.ok(parsed.ok); + + done(); + } + ); + }); + + test('fails for invalid user because it is not in redis', function (done) { + global.environment.health.username = 'invalid'; + + assert.response(server, + healthCheckRequest, + { + status: 503 + }, + function (res, err) { + assert.ok(!err); + + var parsed = JSON.parse(res.body); + + assert.equal(parsed.enabled, true); + assert.equal(parsed.ok, false); + + assert.equal(parsed.result.redis.ok, false); + + done(); + } + ); + }); + +});