'use strict'; /* eslint-env jasmine */ const express = require('express'); const supertest = require('supertest'); const bundle = require('../'); const Koa = require('koa'); const c2k = require('koa-connect'); const supertestKoa = require('supertest-koa-agent'); const promClient = require('prom-client'); const cluster = require('cluster'); describe('index', () => { beforeEach(() => { promClient.register.clear(); }); it('metrics returns up=1', done => { const app = express(); const bundled = bundle({ excludeRoutes: ['/irrelevant', /at.all/] }); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent.get('/test').end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/up\s1/); done(); }); }); }); it('"up"-metric can be excluded', done => { const app = express(); const bundled = bundle({ includeUp: false }); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent.get('/test').end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).not.toMatch(/up\s1/); done(); }); }); }); it('metrics path can be defined with a regex', done => { const app = express(); const bundled = bundle({ metricsPath: /^\/prometheus$/ }); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent.get('/test').end(() => { agent .get('/prometheus') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/up\s1/); done(); }); }); }); it('metrics path can be defined as regexp', done => { const app = express(); const bundled = bundle(); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent.get('/test').end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/up\s1/); done(); }); }); }); it('httpDurationMetricName overrides histogram metric name', done => { const app = express(); const bundled = bundle({ httpDurationMetricName: 'my_http_duration' }); app.use(bundled); const agent = supertest(app); agent.get('/metrics') .end((err, res) => { expect(res.text).toMatch(/my_http_duration/m); done(); }); }); it('metrics should be attached to /metrics by default', done => { const app = express(); const bundled = bundle(); app.use(bundled); const agent = supertest(app); agent.get('/metrics') .end((err, res) => { expect(res.status).toBe(200); done(); }); }); it('metrics can be attached to /metrics programatically', done => { const app = express(); const bundled = bundle({ autoregister: false }); app.use(bundled.metricsMiddleware); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent.get('/metrics') .end((err, res) => { expect(res.status).toBe(200); done(); }); }); it('returns error 500 on incorrect middleware usage', done => { const app = express(); app.use(bundle); supertest(app) .get('/metrics') .end((err, res) => { expect(res.status).toBe(500); done(); }); }); it('http latency gets counted', done => { const app = express(); const instance = bundle(); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/test') .end(() => { const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap; expect(metricHashMap['status_code:200']).toBeDefined(); const labeled = metricHashMap['status_code:200']; expect(labeled.count).toBe(1); agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); done(); }); }); }); it('filters out the excludeRoutes', done => { const app = express(); const instance = bundle({ excludeRoutes: ['/test', /bad.word/] }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); app.use('/some/bad-word', (req, res) => res.send('it worked too')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/some/bad-word') .end(() => { const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap; expect(metricHashMap['status_code:200']).not.toBeDefined(); agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); done(); }); }); }); }); it('bypass requests', done => { const app = express(); const instance = bundle({ bypass: (req)=> { // metrics added here to attempt skipping /metrics // this should fail though, because serving /metrics preceeds bypassing return !!req.url.match(/test|bad.word|metrics/); } }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); app.use('/some/bad-word', (req, res) => res.send('it worked too')); app.use('/good-word', (req, res) => res.send('this will be counted')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/some/bad-word') .end(() => { agent .get('/good-word') .end(() => { const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap; expect(metricHashMap['status_code:200']).toBeDefined(); // only /good-word should be counted expect(metricHashMap['status_code:200'].count).toBe(1); agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); done(); }); }); }); }); }); it('complains about deprecated options', () => { expect(() => bundle({prefix: 'hello'})).toThrow(); }); it('tolerates includePath, includeMethod, includeCustomLabels', done => { const app = express(); const instance = bundle({ includePath: true, includeMethod: true, includeCustomLabels: {foo: 'bar'} }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); done(); }); }); }); it('metric type histogram works', done => { const app = express(); const bundled = bundle({ metricType: 'histogram', buckets: [10, 100], }); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent.get('/test').end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/le="100"/); done(); }); }); }); it('throws on unknown metricType ', () => { expect(() => { bundle({metricType: 'hello'}); }).toThrow(); }); describe('usage of normalizePath()', () => { it('normalizePath can be replaced gloablly', done => { const app = express(); const original = bundle.normalizePath; bundle.normalizePath = () => 'dummy'; const instance = bundle({ includePath: true, }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/"dummy"/m); bundle.normalizePath = original; done(); }); }); }); it('normalizePath function can be overridden', done => { const app = express(); const instance = bundle({ includePath: true, normalizePath: req => req.originalUrl + '-suffixed' }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/"\/test-suffixed"/m); done(); }); }); }); it('normalizePath can be passed as an array of [regex, replacement]', done => { const app = express(); const instance = bundle({ includePath: true, normalizePath: [ ['^/docs/whatever/.*$', '/docs'], [/^\/docs$/, '/mocks'] ] }); app.use(instance); app.use('/docs/whatever/:andmore', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/docs/whatever/unimportant') .end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/"\/mocks"/m); done(); }); }); }); }); it('formatStatusCode can be overridden', done => { const app = express(); const instance = bundle({ formatStatusCode: () => 555 }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/555/); done(); }); }); }); it('includeStatusCode=false removes status_code label from metrics', done => { const app = express(); const instance = bundle({ includeStatusCode: false }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).not.toMatch(/="200"/); done(); }); }); }); it('handles errors in collectors', done => { const app = express(); const instance = bundle({}); app.use(instance); new promClient.Gauge({ name: 'kaboom', help: 'this metric explodes', collect() { throw new Error('kaboom!'); } }); // the error will NOT be displayed if NODE_ENV=test (as defined in package.json) supertest(app) .get('/metrics') .expect(500) .end((err) => done(err)); }); it('customLabels={foo: "bar"} adds foo="bar" label to metrics', done => { const app = express(); const instance = bundle({ customLabels: {foo: 'bar'} }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/foo="bar"/); done(); }); }); }); it('tarnsformLabels can set label values', done => { const app = express(); const instance = bundle({ includePath: true, customLabels: {foo: 'bar'}, transformLabels: labels => { labels.foo = 'baz'; labels.path += '/ok'; } }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent .get('/test') .end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/foo="baz"/); expect(res.text).toMatch(/path="\/test\/ok"/); done(); }); }); }); it('Koa: metrics returns up=1', done => { const app = new Koa(); const bundled = bundle(); app.use(c2k(bundled)); app.use(function(ctx, next) { return next().then(() => ctx.body = 'it worked'); }); const agent = supertestKoa(app); agent.get('/test').end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/^up\s1/m); done(); }); }); }); it('calls promClient.collectDefaultMetrics', () => { const spy = spyOn(promClient, 'collectDefaultMetrics'); bundle({ promClient: { collectDefaultMetrics: { } } }); expect(spy).toHaveBeenCalledWith({}); }); describe('usage of clusterMetrics()', () => { it('clusterMetrics returns 200 even without a cluster', (done) => { const app = express(); cluster.workers = []; app.use('/cluster_metrics', bundle.clusterMetrics()); const agent = supertest(app); agent .get('/cluster_metrics') .end((err, res) => { expect(res.status).toBe(200); done(); }); }); it('clusterMetrics returns 500 in case of an error', (done) => { const app = express(); app.use('/cluster_metrics', bundle.clusterMetrics()); const agent = supertest(app); // create a fake worker, which would not respond in time cluster.workers = [{ isConnected: () => true, send: () => {} }]; const errorSpy = spyOn(console, 'error'); // mute console.error agent .get('/cluster_metrics') .end((err, res) => { expect(res.status).toBe(500); expect(errorSpy).toHaveBeenCalled(); done(); }); }, 6000); }); describe('metricType: summary', () => { it('metric type summary works', done => { const app = express(); const bundled = bundle({ metricType: 'summary' }); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent.get('/test').end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/quantile="0.98"/); done(); }); }); }); it('custom pecentiles work', done => { const app = express(); const bundled = bundle({ metricType: 'summary', percentiles: [0.5, 0.85, 0.99], }); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); const agent = supertest(app); agent.get('/test').end(() => { agent .get('/metrics') .end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/quantile="0.85"/); done(); }); }); }); it('additional metricsApp can be used', done => { const app = express(); const metricsApp = express(); const bundled = bundle({metricsApp}); app.use(bundled); const agent = supertest(app); const metricsAgent = supertest(metricsApp); agent.get('/').end(() => { metricsAgent.get('/metrics').end((err, res) => { expect(res.status).toBe(200); expect(res.text).toMatch(/status_code="404"/); done(); }); }); }); }); });