diff --git a/NEWS.md b/NEWS.md index b08ff5c5..0a2a8916 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,14 @@ -1.21.3 -- 2014-mm-dd +1.22.1 -- 2015-mm-dd -------------------- +1.22.0 -- 2015-01-13 +-------------------- + +New features: + - Health check endpoint + + 1.21.2 -- 2014-12-15 -------------------- @@ -19,6 +26,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 f9bc51be..70046ffb 100644 --- a/config/environments/development.js.example +++ b/config/environments/development.js.example @@ -158,6 +158,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 69585a92..0d3a161c 100644 --- a/config/environments/production.js.example +++ b/config/environments/production.js.example @@ -167,6 +167,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 311eaaec..4e2cb673 100644 --- a/config/environments/staging.js.example +++ b/config/environments/staging.js.example @@ -167,6 +167,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 2b931f38..73e522bd 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -154,6 +154,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 b5537e85..032b096b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.21.3", + "version": "1.22.1", "dependencies": { "cartodb-psql": { "version": "0.4.0", @@ -308,21 +308,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" } } }, @@ -2142,7 +2144,36 @@ "torque.js": { "version": "2.7.1", "from": "https://github.com/CartoDB/torque/tarball/master", - "resolved": "https://github.com/CartoDB/torque/tarball/master" + "resolved": "https://github.com/CartoDB/torque/tarball/master", + "dependencies": { + "carto": { + "version": "0.15.1", + "from": "https://github.com/CartoDB/carto/archive/master.tar.gz", + "resolved": "https://github.com/CartoDB/carto/archive/master.tar.gz", + "dependencies": { + "mapnik-reference": { + "version": "6.0.5", + "from": "mapnik-reference@~6.0.2", + "resolved": "https://registry.npmjs.org/mapnik-reference/-/mapnik-reference-6.0.5.tgz" + }, + "optimist": { + "version": "0.6.1", + "from": "optimist@~0.6.0", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "from": "wordwrap@~0.0.2" + }, + "minimist": { + "version": "0.0.10", + "from": "minimist@~0.0.1" + } + } + } + } + } + } }, "request": { "version": "2.48.0", @@ -2240,21 +2271,23 @@ } }, "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" } } }, diff --git a/package.json b/package.json index 706a3aa2..d4cc02fb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.21.3", + "version": "1.22.1", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" 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(); + } + ); + }); + +});