feat: add prometheus metrics

This commit is contained in:
hyperdefined 2025-06-01 19:45:54 -04:00
parent b4a53d0fde
commit df2dcbc69d
No known key found for this signature in database
GPG Key ID: 38C93C4835071D4A
5 changed files with 107 additions and 7 deletions

View File

@ -36,6 +36,7 @@
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"mime": "^4.0.4", "mime": "^4.0.4",
"nanoid": "^5.0.9", "nanoid": "^5.0.9",
"prom-client": "^15.1.3",
"set-cookie-parser": "2.6.0", "set-cookie-parser": "2.6.0",
"undici": "^5.19.1", "undici": "^5.19.1",
"url-pattern": "1.0.3", "url-pattern": "1.0.3",

View File

@ -3,6 +3,8 @@ import http from "node:http";
import rateLimit from "express-rate-limit"; import rateLimit from "express-rate-limit";
import { setGlobalDispatcher, ProxyAgent } from "undici"; import { setGlobalDispatcher, ProxyAgent } from "undici";
import { getCommit, getBranch, getRemote, getVersion } from "@imput/version-info"; import { getCommit, getBranch, getRemote, getVersion } from "@imput/version-info";
import registry from "../misc/metrics.js"
import { httpRequests, httpRequestDuration } from "../misc/metrics.js"
import jwt from "../security/jwt.js"; import jwt from "../security/jwt.js";
import stream from "../stream/stream.js"; import stream from "../stream/stream.js";
@ -111,6 +113,17 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
app.set('trust proxy', ['loopback', 'uniquelocal']); app.set('trust proxy', ['loopback', 'uniquelocal']);
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer({ method: req.method });
res.on('finish', () => {
httpRequests.labels(req.method, res.statusCode.toString()).inc();
end();
});
next();
});
app.use('/', cors({ app.use('/', cors({
methods: ['GET', 'POST'], methods: ['GET', 'POST'],
exposedHeaders: [ exposedHeaders: [
@ -321,6 +334,11 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
res.status(404).end(); res.status(404).end();
}) })
app.get('/metrics', async (req, res) => {
res.set('Content-Type', registry.contentType);
res.send(await registry.metrics());
});
app.get('/*', (req, res) => { app.get('/*', (req, res) => {
res.redirect('/'); res.redirect('/');
}) })

45
api/src/misc/metrics.js Normal file
View File

@ -0,0 +1,45 @@
import { Registry } from 'prom-client';
import { collectDefaultMetrics, Counter, Histogram } from "prom-client";
const registry = new Registry();
export default registry;
collectDefaultMetrics({ register: registry });
export const failedRequests = new Counter({
name: 'cobalt_fail_request_count',
help: 'Total number of failed requests',
labelNames: ['service'],
});
export const successfulRequests = new Counter({
name: 'cobalt_success_request_count',
help: 'Total number of successful requests',
labelNames: ['service'],
});
export const httpRequests = new Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'status'],
});
export const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method'],
buckets: [0.1, 0.5, 1, 1.5, 2, 5],
});
registry.registerMetric(failedRequests);
registry.registerMetric(successfulRequests);
registry.registerMetric(httpRequests);
registry.registerMetric(httpRequestDuration);
export function incrementFailed(type) {
failedRequests.labels(type).inc();
}
export function incrementSuccessful(type) {
successfulRequests.labels(type).inc();
}

View File

@ -8,6 +8,8 @@ import matchAction from "./match-action.js";
import { friendlyServiceName } from "./service-alias.js"; import { friendlyServiceName } from "./service-alias.js";
import { incrementFailed, incrementSuccessful } from "../misc/metrics.js"
import bilibili from "./services/bilibili.js"; import bilibili from "./services/bilibili.js";
import reddit from "./services/reddit.js"; import reddit from "./services/reddit.js";
import twitter from "./services/twitter.js"; import twitter from "./services/twitter.js";
@ -286,12 +288,16 @@ export default async function({ host, patternMatch, params, isSession }) {
break; break;
} }
incrementFailed(host);
return createResponse("error", { return createResponse("error", {
code: `error.api.${r.error}`, code: `error.api.${r.error}`,
context, context,
}) })
} }
incrementSuccessful(host);
let localProcessing = params.localProcessing; let localProcessing = params.localProcessing;
const lpEnv = env.forceLocalProcessing; const lpEnv = env.forceLocalProcessing;

View File

@ -49,6 +49,9 @@ importers:
nanoid: nanoid:
specifier: ^5.0.9 specifier: ^5.0.9
version: 5.0.9 version: 5.0.9
prom-client:
specifier: ^15.1.3
version: 15.1.3
set-cookie-parser: set-cookie-parser:
specifier: 2.6.0 specifier: 2.6.0
version: 2.6.0 version: 2.6.0
@ -613,6 +616,10 @@ packages:
resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==} resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
'@opentelemetry/api@1.9.0':
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -930,6 +937,9 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
bintrees@1.0.2:
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
body-parser@1.20.3: body-parser@1.20.3:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@ -1749,6 +1759,10 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
prom-client@15.1.3:
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
engines: {node: ^16 || ^18 || >=20}
proxy-addr@2.0.7: proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
@ -1982,6 +1996,9 @@ packages:
syscall-napi@0.0.6: syscall-napi@0.0.6:
resolution: {integrity: sha512-qHbwjyFXAAekKUXxl70lhDiBYJ3e7XM7kQwu7LV3F0pHMenKox+VcZPZkRkhdmL/wNJD3NmrMGnL7161kdecUQ==} resolution: {integrity: sha512-qHbwjyFXAAekKUXxl70lhDiBYJ3e7XM7kQwu7LV3F0pHMenKox+VcZPZkRkhdmL/wNJD3NmrMGnL7161kdecUQ==}
tdigest@0.1.2:
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
thenify-all@1.6.0: thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
@ -2363,7 +2380,7 @@ snapshots:
'@eslint/config-array@0.19.1': '@eslint/config-array@0.19.1':
dependencies: dependencies:
'@eslint/object-schema': 2.1.5 '@eslint/object-schema': 2.1.5
debug: 4.3.6 debug: 4.4.0
minimatch: 3.1.2 minimatch: 3.1.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -2375,7 +2392,7 @@ snapshots:
'@eslint/eslintrc@3.2.0': '@eslint/eslintrc@3.2.0':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
debug: 4.3.6 debug: 4.4.0
espree: 10.3.0 espree: 10.3.0
globals: 14.0.0 globals: 14.0.0
ignore: 5.3.1 ignore: 5.3.1
@ -2478,6 +2495,8 @@ snapshots:
'@oozcitak/util@8.3.8': {} '@oozcitak/util@8.3.8': {}
'@opentelemetry/api@1.9.0': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@ -2675,7 +2694,7 @@ snapshots:
'@typescript-eslint/types': 8.18.0 '@typescript-eslint/types': 8.18.0
'@typescript-eslint/typescript-estree': 8.18.0(typescript@5.5.4) '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.5.4)
'@typescript-eslint/visitor-keys': 8.18.0 '@typescript-eslint/visitor-keys': 8.18.0
debug: 4.3.6 debug: 4.4.0
eslint: 9.16.0 eslint: 9.16.0
typescript: 5.5.4 typescript: 5.5.4
transitivePeerDependencies: transitivePeerDependencies:
@ -2690,7 +2709,7 @@ snapshots:
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 8.18.0(typescript@5.5.4) '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.5.4)
'@typescript-eslint/utils': 8.18.0(eslint@9.16.0)(typescript@5.5.4) '@typescript-eslint/utils': 8.18.0(eslint@9.16.0)(typescript@5.5.4)
debug: 4.3.6 debug: 4.4.0
eslint: 9.16.0 eslint: 9.16.0
ts-api-utils: 1.3.0(typescript@5.5.4) ts-api-utils: 1.3.0(typescript@5.5.4)
typescript: 5.5.4 typescript: 5.5.4
@ -2703,7 +2722,7 @@ snapshots:
dependencies: dependencies:
'@typescript-eslint/types': 8.18.0 '@typescript-eslint/types': 8.18.0
'@typescript-eslint/visitor-keys': 8.18.0 '@typescript-eslint/visitor-keys': 8.18.0
debug: 4.3.6 debug: 4.4.0
fast-glob: 3.3.2 fast-glob: 3.3.2
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.5 minimatch: 9.0.5
@ -2746,7 +2765,7 @@ snapshots:
agent-base@6.0.2: agent-base@6.0.2:
dependencies: dependencies:
debug: 4.3.6 debug: 4.4.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -2790,6 +2809,8 @@ snapshots:
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
bintrees@1.0.2: {}
body-parser@1.20.3: body-parser@1.20.3:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
@ -3320,7 +3341,7 @@ snapshots:
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
debug: 4.3.6 debug: 4.4.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -3608,6 +3629,11 @@ snapshots:
progress@2.0.3: {} progress@2.0.3: {}
prom-client@15.1.3:
dependencies:
'@opentelemetry/api': 1.9.0
tdigest: 0.1.2
proxy-addr@2.0.7: proxy-addr@2.0.7:
dependencies: dependencies:
forwarded: 0.2.0 forwarded: 0.2.0
@ -3866,6 +3892,10 @@ snapshots:
syscall-napi@0.0.6: syscall-napi@0.0.6:
optional: true optional: true
tdigest@0.1.2:
dependencies:
bintrees: 1.0.2
thenify-all@1.6.0: thenify-all@1.6.0:
dependencies: dependencies:
thenify: 3.3.1 thenify: 3.3.1