Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"dompurify": "^3.2.4",
"dropzone": "^6.0.0-beta.2",
"express": "^5.1.0",
"express-prom-bundle": "^8.0.0",
"file-saver": "^2.0.5",
"filesize": "^6.4.0",
"graphql": "^16.8.1",
Expand All @@ -82,6 +83,7 @@
"mermaid": "^11.10.0",
"morgan": "^1.10.1",
"pica": "^9.0.1",
"prom-client": "^15.1.3",
"query-string": "^6.14.1",
"react": "^18.2.0",
"react-autosuggest": "^10.1.0",
Expand Down
45 changes: 39 additions & 6 deletions client/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ import morgan from "morgan";

const BUILD_PATH = "./build/server/index.js";
const DEVELOPMENT = process.env.NODE_ENV === "development";
const PORT = Number.parseInt(process.env.PORT || "3000");
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const METRICS_ENABLED =
!!+process.env.METRICS_ENABLED ||
process.env.METRICS_ENABLED?.toLowerCase() === "true" ||
false;
const METRICS_PORT = Number.parseInt(process.env.METRICS_PORT || "9090", 10);

if (DEVELOPMENT) {
throw new Error("Can only run in production");
}

const app = express();

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

if (DEVELOPMENT) {
throw new Error("Can only run in production");
}

// eslint-disable-next-line no-console
console.log("Starting production server");

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

// Register metrics for application routes, we do not want to collect metrics for the routes above
if (METRICS_ENABLED) {
// eslint-disable-next-line no-console
console.log("Setting up metrics");

const metricsApp = express();
metricsApp.use(compression());
metricsApp.disable("x-powered-by");

await import(BUILD_PATH).then(
async (
/**
* @import * as ModuleType from './server/app'
* @type {ModuleType} */
mod
) => {
await mod.metrics({ app, metricsApp });
}
);

metricsApp.listen(METRICS_PORT, () => {
// eslint-disable-next-line no-console
console.log(
`Prometheus exporter is running at http://localhost:${METRICS_PORT}/metrics`
);
});
}

// Server-side rendering
app.use(
await import(BUILD_PATH).then(
Expand All @@ -121,5 +154,5 @@ app.use(

app.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log(`Server is running on http://localhost:${PORT}`);
console.log(`Server is running at http://localhost:${PORT}`);
});
2 changes: 2 additions & 0 deletions client/server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ app.use(
);

export * as constants from "./constants";

export { metrics } from "./metrics";
70 changes: 70 additions & 0 deletions client/server/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*!
* Copyright 2025 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { Express } from "express";
import type { PrometheusContentType, Registry } from "prom-client";

interface MetricsArgs {
app: Express;
metricsApp: Express;
}

export async function metrics({
app,
metricsApp,
}: MetricsArgs): Promise<Registry<PrometheusContentType>> {
// Initialize metrics
const promClient = await import("prom-client");
const promBundle = (await import("express-prom-bundle")).default;
const register = new promClient.Registry();

// Collect default metrics
promClient.collectDefaultMetrics({ register });

// Register the "prom-bundle" middleware
app.use(
promBundle({
autoregister: false,
includeMethod: true,
includeStatusCode: true,
metricsApp,
promRegistry: register,
})
);

// Collect HTTP requests total (App only)
const requestCounter = new promClient.Counter({
name: "http_requests_total",
help: "Total number of HTTP requests.",
labelNames: ["method", "status_code"],
registers: [register],
});
app.use(async (req, res, next) => {
res.on("close", () => {
requestCounter
.labels({
method: req.method,
status_code: res.statusCode,
})
.inc();
});
next();
});

return register;
}
Loading