mirror of
https://github.com/p-stream/backend.git
synced 2026-01-11 20:10:33 +00:00
add metrics
hopefully this works
This commit is contained in:
parent
180fac4164
commit
e5a8afab2f
10 changed files with 1183 additions and 2 deletions
604
.metrics.json
Normal file
604
.metrics.json
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
[
|
||||
{
|
||||
"help": "Number of users by namespace",
|
||||
"name": "mw_user_count",
|
||||
"type": "counter",
|
||||
"values": [
|
||||
{
|
||||
"value": 3,
|
||||
"labels": {
|
||||
"namespace": "movie-web"
|
||||
}
|
||||
}
|
||||
],
|
||||
"aggregator": "sum"
|
||||
},
|
||||
{
|
||||
"help": "Number of captcha solves by success status",
|
||||
"name": "mw_captcha_solves",
|
||||
"type": "counter",
|
||||
"values": [],
|
||||
"aggregator": "sum"
|
||||
},
|
||||
{
|
||||
"help": "Number of requests by provider hostname",
|
||||
"name": "mw_provider_hostname_count",
|
||||
"type": "counter",
|
||||
"values": [
|
||||
{
|
||||
"value": 9,
|
||||
"labels": {
|
||||
"hostname": "<UNKNOWN>"
|
||||
}
|
||||
}
|
||||
],
|
||||
"aggregator": "sum"
|
||||
},
|
||||
{
|
||||
"help": "Number of provider requests by status",
|
||||
"name": "mw_provider_status_count",
|
||||
"type": "counter",
|
||||
"values": [
|
||||
{
|
||||
"value": 23,
|
||||
"labels": {
|
||||
"provider_id": "test",
|
||||
"status": "success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"labels": {
|
||||
"provider_id": "test2",
|
||||
"status": "failure"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"labels": {
|
||||
"provider_id": "uiralive",
|
||||
"status": "success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"labels": {
|
||||
"provider_id": "uiralive",
|
||||
"status": "failure"
|
||||
}
|
||||
}
|
||||
],
|
||||
"aggregator": "sum"
|
||||
},
|
||||
{
|
||||
"help": "Number of media watch events",
|
||||
"name": "mw_media_watch_count",
|
||||
"type": "counter",
|
||||
"values": [
|
||||
{
|
||||
"value": 2,
|
||||
"labels": {
|
||||
"tmdb_full_id": "movie-132",
|
||||
"provider_id": "test",
|
||||
"title": "Test Movie 132",
|
||||
"success": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"labels": {
|
||||
"tmdb_full_id": "movie-123",
|
||||
"provider_id": "test",
|
||||
"title": "Test Movie",
|
||||
"success": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"labels": {
|
||||
"tmdb_full_id": "show-11123",
|
||||
"provider_id": "test2",
|
||||
"title": "Test Movie boobs",
|
||||
"success": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"labels": {
|
||||
"tmdb_full_id": "show-61222",
|
||||
"provider_id": "uiralive",
|
||||
"title": "BoJack Horseman",
|
||||
"success": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"labels": {
|
||||
"tmdb_full_id": "show-61222",
|
||||
"provider_id": "uiralive",
|
||||
"title": "BoJack Horseman",
|
||||
"success": "false"
|
||||
}
|
||||
}
|
||||
],
|
||||
"aggregator": "sum"
|
||||
},
|
||||
{
|
||||
"help": "Number of provider tool usages",
|
||||
"name": "mw_provider_tool_count",
|
||||
"type": "counter",
|
||||
"values": [
|
||||
{
|
||||
"value": 2,
|
||||
"labels": {
|
||||
"tool": "extension"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"labels": {
|
||||
"tool": "custom-proxy"
|
||||
}
|
||||
}
|
||||
],
|
||||
"aggregator": "sum"
|
||||
},
|
||||
{
|
||||
"name": "http_request_duration_seconds",
|
||||
"help": "request duration in seconds",
|
||||
"type": "histogram",
|
||||
"values": [
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.005,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.01,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.025,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.05,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.1,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.25,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.5,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 1,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 2.5,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 5,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 10,
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": "+Inf",
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 16.000009167,
|
||||
"metricName": "http_request_duration_seconds_sum",
|
||||
"labels": {
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_count",
|
||||
"labels": {
|
||||
"method": "GET",
|
||||
"route": "/favicon.ico",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.005,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.01,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.025,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.05,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.1,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.25,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.5,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 1,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 2.5,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 5,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 10,
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": "+Inf",
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 16.000032751,
|
||||
"metricName": "http_request_duration_seconds_sum",
|
||||
"labels": {
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_count",
|
||||
"labels": {
|
||||
"method": "POST",
|
||||
"route": "/metrics/providers",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.005,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.01,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.025,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.05,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.1,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.25,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 0.5,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 0,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 1,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 2.5,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 5,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": 10,
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_bucket",
|
||||
"exemplar": null,
|
||||
"labels": {
|
||||
"le": "+Inf",
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 3.000003208,
|
||||
"metricName": "http_request_duration_seconds_sum",
|
||||
"labels": {
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"metricName": "http_request_duration_seconds_count",
|
||||
"labels": {
|
||||
"method": "GET",
|
||||
"route": "/",
|
||||
"status_code": "200"
|
||||
}
|
||||
}
|
||||
],
|
||||
"aggregator": "sum"
|
||||
}
|
||||
]
|
||||
|
|
@ -14,7 +14,7 @@ export default defineNitroConfig({
|
|||
name: process.env.META_NAME || '',
|
||||
description: process.env.META_DESCRIPTION || '',
|
||||
version: version || '',
|
||||
captcha: process.env.CAPTCHA || false,
|
||||
captcha: (process.env.CAPTCHA === 'true').toString(),
|
||||
captchaClientKey: process.env.CAPTCHA_CLIENT_KEY || ''
|
||||
}
|
||||
},
|
||||
|
|
|
|||
40
package-lock.json
generated
40
package-lock.json
generated
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "open-backend-1",
|
||||
"name": "open-backend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"bs58": "^6.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"prom-client": "^15.1.3",
|
||||
"tmdb-ts": "^2.0.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"zod": "^3.24.2"
|
||||
|
|
@ -733,6 +734,15 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
|
|
@ -2052,6 +2062,12 @@
|
|||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bintrees": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
|
|
@ -4574,6 +4590,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prom-client": {
|
||||
"version": "15.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
|
||||
"integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"tdigest": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16 || ^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.8.tgz",
|
||||
|
|
@ -5397,6 +5426,15 @@
|
|||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tdigest": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
|
||||
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bintrees": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.39.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"bs58": "^6.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"prom-client": "^15.1.3",
|
||||
"tmdb-ts": "^2.0.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"zod": "^3.24.2"
|
||||
|
|
|
|||
59
server/middleware/metrics.ts
Normal file
59
server/middleware/metrics.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { recordHttpRequest } from '~/utils/metrics';
|
||||
import { scopedLogger } from '~/utils/logger';
|
||||
|
||||
const log = scopedLogger('metrics-middleware');
|
||||
|
||||
// Paths we don't want to track metrics for
|
||||
const EXCLUDED_PATHS = [
|
||||
'/metrics',
|
||||
'/ping.txt',
|
||||
'/favicon.ico',
|
||||
'/robots.txt',
|
||||
'/sitemap.xml'
|
||||
];
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Skip tracking excluded paths
|
||||
if (EXCLUDED_PATHS.includes(event.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = process.hrtime();
|
||||
|
||||
try {
|
||||
// Wait for the request to complete
|
||||
await event._handled;
|
||||
} finally {
|
||||
// Calculate duration once the response is sent
|
||||
const [seconds, nanoseconds] = process.hrtime(start);
|
||||
const duration = seconds + nanoseconds / 1e9;
|
||||
|
||||
// Get cleaned route path (remove dynamic segments)
|
||||
const method = event.method;
|
||||
const route = getCleanPath(event.path);
|
||||
const statusCode = event.node.res.statusCode || 200;
|
||||
|
||||
// Record the request metrics
|
||||
recordHttpRequest(method, route, statusCode, duration);
|
||||
|
||||
log.debug('Recorded HTTP request metrics', {
|
||||
evt: 'http_metrics',
|
||||
method,
|
||||
route,
|
||||
statusCode,
|
||||
duration
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to normalize routes with dynamic segments (e.g., /users/123 -> /users/:id)
|
||||
function getCleanPath(path: string): string {
|
||||
// Common patterns for Nitro routes
|
||||
return path
|
||||
.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, '/:uuid')
|
||||
.replace(/\/\d+/g, '/:id')
|
||||
.replace(/@me/, ':uid')
|
||||
.replace(/\/[^\/]+\/progress\/[^\/]+/, '/:uid/progress/:tmdbid')
|
||||
.replace(/\/[^\/]+\/bookmarks\/[^\/]+/, '/:uid/bookmarks/:tmdbid')
|
||||
.replace(/\/sessions\/[^\/]+/, '/sessions/:sid');
|
||||
}
|
||||
34
server/routes/metrics.ts
Normal file
34
server/routes/metrics.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { register } from 'prom-client';
|
||||
import { setupMetrics } from '../utils/metrics';
|
||||
import { scopedLogger } from '../utils/logger';
|
||||
|
||||
const log = scopedLogger('metrics-endpoint');
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
async function ensureMetricsInitialized() {
|
||||
if (!isInitialized) {
|
||||
log.info('Initializing metrics from endpoint...', { evt: 'init_start' });
|
||||
await setupMetrics();
|
||||
isInitialized = true;
|
||||
log.info('Metrics initialized from endpoint', { evt: 'init_complete' });
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await ensureMetricsInitialized();
|
||||
const metrics = await register.metrics();
|
||||
event.node.res.setHeader('Content-Type', register.contentType);
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
log.error('Error in metrics endpoint:', {
|
||||
evt: 'metrics_error',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error instanceof Error ? error.message : 'Failed to collect metrics'
|
||||
});
|
||||
}
|
||||
});
|
||||
41
server/routes/metrics/captcha.post.ts
Normal file
41
server/routes/metrics/captcha.post.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { z } from 'zod';
|
||||
import { getMetrics, recordCaptchaMetrics } from '~/utils/metrics';
|
||||
import { scopedLogger } from '~/utils/logger';
|
||||
import { setupMetrics } from '~/utils/metrics';
|
||||
|
||||
const log = scopedLogger('metrics-captcha');
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
async function ensureMetricsInitialized() {
|
||||
if (!isInitialized) {
|
||||
log.info('Initializing metrics from captcha endpoint...', { evt: 'init_start' });
|
||||
await setupMetrics();
|
||||
isInitialized = true;
|
||||
log.info('Metrics initialized from captcha endpoint', { evt: 'init_complete' });
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await ensureMetricsInitialized();
|
||||
|
||||
const body = await readBody(event);
|
||||
const validatedBody = z.object({
|
||||
success: z.boolean(),
|
||||
}).parse(body);
|
||||
|
||||
recordCaptchaMetrics(validatedBody.success);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('Failed to process captcha metrics', {
|
||||
evt: 'metrics_error',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw createError({
|
||||
statusCode: error instanceof Error && error.message === 'metrics not initialized' ? 503 : 400,
|
||||
message: error instanceof Error ? error.message : 'Failed to process metrics'
|
||||
});
|
||||
}
|
||||
});
|
||||
60
server/routes/metrics/providers.post.ts
Normal file
60
server/routes/metrics/providers.post.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { z } from 'zod';
|
||||
import { getMetrics, recordProviderMetrics } from '~/utils/metrics';
|
||||
import { scopedLogger } from '~/utils/logger';
|
||||
import { setupMetrics } from '~/utils/metrics';
|
||||
|
||||
const log = scopedLogger('metrics-providers');
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
async function ensureMetricsInitialized() {
|
||||
if (!isInitialized) {
|
||||
log.info('Initializing metrics from providers endpoint...', { evt: 'init_start' });
|
||||
await setupMetrics();
|
||||
isInitialized = true;
|
||||
log.info('Metrics initialized from providers endpoint', { evt: 'init_complete' });
|
||||
}
|
||||
}
|
||||
|
||||
const metricsProviderSchema = z.object({
|
||||
tmdbId: z.string(),
|
||||
type: z.string(),
|
||||
title: z.string(),
|
||||
seasonId: z.string().optional(),
|
||||
episodeId: z.string().optional(),
|
||||
status: z.string(),
|
||||
providerId: z.string(),
|
||||
embedId: z.string().optional(),
|
||||
errorMessage: z.string().optional(),
|
||||
fullError: z.string().optional(),
|
||||
});
|
||||
|
||||
const metricsProviderInputSchema = z.object({
|
||||
items: z.array(metricsProviderSchema).max(10).min(1),
|
||||
tool: z.string().optional(),
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await ensureMetricsInitialized();
|
||||
|
||||
const body = await readBody(event);
|
||||
const validatedBody = metricsProviderInputSchema.parse(body);
|
||||
|
||||
const hostname = event.node.req.headers.origin?.slice(0, 255) ?? '<UNKNOWN>';
|
||||
|
||||
// Use the simplified recordProviderMetrics function to handle all metrics recording
|
||||
recordProviderMetrics(validatedBody.items, hostname, validatedBody.tool);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('Failed to process metrics', {
|
||||
evt: 'metrics_error',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw createError({
|
||||
statusCode: error instanceof Error && error.message === 'metrics not initialized' ? 503 : 400,
|
||||
message: error instanceof Error ? error.message : 'Failed to process metrics'
|
||||
});
|
||||
}
|
||||
});
|
||||
38
server/utils/logger.ts
Normal file
38
server/utils/logger.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
||||
|
||||
interface LogContext {
|
||||
evt?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Logger {
|
||||
info(message: string, context?: LogContext): void;
|
||||
warn(message: string, context?: LogContext): void;
|
||||
error(message: string, context?: LogContext): void;
|
||||
debug(message: string, context?: LogContext): void;
|
||||
}
|
||||
|
||||
function createLogger(scope: string): Logger {
|
||||
const log = (level: LogLevel, message: string, context?: LogContext) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logData = {
|
||||
timestamp,
|
||||
level,
|
||||
scope,
|
||||
message,
|
||||
...context,
|
||||
};
|
||||
console.log(JSON.stringify(logData));
|
||||
};
|
||||
|
||||
return {
|
||||
info: (message: string, context?: LogContext) => log('info', message, context),
|
||||
warn: (message: string, context?: LogContext) => log('warn', message, context),
|
||||
error: (message: string, context?: LogContext) => log('error', message, context),
|
||||
debug: (message: string, context?: LogContext) => log('debug', message, context),
|
||||
};
|
||||
}
|
||||
|
||||
export function scopedLogger(scope: string): Logger {
|
||||
return createLogger(scope);
|
||||
}
|
||||
306
server/utils/metrics.ts
Normal file
306
server/utils/metrics.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import { Counter, register, collectDefaultMetrics, Histogram, Summary } from 'prom-client';
|
||||
import { prisma } from './prisma';
|
||||
import { scopedLogger } from '~/utils/logger';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const log = scopedLogger('metrics');
|
||||
const METRICS_FILE = '.metrics.json';
|
||||
|
||||
export type Metrics = {
|
||||
user: Counter<'namespace'>;
|
||||
captchaSolves: Counter<'success'>;
|
||||
providerHostnames: Counter<'hostname'>;
|
||||
providerStatuses: Counter<'provider_id' | 'status'>;
|
||||
watchMetrics: Counter<'title' | 'tmdb_full_id' | 'provider_id' | 'success'>;
|
||||
toolMetrics: Counter<'tool'>;
|
||||
httpRequestDuration: Histogram<'method' | 'route' | 'status_code'>;
|
||||
httpRequestSummary: Summary<'method' | 'route' | 'status_code'>;
|
||||
};
|
||||
|
||||
let metrics: null | Metrics = null;
|
||||
|
||||
export function getMetrics() {
|
||||
if (!metrics) throw new Error('metrics not initialized');
|
||||
return metrics;
|
||||
}
|
||||
|
||||
async function createMetrics(): Promise<Metrics> {
|
||||
const newMetrics = {
|
||||
user: new Counter({
|
||||
name: 'mw_user_count',
|
||||
help: 'Number of users by namespace',
|
||||
labelNames: ['namespace'],
|
||||
}),
|
||||
captchaSolves: new Counter({
|
||||
name: 'mw_captcha_solves',
|
||||
help: 'Number of captcha solves by success status',
|
||||
labelNames: ['success'],
|
||||
}),
|
||||
providerHostnames: new Counter({
|
||||
name: 'mw_provider_hostname_count',
|
||||
help: 'Number of requests by provider hostname',
|
||||
labelNames: ['hostname'],
|
||||
}),
|
||||
providerStatuses: new Counter({
|
||||
name: 'mw_provider_status_count',
|
||||
help: 'Number of provider requests by status',
|
||||
labelNames: ['provider_id', 'status'],
|
||||
}),
|
||||
watchMetrics: new Counter({
|
||||
name: 'mw_media_watch_count',
|
||||
help: 'Number of media watch events',
|
||||
labelNames: ['title', 'tmdb_full_id', 'provider_id', 'success'],
|
||||
}),
|
||||
toolMetrics: new Counter({
|
||||
name: 'mw_provider_tool_count',
|
||||
help: 'Number of provider tool usages',
|
||||
labelNames: ['tool'],
|
||||
}),
|
||||
httpRequestDuration: new Histogram({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'request duration in seconds',
|
||||
labelNames: ['method', 'route', 'status_code'],
|
||||
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
||||
}),
|
||||
httpRequestSummary: new Summary({
|
||||
name: 'http_request_summary_seconds',
|
||||
help: 'request duration in seconds summary',
|
||||
labelNames: ['method', 'route', 'status_code'],
|
||||
percentiles: [0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999],
|
||||
}),
|
||||
};
|
||||
|
||||
// Register all metrics with the Prometheus registry
|
||||
register.registerMetric(newMetrics.user);
|
||||
register.registerMetric(newMetrics.captchaSolves);
|
||||
register.registerMetric(newMetrics.providerHostnames);
|
||||
register.registerMetric(newMetrics.providerStatuses);
|
||||
register.registerMetric(newMetrics.watchMetrics);
|
||||
register.registerMetric(newMetrics.toolMetrics);
|
||||
register.registerMetric(newMetrics.httpRequestDuration);
|
||||
register.registerMetric(newMetrics.httpRequestSummary);
|
||||
|
||||
return newMetrics;
|
||||
}
|
||||
|
||||
async function saveMetricsToFile() {
|
||||
try {
|
||||
if (!metrics) return;
|
||||
|
||||
const metricsData = await register.getMetricsAsJSON();
|
||||
const relevantMetrics = metricsData.filter(metric =>
|
||||
metric.name.startsWith('mw_') ||
|
||||
metric.name === 'http_request_duration_seconds'
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
METRICS_FILE,
|
||||
JSON.stringify(relevantMetrics, null, 2)
|
||||
);
|
||||
|
||||
log.info('Metrics saved to file', { evt: 'metrics_saved' });
|
||||
} catch (error) {
|
||||
log.error('Failed to save metrics', {
|
||||
evt: 'save_metrics_error',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMetricsFromFile(): Promise<any[]> {
|
||||
try {
|
||||
if (!fs.existsSync(METRICS_FILE)) {
|
||||
log.info('No saved metrics found', { evt: 'no_saved_metrics' });
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(METRICS_FILE, 'utf8');
|
||||
const savedMetrics = JSON.parse(data);
|
||||
log.info('Loaded saved metrics', {
|
||||
evt: 'metrics_loaded',
|
||||
count: savedMetrics.length
|
||||
});
|
||||
return savedMetrics;
|
||||
} catch (error) {
|
||||
log.error('Failed to load metrics', {
|
||||
evt: 'load_metrics_error',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Periodically save metrics
|
||||
const SAVE_INTERVAL = 60000; // Save every minute
|
||||
setInterval(saveMetricsToFile, SAVE_INTERVAL);
|
||||
|
||||
// Save metrics on process exit
|
||||
process.on('SIGTERM', async () => {
|
||||
log.info('Saving metrics before exit...', { evt: 'exit_save' });
|
||||
await saveMetricsToFile();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
log.info('Saving metrics before exit...', { evt: 'exit_save' });
|
||||
await saveMetricsToFile();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
export async function setupMetrics() {
|
||||
try {
|
||||
log.info('Setting up metrics...', { evt: 'start' });
|
||||
|
||||
// Clear all existing metrics
|
||||
log.info('Clearing metrics registry...', { evt: 'clear' });
|
||||
register.clear();
|
||||
|
||||
// Enable default Node.js metrics collection with appropriate settings
|
||||
collectDefaultMetrics({
|
||||
register,
|
||||
prefix: '', // No prefix to match the example output
|
||||
eventLoopMonitoringPrecision: 1, // Ensure eventloop metrics are collected
|
||||
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // Match the example buckets
|
||||
});
|
||||
|
||||
// Create new metrics instance
|
||||
metrics = await createMetrics();
|
||||
log.info('Created new metrics...', { evt: 'created' });
|
||||
|
||||
// Load saved metrics
|
||||
const savedMetrics = await loadMetricsFromFile();
|
||||
if (savedMetrics.length > 0) {
|
||||
log.info('Restoring saved metrics...', { evt: 'restore_metrics' });
|
||||
savedMetrics.forEach((metric) => {
|
||||
if (metric.values) {
|
||||
metric.values.forEach((value) => {
|
||||
switch (metric.name) {
|
||||
case 'mw_user_count':
|
||||
metrics?.user.inc(value.labels, value.value);
|
||||
break;
|
||||
case 'mw_captcha_solves':
|
||||
metrics?.captchaSolves.inc(value.labels, value.value);
|
||||
break;
|
||||
case 'mw_provider_hostname_count':
|
||||
metrics?.providerHostnames.inc(value.labels, value.value);
|
||||
break;
|
||||
case 'mw_provider_status_count':
|
||||
metrics?.providerStatuses.inc(value.labels, value.value);
|
||||
break;
|
||||
case 'mw_media_watch_count':
|
||||
metrics?.watchMetrics.inc(value.labels, value.value);
|
||||
break;
|
||||
case 'mw_provider_tool_count':
|
||||
metrics?.toolMetrics.inc(value.labels, value.value);
|
||||
break;
|
||||
case 'http_request_duration_seconds':
|
||||
// For histograms, special handling for sum and count
|
||||
if (value.metricName === 'http_request_duration_seconds_sum' ||
|
||||
value.metricName === 'http_request_duration_seconds_count') {
|
||||
metrics?.httpRequestDuration.observe(value.labels, value.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize metrics with current data
|
||||
log.info('Syncing up metrics...', { evt: 'sync' });
|
||||
await updateMetrics();
|
||||
log.info('Metrics initialized!', { evt: 'end' });
|
||||
|
||||
// Save initial state
|
||||
await saveMetricsToFile();
|
||||
} catch (error) {
|
||||
log.error('Failed to setup metrics', {
|
||||
evt: 'setup_error',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMetrics() {
|
||||
try {
|
||||
log.info('Fetching users from database...', { evt: 'update_metrics_start' });
|
||||
|
||||
const users = await prisma.users.groupBy({
|
||||
by: ['namespace'],
|
||||
_count: true,
|
||||
});
|
||||
|
||||
log.info('Found users', { evt: 'users_found', count: users.length });
|
||||
|
||||
metrics?.user.reset();
|
||||
log.info('Reset user metrics counter', { evt: 'metrics_reset' });
|
||||
|
||||
users.forEach((v) => {
|
||||
log.info('Incrementing user metric', {
|
||||
evt: 'increment_metric',
|
||||
namespace: v.namespace,
|
||||
count: v._count
|
||||
});
|
||||
metrics?.user.inc({ namespace: v.namespace }, v._count);
|
||||
});
|
||||
|
||||
log.info('Successfully updated metrics', { evt: 'update_metrics_complete' });
|
||||
} catch (error) {
|
||||
log.error('Failed to update metrics', {
|
||||
evt: 'update_metrics_error',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Export function to record HTTP request duration
|
||||
export function recordHttpRequest(method: string, route: string, statusCode: number, duration: number) {
|
||||
if (!metrics) return;
|
||||
|
||||
const labels = {
|
||||
method,
|
||||
route,
|
||||
status_code: statusCode.toString()
|
||||
};
|
||||
|
||||
// Record in both histogram and summary
|
||||
metrics.httpRequestDuration.observe(labels, duration);
|
||||
metrics.httpRequestSummary.observe(labels, duration);
|
||||
}
|
||||
|
||||
// Functions to match previous backend API
|
||||
export function recordProviderMetrics(items: any[], hostname: string, tool?: string) {
|
||||
if (!metrics) return;
|
||||
|
||||
// Record hostname once per request
|
||||
metrics.providerHostnames.inc({ hostname });
|
||||
|
||||
// Record status and watch metrics for each item
|
||||
items.forEach((item) => {
|
||||
// Record provider status
|
||||
metrics.providerStatuses.inc({
|
||||
provider_id: item.embedId ?? item.providerId,
|
||||
status: item.status,
|
||||
});
|
||||
|
||||
// Record watch metrics for each item
|
||||
metrics.watchMetrics.inc({
|
||||
tmdb_full_id: item.type + '-' + item.tmdbId,
|
||||
provider_id: item.providerId,
|
||||
title: item.title,
|
||||
success: (item.status === 'success').toString(),
|
||||
});
|
||||
});
|
||||
|
||||
// Record tool metrics
|
||||
if (tool) {
|
||||
metrics.toolMetrics.inc({ tool });
|
||||
}
|
||||
}
|
||||
|
||||
export function recordCaptchaMetrics(success: boolean) {
|
||||
metrics?.captchaSolves.inc({ success: success.toString() });
|
||||
}
|
||||
Loading…
Reference in a new issue