Skip to content

Commit 683101d

Browse files
authored
feat: add prometheus metrics to UI (#3889)
1 parent dd81475 commit 683101d

5 files changed

Lines changed: 179 additions & 19 deletions

File tree

client/package-lock.json

Lines changed: 66 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"dompurify": "^3.2.4",
7070
"dropzone": "^6.0.0-beta.2",
7171
"express": "^5.1.0",
72+
"express-prom-bundle": "^8.0.0",
7273
"file-saver": "^2.0.5",
7374
"filesize": "^6.4.0",
7475
"graphql": "^16.8.1",
@@ -82,6 +83,7 @@
8283
"mermaid": "^11.10.0",
8384
"morgan": "^1.10.1",
8485
"pica": "^9.0.1",
86+
"prom-client": "^15.1.3",
8587
"query-string": "^6.14.1",
8688
"react": "^18.2.0",
8789
"react-autosuggest": "^10.1.0",

client/server.js

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,22 @@ import morgan from "morgan";
2222

2323
const BUILD_PATH = "./build/server/index.js";
2424
const DEVELOPMENT = process.env.NODE_ENV === "development";
25-
const PORT = Number.parseInt(process.env.PORT || "3000");
25+
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
26+
const METRICS_ENABLED =
27+
!!+process.env.METRICS_ENABLED ||
28+
process.env.METRICS_ENABLED?.toLowerCase() === "true" ||
29+
false;
30+
const METRICS_PORT = Number.parseInt(process.env.METRICS_PORT || "9090", 10);
31+
32+
if (DEVELOPMENT) {
33+
throw new Error("Can only run in production");
34+
}
2635

2736
const app = express();
2837

2938
app.use(compression());
3039
app.disable("x-powered-by");
3140

32-
if (DEVELOPMENT) {
33-
throw new Error("Can only run in production");
34-
}
35-
3641
// eslint-disable-next-line no-console
3742
console.log("Starting production server");
3843

@@ -107,6 +112,34 @@ if (process.env.CI !== "1") {
107112
// Client files
108113
app.use(express.static("build/client"));
109114

115+
// Register metrics for application routes, we do not want to collect metrics for the routes above
116+
if (METRICS_ENABLED) {
117+
// eslint-disable-next-line no-console
118+
console.log("Setting up metrics");
119+
120+
const metricsApp = express();
121+
metricsApp.use(compression());
122+
metricsApp.disable("x-powered-by");
123+
124+
await import(BUILD_PATH).then(
125+
async (
126+
/**
127+
* @import * as ModuleType from './server/app'
128+
* @type {ModuleType} */
129+
mod
130+
) => {
131+
await mod.metrics({ app, metricsApp });
132+
}
133+
);
134+
135+
metricsApp.listen(METRICS_PORT, () => {
136+
// eslint-disable-next-line no-console
137+
console.log(
138+
`Prometheus exporter is running at http://localhost:${METRICS_PORT}/metrics`
139+
);
140+
});
141+
}
142+
110143
// Server-side rendering
111144
app.use(
112145
await import(BUILD_PATH).then(
@@ -121,5 +154,5 @@ app.use(
121154

122155
app.listen(PORT, () => {
123156
// eslint-disable-next-line no-console
124-
console.log(`Server is running on http://localhost:${PORT}`);
157+
console.log(`Server is running at http://localhost:${PORT}`);
125158
});

client/server/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ app.use(
1111
);
1212

1313
export * as constants from "./constants";
14+
15+
export { metrics } from "./metrics";

client/server/metrics.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*!
2+
* Copyright 2025 - Swiss Data Science Center (SDSC)
3+
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
4+
* Eidgenössische Technische Hochschule Zürich (ETHZ).
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import type { Express } from "express";
20+
import type { PrometheusContentType, Registry } from "prom-client";
21+
22+
interface MetricsArgs {
23+
app: Express;
24+
metricsApp: Express;
25+
}
26+
27+
export async function metrics({
28+
app,
29+
metricsApp,
30+
}: MetricsArgs): Promise<Registry<PrometheusContentType>> {
31+
// Initialize metrics
32+
const promClient = await import("prom-client");
33+
const promBundle = (await import("express-prom-bundle")).default;
34+
const register = new promClient.Registry();
35+
36+
// Collect default metrics
37+
promClient.collectDefaultMetrics({ register });
38+
39+
// Register the "prom-bundle" middleware
40+
app.use(
41+
promBundle({
42+
autoregister: false,
43+
includeMethod: true,
44+
includeStatusCode: true,
45+
metricsApp,
46+
promRegistry: register,
47+
})
48+
);
49+
50+
// Collect HTTP requests total (App only)
51+
const requestCounter = new promClient.Counter({
52+
name: "http_requests_total",
53+
help: "Total number of HTTP requests.",
54+
labelNames: ["method", "status_code"],
55+
registers: [register],
56+
});
57+
app.use(async (req, res, next) => {
58+
res.on("close", () => {
59+
requestCounter
60+
.labels({
61+
method: req.method,
62+
status_code: res.statusCode,
63+
})
64+
.inc();
65+
});
66+
next();
67+
});
68+
69+
return register;
70+
}

0 commit comments

Comments
 (0)