Skip to content

Commit 8e3e68e

Browse files
committed
feat: Implement /_healthcheck
This checks the health of the adapter and present it with meta information about the deployment. Fixes #55
1 parent de091c0 commit 8e3e68e

File tree

8 files changed

+266
-137
lines changed

8 files changed

+266
-137
lines changed

src/db.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ function initDbAdapter(options) {
44
const adapterName = options.adapter;
55
const adapter = require(`micro-analytics-adapter-${adapterName}`);
66

7+
module.exports.version = adapter.version;
8+
module.exports.healthcheck = adapter.healthcheck;
79
module.exports.get = adapter.get;
810
module.exports.getAll = adapter.getAll;
911
module.exports.put = adapter.put;

src/handler.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const url = require('url');
22
const { send, createError, sendError } = require('micro');
33

44
const db = require('./db');
5+
const healthcheckHandler = require('./healthcheck');
56
const { pushView } = require('./utils');
67

78
let sse;
@@ -73,14 +74,19 @@ async function analyticsHandler(req, res) {
7374
}
7475
}
7576

76-
module.exports = async function(req, res) {
77-
const { pathname, query } = url.parse(req.url, /* parseQueryString */ true);
77+
module.exports = function createHandler(options) {
78+
return async function(req, res) {
79+
const { pathname, query } = url.parse(req.url, /* parseQueryString */ true);
7880

79-
switch (pathname) {
80-
case '/_realtime':
81-
return realtimeHandler(req, res);
81+
switch (pathname) {
82+
case '/_realtime':
83+
return realtimeHandler(req, res);
8284

83-
default:
84-
return analyticsHandler(req, res);
85-
}
85+
case '/_healthcheck':
86+
return healthcheckHandler(options, req, res);
87+
88+
default:
89+
return analyticsHandler(req, res);
90+
}
91+
};
8692
};

src/healthcheck.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const { send } = require('micro');
2+
3+
const pkg = require('../package.json');
4+
const db = require('./db');
5+
6+
module.exports = async function healthcheckHandler(options, req, res) {
7+
const health = db.hasFeature('healthcheck')
8+
? await db.healthcheck()
9+
: 'unknown';
10+
11+
send(res, health === 'ok' ? 200 : 500, {
12+
health,
13+
version: pkg.version,
14+
adapter: {
15+
name: options.adapter,
16+
version: db.version,
17+
features: {
18+
realtime: db.hasFeature('subscribe'),
19+
},
20+
},
21+
});
22+
};

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ try {
88

99
db.initDbAdapter(flags);
1010

11-
const handler = require('./handler');
11+
const handler = require('./handler')(flags);
1212
const server = micro(handler);
1313

1414
server.listen(flags.port, flags.host, error => {

tests/atomicity.test.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
1-
const request = require('request-promise')
2-
const { listen, mockDb } = require('./utils')
1+
const request = require('request-promise');
2+
const { listen, mockDb } = require('./utils');
33

4-
jest.mock('flat-file-db', () => mockDb)
5-
const db = require('../src/db')
6-
const service = require('../src/handler')
7-
let url
4+
jest.mock('flat-file-db', () => mockDb);
5+
const db = require('../src/db');
6+
const service = require('../src/handler');
7+
let url;
88

99
beforeAll(() => {
1010
db.initDbAdapter({ adapter: 'flat-file-db' });
11-
})
11+
});
1212

1313
beforeEach(async () => {
14-
url = await listen(service)
15-
mockDb._setDelay(10)
16-
})
14+
url = await listen(service({ adapter: 'flat-file-db' }));
15+
mockDb._setDelay(10);
16+
});
1717

1818
afterEach(async () => {
19-
mockDb._reset()
20-
mockDb._setDelay()
21-
})
19+
mockDb._reset();
20+
mockDb._setDelay();
21+
});
2222

2323
it('should atomically set two views coming in at the same time', async () => {
2424
// Request twice at the same time
2525
// NOTE: These two requests will return a wrong view count, they'll both say it's 1
26-
request(`${url}/path`)
27-
request(`${url}/path`)
26+
request(`${url}/path`);
27+
request(`${url}/path`);
2828
// After the data is persisted (in the mocked case after 10ms) the path shows the right view count
2929
setTimeout(async () => {
30-
const body = JSON.parse(await request(`${url}/path`))
31-
expect(body.views).toEqual(3)
32-
}, 10)
33-
})
30+
const body = JSON.parse(await request(`${url}/path`));
31+
expect(body.views).toEqual(3);
32+
}, 10);
33+
});

tests/errors.test.js

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
1-
const request = require('request-promise')
2-
const { listen } = require('./utils')
1+
const request = require('request-promise');
2+
const { listen } = require('./utils');
33

4-
const service = require('../src/handler')
5-
const db = require('../src/db')
6-
let url
4+
const service = require('../src/handler');
5+
const db = require('../src/db');
6+
let url;
77

88
beforeAll(() => {
99
db.initDbAdapter({ adapter: 'flat-file-db' });
10-
})
10+
});
1111

1212
beforeEach(async () => {
13-
url = await listen(service)
14-
})
13+
url = await listen(service({ adapter: 'flat-file-db' }));
14+
});
1515

1616
it('should throw an error if no pathname is provided', async () => {
17-
const fn = jest.fn()
17+
const fn = jest.fn();
1818
try {
19-
await request(url)
20-
fn()
19+
await request(url);
20+
fn();
2121
} catch (err) {
22-
expect(err.statusCode).toBe(400)
23-
expect(err.message.indexOf('include a path')).toBeGreaterThan(-1)
22+
expect(err.statusCode).toBe(400);
23+
expect(err.message.indexOf('include a path')).toBeGreaterThan(-1);
2424
}
25-
expect(fn).not.toHaveBeenCalled()
26-
})
25+
expect(fn).not.toHaveBeenCalled();
26+
});
2727

2828
it('should throw an error if a PUT request comes in', async () => {
29-
const fn = jest.fn()
29+
const fn = jest.fn();
3030
try {
31-
await request.put(`${url}/test`)
32-
expect(err.message.indexOf('make a GET or a POST request')).toBeGreaterThan(-1)
33-
fn()
31+
await request.put(`${url}/test`);
32+
expect(err.message.indexOf('make a GET or a POST request')).toBeGreaterThan(
33+
-1
34+
);
35+
fn();
3436
} catch (err) {
35-
expect(err.statusCode).toBe(400)
37+
expect(err.statusCode).toBe(400);
3638
}
37-
expect(fn).not.toHaveBeenCalled()
38-
})
39+
expect(fn).not.toHaveBeenCalled();
40+
});

tests/healthcheck.test.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const request = require('request-promise');
2+
3+
const db = require('../src/db');
4+
const { listen } = require('./utils');
5+
6+
let mockStatus = 'ok';
7+
jest.mock('micro-analytics-adapter-flat-file-db', () => ({
8+
version: '42.0.0',
9+
subscribe: () => {},
10+
healthcheck: async () => {
11+
await new Promise(resolve => setTimeout(resolve, 20));
12+
return mockStatus;
13+
},
14+
}));
15+
16+
jest.mock('micro-analytics-adapter-memory', () => ({
17+
version: '42.0.0',
18+
}));
19+
20+
const service = require('../src/handler')({ adapter: 'flat-file-db' });
21+
22+
let url;
23+
24+
beforeAll(() => {
25+
db.initDbAdapter({ adapter: 'flat-file-db' });
26+
});
27+
28+
beforeEach(async () => {
29+
url = await listen(service);
30+
});
31+
32+
test('GET /_healtcheck adapter healthcheck returns "ok"', async () => {
33+
mockStatus = 'ok';
34+
const body = JSON.parse(await request(`${url}/_healthcheck`));
35+
36+
expect(body).toMatchObject({
37+
health: 'ok',
38+
adapter: {
39+
name: 'flat-file-db',
40+
version: '42.0.0',
41+
features: {
42+
realtime: true,
43+
},
44+
},
45+
});
46+
});
47+
48+
test('GET /_healtcheck adapter healthcheck returns "critical"', async () => {
49+
mockStatus = 'critical';
50+
let error;
51+
52+
try {
53+
const body = JSON.parse(await request(`${url}/_healthcheck`));
54+
} catch (e) {
55+
error = e;
56+
}
57+
58+
expect(error.statusCode).toEqual(500);
59+
expect(JSON.parse(error.error)).toMatchObject({
60+
health: 'critical',
61+
adapter: {
62+
name: 'flat-file-db',
63+
version: '42.0.0',
64+
features: {
65+
realtime: true,
66+
},
67+
},
68+
});
69+
});
70+
71+
test('GET /_healtcheck when adapter has no healthcheck', async () => {
72+
let error;
73+
url = await listen(require('../src/handler')({ adapter: 'memory' }));
74+
db.initDbAdapter({ adapter: 'memory' });
75+
76+
try {
77+
const body = JSON.parse(await request(`${url}/_healthcheck`));
78+
} catch (e) {
79+
error = e;
80+
}
81+
82+
expect(error.statusCode).toEqual(500);
83+
expect(JSON.parse(error.error)).toMatchObject({
84+
health: 'unknown',
85+
adapter: {
86+
name: 'memory',
87+
version: '42.0.0',
88+
features: {
89+
realtime: false,
90+
},
91+
},
92+
});
93+
});

0 commit comments

Comments
 (0)