This repository has been archived by the owner on Mar 27, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1367a01
Showing
8 changed files
with
7,497 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"env": { | ||
"node": true, | ||
"es6": true | ||
}, | ||
"extends": ["prettier"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.