mirror of
https://github.com/p-stream/backend.git
synced 2026-04-21 08:12:18 +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 || '',
|
name: process.env.META_NAME || '',
|
||||||
description: process.env.META_DESCRIPTION || '',
|
description: process.env.META_DESCRIPTION || '',
|
||||||
version: version || '',
|
version: version || '',
|
||||||
captcha: process.env.CAPTCHA || false,
|
captcha: (process.env.CAPTCHA === 'true').toString(),
|
||||||
captchaClientKey: process.env.CAPTCHA_CLIENT_KEY || ''
|
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,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"prom-client": "^15.1.3",
|
||||||
"tmdb-ts": "^2.0.1",
|
"tmdb-ts": "^2.0.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
|
|
@ -733,6 +734,15 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@parcel/watcher": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||||
|
|
@ -2052,6 +2062,12 @@
|
||||||
"file-uri-to-path": "1.0.0"
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
|
@ -4574,6 +4590,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/quansync": {
|
||||||
"version": "0.2.8",
|
"version": "0.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.8.tgz",
|
||||||
|
|
@ -5397,6 +5426,15 @@
|
||||||
"streamx": "^2.15.0"
|
"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": {
|
"node_modules/terser": {
|
||||||
"version": "5.39.0",
|
"version": "5.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"prom-client": "^15.1.3",
|
||||||
"tmdb-ts": "^2.0.1",
|
"tmdb-ts": "^2.0.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"zod": "^3.24.2"
|
"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