Skip to content
This repository has been archived by the owner on Mar 27, 2023. It is now read-only.

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fatmatto committed Apr 10, 2020
0 parents commit 1367a01
Show file tree
Hide file tree
Showing 8 changed files with 7,497 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"env": {
"node": true,
"es6": true
},
"extends": ["prettier"]
}
59 changes: 59 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# express-prometheus

Based on [@trussle/tricorder](https://github.com/trussle/tricorder)

## Installation and Setup

WIP

## API

WIP
5 changes: 5 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as promClient from "prom-client";
import * as express from "express";

export const client: typeof promClient;
export declare function instrument (server: express.Express): void;
82 changes: 82 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const client = require("prom-client")

client.collectDefaultMetrics();

const metric = {
http: {
requests: {
clients: new client.Gauge({
name: "http_requests_processing",
help: "Http requests in progress",
labelNames: ["method", "path", "status"]
}),
throughput: new client.Counter({
name: "http_requests_total",
help: "Http request throughput",
labelNames: ["method", "path", "status"]
}),
duration: new client.Histogram({
name: "http_request_duration_seconds",
help: "Request duration histogram in seconds",
labelNames: ["method", "path", "status"]
})
}
}
};

function defaultOptions(options) {
options = options || {};
options.url = options.url || "/metrics";
return options;
}

function isPathExcluded(options, path) {
if (!options.excludePaths) {
return false
} else {
return options.excludePaths.some(pathToExclude => {
let regExp = new RegExp(pathToExclude)
return regExp.test(path)
})
}
}

function instrument(server, options) {
const opt = defaultOptions(options);

function middleware(req, res, next) {
// If path is the excluded paths list, we don't add a metric for it :)
if (isPathExcluded(opt, req.path)) {
return next()
}
if (req.path !== opt.url) {
const end = metric.http.requests.duration.startTimer();
metric.http.requests.clients.inc(1, Date.now());

res.on("finish", function () {
const labels = {
method: req.method,
path: req.route ? req.baseUrl + req.route.path : req.path,
status: res.statusCode
};

metric.http.requests.clients.dec(1, Date.now());
metric.http.requests.throughput.inc(labels, 1, Date.now());
end(labels);
});
}

return next();
}

server.use(middleware);

server.get(opt.url, (req, res) => {
res.header("content-type", "text/plain; charset=utf-8");
return res.send(client.register.metrics());
});
}
module.exports = {
client,
instrument
};
226 changes: 226 additions & 0 deletions lib/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
const supertest = require("supertest");
const express = require("express");
const tricorder = require("./index");

describe("index", () => {
let sut;

describe("options", () => {
beforeEach(() => {
sut = express();
});

test("When not given url, should default to metrics", () => {
// Arrange
tricorder.instrument(sut);
// Act
return supertest(sut)
.get("/metrics")
.then(result => {
// Assert
expect(result.statusCode).toBe(200);
});
});

test("When given url, should default to url value", () => {
// Arrange
const options = {
url: "/metric-route"
};
tricorder.instrument(sut, options);

// Act
return supertest(sut)
.get(options.url)
.then(result => {
// Assert
expect(result.statusCode).toBe(200);
});
});
});

describe("metrics", () => {
let agent;

beforeEach(() => {
sut = express();
tricorder.instrument(sut);

sut.get("/resource", (req, res) => {
res.send();
});

sut.get("/resource/:id", (req, res) => {
res.send();
});

agent = supertest.agent(sut);
});

describe("http_requests_processing", () => {
test("Given metrics are running, should include http_requests_processing", () => {
// Arrange
// Act
return agent.get("/resource").then(() => {
return agent.get("/metrics").then(result => {
expect(result.statusCode).toBe(200);
expect(result.headers["content-type"]).toBe(
"text/plain; charset=utf-8"
);
expect(result.text).toContain("http_requests_processing 0");
});
});
});
});

describe("http_requests_total", () => {
test("Given resource has been request, should being included in http_requests_total", () => {
// Arrange
// Act
return agent.get("/resource").then(() => {
return agent.get("/metrics").then(result => {
expect(result.statusCode).toBe(200);
expect(result.headers["content-type"]).toBe(
"text/plain; charset=utf-8"
);
expect(result.text).toContain(
'http_requests_total{method="GET",path="/resource",status="200"} 2'
);
});
});
});

test("Given resource/:id has been request, should being included in http_requests_total", () => {
// Arrange
const id = 8;

// Act
return agent.get(`/resource/${id}`).then(() => {
return agent.get("/metrics").then(result => {
expect(result.statusCode).toBe(200);
expect(result.headers["content-type"]).toBe(
"text/plain; charset=utf-8"
);
expect(result.text).toContain(
'http_requests_total{method="GET",path="/resource/:id",status="200"} 1'
);
});
});
});
});

describe("http_request_duration_seconds", () => {
test("Given resource has been request, should being included in http_request_duration_seconds", () => {
// Arrange
// Act
return agent.get("/resource").then(() => {
return agent.get("/metrics").then(result => {
expect(result.statusCode).toBe(200);
expect(result.headers["content-type"]).toBe(
"text/plain; charset=utf-8"
);
expect(result.text).toContain(
'http_request_duration_seconds_bucket{le="10",method="GET",path="/resource",status="200"} 3'
);
});
});
});

test("Given resource/:id has been request, should being included in http_request_duration_seconds", () => {
// Arrange
const id = 3;

// Act
return agent.get(`/resource/${id}`).then(() => {
return agent.get("/metrics").then(result => {
expect(result.statusCode).toBe(200);
expect(result.headers["content-type"]).toBe(
"text/plain; charset=utf-8"
);
expect(result.text).toContain(
'http_request_duration_seconds_bucket{le="10",method="GET",path="/resource/:id",status="200"} 2'
);
});
});
});
});

describe("third party metrics", () => {

test("Given metrics running, should include default node metrics", () => {
// Arrange
// Act
return agent.get("/metrics").then(result => {
// Assert
expect(result.text).toContain("nodejs_active_handles_total");
expect(result.text).toContain("nodejs_active_requests_total");
expect(result.text).toContain("nodejs_external_memory_bytes");
});
});
});
});

describe("labels", () => {
let agent;

beforeEach(() => {
sut = express();
tricorder.instrument(sut);

const router = express.Router();

router.get("/", (req, res) => {
res.send();
});

router.get("/:id", (req, res) => {
res.send();
});

sut.use("/resource-1", router);
sut.use("/resource-2", router);

sut.get("/resource-3/", (req, res) => {
res.send();
});

sut.get("/resource-3/:id", (req, res) => {
res.send();
});

agent = supertest.agent(sut);
});

test("Given a router route is called, should calculate path correctly", () => {
// Arrange
// Act
return agent.get("/resource-1/id-thing").then(() => {
return agent.get("/metrics").then(result => {
expect(result.statusCode).toBe(200);
expect(result.headers["content-type"]).toBe(
"text/plain; charset=utf-8"
);
expect(result.text).toContain(
'http_requests_total{method="GET",path="/resource-1/:id",status="200"} 1'
);
});
});
});

test("Given a app route is called, should calculate path correctly", () => {
// Arrange
// Act
return agent.get("/resource-3/id-thing-3").then(() => {
return agent.get("/metrics").then(result => {
expect(result.statusCode).toBe(200);
expect(result.headers["content-type"]).toBe(
"text/plain; charset=utf-8"
);
expect(result.text).toContain(
'http_requests_total{method="GET",path="/resource-3/:id",status="200"} 1'
);
});
});
});
});
});

0 comments on commit 1367a01

Please sign in to comment.