Skip to content

Commit

Permalink
Add protected prometheus metrics endpoint at website
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikita Pavlovskiy committed Aug 15, 2023
1 parent 969c7c5 commit 78b5c07
Show file tree
Hide file tree
Showing 11 changed files with 845 additions and 83 deletions.
4 changes: 4 additions & 0 deletions .k8s/prod/carres-website-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ apiVersion: v1
kind: Service
metadata:
name: carres-website
annotations:
prometheus.io/path: "/api/metrics"
prometheus.io/port: "3000"
prometheus.io/scrape: "true"
spec:
selector:
app: carres-website
Expand Down
821 changes: 739 additions & 82 deletions apps/website/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
"cookies-next": "^2.1.1",
"eslint": "8.31.0",
"eslint-config-next": "13.1.1",
"jsonwebtoken": "^9.0.1",
"jwks-rsa": "^3.0.1",
"next": "13.1.1",
"next-auth": "^4.18.8",
"prom-client": "^14.2.0",
"react": "18.2.0",
"react-bootstrap": "^2.7.0",
"react-datepicker": "^4.11.0",
Expand Down
22 changes: 22 additions & 0 deletions apps/website/pages/api/metrics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextApiRequest, NextApiResponse } from "next";
import { register } from "prom-client";
import { verifyToken } from "../../../utils/monitoring/metricsCollectorTokenVerifier";

export default async (req: NextApiRequest, res: NextApiResponse) => {
const accessTokenBearer = req.headers["authorization"];

if (!accessTokenBearer) {
res.status(401);
return res.end();
}

const accessToken = accessTokenBearer.slice("Bearer ".length);

if (!(await verifyToken(accessToken, "metrics-scraper"))) {
res.status(401);
return res.end();
}

res.setHeader("Content-type", register.contentType);
res.send(await register.metrics());
};
3 changes: 3 additions & 0 deletions apps/website/pages/create-reservation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getCars } from '../utils/client/apiClient';
import Car from '../utils/client/types/Car';
import { SharedSessionData, getSessionData } from '../utils/session/getSharedSessionData';
import OGTags from '../components/OGTags';
import { addHttpVisit } from '../utils/monitoring/prometheus';

interface NewReservationScreenProps extends SharedSessionData { }

Expand Down Expand Up @@ -70,6 +71,8 @@ export default function NewReservationScreen({ isManager, idToken, needsReservat
export const getServerSideProps: GetServerSideProps = async ({ res, req }) => {
const sharedSessionData = await getSessionData(req);

addHttpVisit(req.url ?? "?", req.headers["user-agent"] ?? "?");

return {
props: { ...sharedSessionData }
};
Expand Down
3 changes: 3 additions & 0 deletions apps/website/pages/create-reservation/[carId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Car from '../../utils/client/types/Car';
import { SharedSessionData, getSessionData } from '../../utils/session/getSharedSessionData';
import CarReservationForm from './CarReservationForm';
import OGTags from '../../components/OGTags';
import { addHttpVisit } from '../../utils/monitoring/prometheus';

interface CarReservationPageProps extends SharedSessionData { }

Expand Down Expand Up @@ -77,6 +78,8 @@ const getYearFromDateString = (dateString: string): string => {

export const getServerSideProps: GetServerSideProps = async ({ res, req, params }) => {
const sharedSessionData = await getSessionData(req);

addHttpVisit(req.url ?? "?", req.headers["user-agent"] ?? "?");

return {
props: {
Expand Down
3 changes: 3 additions & 0 deletions apps/website/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useEffect, useState } from 'react';
import { getUsersReservations, cancelReservation as cancelReservationApi } from '../../utils/client/apiClient';
import AdminUserEmailForm from '../../components/AdminUserEmailForm';
import OGTags from '../../components/OGTags';
import { addHttpVisit } from '../../utils/monitoring/prometheus';

interface DashboardProps extends SharedSessionData { }

Expand Down Expand Up @@ -138,6 +139,8 @@ const getButtonsForAllPages = (totalPages: number, currentPage: number, onClick:
export const getServerSideProps: GetServerSideProps = async ({ res, req }) => {
const sharedSessionData = await getSessionData(req);

addHttpVisit(req.url ?? "?", req.headers["user-agent"] ?? "?");

return {
props: { ...sharedSessionData }
};
Expand Down
4 changes: 4 additions & 0 deletions apps/website/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { getReservations } from '../utils/client';
import Reservation from '../utils/client/types/Reservation';
import { SharedSessionData, getSessionData } from '../utils/session/getSharedSessionData';
import OGTags from '../components/OGTags';
import { addHttpVisit } from '../utils/monitoring/prometheus';
import { userAgent } from 'next/server';

const RESERVATION_COOKIE_NAME = "i_need_reservations";

Expand Down Expand Up @@ -99,6 +101,8 @@ export default function Home({ needsReservations: needsReservationsServerSide, i
export const getServerSideProps: GetServerSideProps = async ({ res, req }) => {
const sharedSessionData = await getSessionData(req);

addHttpVisit(req.url ?? "?", req.headers["user-agent"] ?? "?");

return {
props: { ...sharedSessionData }
};
Expand Down
2 changes: 1 addition & 1 deletion apps/website/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/api/auth/[...nextauth].js", "utils/api/refreshAccessToken.js"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/api/auth/[...nextauth].js", "utils/api/refreshAccessToken.js", "utils/monitoring/prometheus.ts"],
"exclude": ["node_modules"]
}
26 changes: 26 additions & 0 deletions apps/website/utils/monitoring/metricsCollectorTokenVerifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { JwtHeader, JwtPayload, decode, verify } from "jsonwebtoken";
import { JwksClient, SigningKey } from "jwks-rsa";

const getKey = async (header: JwtHeader): Promise<SigningKey> => {
const client = new JwksClient({
jwksUri: `${process.env.KEYCLOAK_REALM_ADDRESS}/protocol/openid-connect/certs`,
});

return await client.getSigningKey(header.kid);
};

export const verifyToken = async (token: string, requiredRole: string): Promise<boolean> => {
try {
const jwtDecoded = decode(token, { complete: true });
const signingKey = await getKey(jwtDecoded?.header as JwtHeader);
const aaa = verify(token, signingKey.getPublicKey(), { complete: true });
const roles = (aaa.payload as JwtPayload & { roles: string[] })["roles"];
if (!roles.includes(requiredRole)) {
throw new Error(`Role <${requiredRole}> is missing from the roles list`);
}
return true;
} catch (err) {
console.error({ err });
return false;
}
};
37 changes: 37 additions & 0 deletions apps/website/utils/monitoring/prometheus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from "next/server";
import { Counter, register, collectDefaultMetrics } from "prom-client";

const globalCounters = globalThis as unknown as {
defaultMetricsCollected: true | undefined;
httpVisits: Counter | undefined;
};

let httpVisits: Counter;

if (process.env.NODE_ENV === "production") {
collectDefaultMetrics();
httpVisits = new Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["path", "userAgent"],
});
} else {
if (!globalCounters.defaultMetricsCollected) {
collectDefaultMetrics();
globalCounters.defaultMetricsCollected = true;
}
if (!globalCounters.httpVisits) {
globalCounters.httpVisits = new Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["path", "userAgent"],
});
}
httpVisits = globalCounters.httpVisits;
}

export const addHttpVisit = (path: string, userAgent: string) =>
httpVisits.inc({
path,
userAgent,
});

0 comments on commit 78b5c07

Please sign in to comment.