Skip to content

Commit 68bdb61

Browse files
committed
feat: [torrust#1403] add extendable-labeled metrics to http-tracker-core and expose in REST API
**URL:** http://0.0.0.0:1212/api/v1/metrics?token=MyAccessToken **Sample response:** ```json { "metrics":[ { "kind":"counter", "name":"http_tracker_core_announce_requests_received_total", "samples":[ { "value":1, "update_at":"2025-04-02T00:00:00+00:00", "labels":[ { "name":"server_binding_ip", "value":"0.0.0.0" }, { "name":"server_binding_port", "value":"7070" }, { "name":"server_binding_protocol", "value":"http" } ] } ] }, { "kind":"gauge", "name":"udp_tracker_server_performance_avg_announce_processing_time_ns", "samples":[ { "value":1.0, "update_at":"2025-04-02T00:00:00+00:00", "labels":[ { "name":"server_binding_ip", "value":"0.0.0.0" }, { "name":"server_binding_port", "value":"7070" }, { "name":"server_binding_protocol", "value":"http" } ] } ] } ] } ``` **URL:** http://0.0.0.0:1212/api/v1/stats?token=MyAccessToken&format=prometheus ``` http_tracker_core_announce_requests_received_total{server_binding_ip="0.0.0.0",server_binding_port="7070",server_binding_protocol="http"} 1 udp_tracker_server_performance_avg_announce_processing_time_ns{server_binding_ip="0.0.0.0",server_binding_port="7070",server_binding_protocol="http"} 1 ```
1 parent 3e66119 commit 68bdb61

17 files changed

Lines changed: 264 additions & 46 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/axum-rest-tracker-api-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ torrust-rest-tracker-api-core = { version = "3.0.0-develop", path = "../rest-tra
3737
torrust-server-lib = { version = "3.0.0-develop", path = "../server-lib" }
3838
torrust-tracker-clock = { version = "3.0.0-develop", path = "../clock" }
3939
torrust-tracker-configuration = { version = "3.0.0-develop", path = "../configuration" }
40+
torrust-tracker-metrics = { version = "3.0.0-develop", path = "../metrics" }
4041
torrust-tracker-primitives = { version = "3.0.0-develop", path = "../primitives" }
4142
torrust-udp-tracker-server = { version = "3.0.0-develop", path = "../udp-tracker-server" }
4243
tower = { version = "0", features = ["timeout"] }

packages/axum-rest-tracker-api-server/src/v1/context/stats/handlers.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use bittorrent_tracker_core::torrent::repository::in_memory::InMemoryTorrentRepo
99
use bittorrent_udp_tracker_core::services::banning::BanService;
1010
use serde::Deserialize;
1111
use tokio::sync::RwLock;
12-
use torrust_rest_tracker_api_core::statistics::services::get_metrics;
12+
use torrust_rest_tracker_api_core::statistics::services::{get_labeled_metrics, get_metrics};
1313

14-
use super::responses::{metrics_response, stats_response};
14+
use super::responses::{labeled_metrics_response, labeled_stats_response, metrics_response, stats_response};
1515

1616
#[derive(Deserialize, Debug, Default)]
1717
#[serde(rename_all = "lowercase")]
@@ -28,7 +28,7 @@ pub struct QueryParams {
2828
pub format: Option<Format>,
2929
}
3030

31-
/// It handles the request to get the tracker statistics.
31+
/// It handles the request to get the tracker global metrics.
3232
///
3333
/// By default it returns a `200` response with the stats in JSON format.
3434
///
@@ -57,3 +57,30 @@ pub async fn get_stats_handler(
5757
None => stats_response(metrics),
5858
}
5959
}
60+
61+
/// It handles the request to get the tracker extendable metrics.
62+
///
63+
/// By default it returns a `200` response with the stats in JSON format.
64+
///
65+
/// You can add the GET parameter `format=prometheus` to get the stats in
66+
/// Prometheus Text Exposition Format.
67+
#[allow(clippy::type_complexity)]
68+
pub async fn get_metrics_handler(
69+
State(state): State<(
70+
Arc<InMemoryTorrentRepository>,
71+
Arc<RwLock<BanService>>,
72+
Arc<bittorrent_http_tracker_core::statistics::repository::Repository>,
73+
Arc<torrust_udp_tracker_server::statistics::repository::Repository>,
74+
)>,
75+
params: Query<QueryParams>,
76+
) -> Response {
77+
let metrics = get_labeled_metrics(state.0.clone(), state.1.clone(), state.2.clone(), state.3.clone()).await;
78+
79+
match params.0.format {
80+
Some(format) => match format {
81+
Format::Json => labeled_stats_response(metrics),
82+
Format::Prometheus => labeled_metrics_response(&metrics),
83+
},
84+
None => labeled_stats_response(metrics),
85+
}
86+
}

packages/axum-rest-tracker-api-server/src/v1/context/stats/resources.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
//! API resources for the [`stats`](crate::v1::context::stats)
22
//! API context.
33
use serde::{Deserialize, Serialize};
4-
use torrust_rest_tracker_api_core::statistics::services::TrackerMetrics;
4+
use torrust_rest_tracker_api_core::statistics::services::{TrackerLabeledMetrics, TrackerMetrics};
5+
use torrust_tracker_metrics::metric_collection::MetricCollection;
56

67
/// It contains all the statistics generated by the tracker.
78
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
@@ -116,6 +117,21 @@ impl From<TrackerMetrics> for Stats {
116117
}
117118
}
118119

120+
/// It contains all the statistics generated by the tracker.
121+
#[derive(Serialize, Debug, PartialEq)]
122+
pub struct LabeledStats {
123+
metrics: MetricCollection,
124+
}
125+
126+
impl From<TrackerLabeledMetrics> for LabeledStats {
127+
#[allow(deprecated)]
128+
fn from(metrics: TrackerLabeledMetrics) -> Self {
129+
Self {
130+
metrics: metrics.metrics,
131+
}
132+
}
133+
}
134+
119135
#[cfg(test)]
120136
mod tests {
121137
use torrust_rest_tracker_api_core::statistics::metrics::Metrics;

packages/axum-rest-tracker-api-server/src/v1/context/stats/responses.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
//! API responses for the [`stats`](crate::v1::context::stats)
22
//! API context.
33
use axum::response::{IntoResponse, Json, Response};
4-
use torrust_rest_tracker_api_core::statistics::services::TrackerMetrics;
4+
use torrust_rest_tracker_api_core::statistics::services::{TrackerLabeledMetrics, TrackerMetrics};
5+
use torrust_tracker_metrics::prometheus::PrometheusSerializable;
56

6-
use super::resources::Stats;
7+
use super::resources::{LabeledStats, Stats};
8+
9+
/// `200` response that contains the [`LabeledStats`] resource as json.
10+
#[must_use]
11+
pub fn labeled_stats_response(tracker_metrics: TrackerLabeledMetrics) -> Response {
12+
Json(LabeledStats::from(tracker_metrics)).into_response()
13+
}
14+
15+
#[must_use]
16+
pub fn labeled_metrics_response(tracker_metrics: &TrackerLabeledMetrics) -> Response {
17+
tracker_metrics.metrics.to_prometheus().into_response()
18+
}
719

820
/// `200` response that contains the [`Stats`] resource as json.
921
#[must_use]

packages/axum-rest-tracker-api-server/src/v1/context/stats/routes.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,27 @@ use axum::routing::get;
99
use axum::Router;
1010
use torrust_rest_tracker_api_core::container::TrackerHttpApiCoreContainer;
1111

12-
use super::handlers::get_stats_handler;
12+
use super::handlers::{get_metrics_handler, get_stats_handler};
1313

1414
/// It adds the routes to the router for the [`stats`](crate::v1::context::stats) API context.
1515
pub fn add(prefix: &str, router: Router, http_api_container: &Arc<TrackerHttpApiCoreContainer>) -> Router {
16-
router.route(
17-
&format!("{prefix}/stats"),
18-
get(get_stats_handler).with_state((
19-
http_api_container.tracker_core_container.in_memory_torrent_repository.clone(),
20-
http_api_container.ban_service.clone(),
21-
http_api_container.http_stats_repository.clone(),
22-
http_api_container.udp_server_stats_repository.clone(),
23-
)),
24-
)
16+
router
17+
.route(
18+
&format!("{prefix}/stats"),
19+
get(get_stats_handler).with_state((
20+
http_api_container.tracker_core_container.in_memory_torrent_repository.clone(),
21+
http_api_container.ban_service.clone(),
22+
http_api_container.http_stats_repository.clone(),
23+
http_api_container.udp_server_stats_repository.clone(),
24+
)),
25+
)
26+
.route(
27+
&format!("{prefix}/metrics"),
28+
get(get_metrics_handler).with_state((
29+
http_api_container.tracker_core_container.in_memory_torrent_repository.clone(),
30+
http_api_container.ban_service.clone(),
31+
http_api_container.http_stats_repository.clone(),
32+
http_api_container.udp_server_stats_repository.clone(),
33+
)),
34+
)
2535
}

packages/http-tracker-core/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,21 @@ bittorrent-primitives = "0.1.0"
2020
bittorrent-tracker-core = { version = "3.0.0-develop", path = "../tracker-core" }
2121
criterion = { version = "0.5.1", features = ["async_tokio"] }
2222
futures = "0"
23+
serde = "1.0.219"
2324
thiserror = "2"
2425
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] }
26+
torrust-tracker-clock = { version = "3.0.0-develop", path = "../clock" }
2527
torrust-tracker-configuration = { version = "3.0.0-develop", path = "../configuration" }
28+
torrust-tracker-metrics = { version = "3.0.0-develop", path = "../metrics" }
2629
torrust-tracker-primitives = { version = "3.0.0-develop", path = "../primitives" }
2730
tracing = "0"
2831

2932
[dev-dependencies]
33+
formatjson = "0.3.1"
3034
mockall = "0"
35+
serde_json = "1.0.140"
3136
torrust-tracker-test-helpers = { version = "3.0.0-develop", path = "../test-helpers" }
3237

3338
[[bench]]
3439
harness = false
3540
name = "http_tracker_core_benchmark"
36-

packages/http-tracker-core/src/event/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::net::{IpAddr, SocketAddr};
22

3+
use torrust_tracker_metrics::label::{LabelName, LabelSet, LabelValue};
34
use torrust_tracker_primitives::service_binding::ServiceBinding;
45

56
pub mod sender;
@@ -59,3 +60,22 @@ pub struct ClientConnectionContext {
5960
pub struct ServerConnectionContext {
6061
service_binding: ServiceBinding,
6162
}
63+
64+
impl From<ConnectionContext> for LabelSet {
65+
fn from(connection_context: ConnectionContext) -> Self {
66+
LabelSet::from([
67+
(
68+
LabelName::new("server_binding_protocol"),
69+
LabelValue::new(&connection_context.server.service_binding.protocol().to_string()),
70+
),
71+
(
72+
LabelName::new("server_binding_ip"),
73+
LabelValue::new(&connection_context.server.service_binding.bind_address().ip().to_string()),
74+
),
75+
(
76+
LabelName::new("server_binding_port"),
77+
LabelValue::new(&connection_context.server.service_binding.bind_address().port().to_string()),
78+
),
79+
])
80+
}
81+
}

packages/http-tracker-core/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1+
use torrust_tracker_clock::clock;
2+
13
pub mod container;
24
pub mod event;
35
pub mod services;
46
pub mod statistics;
57

8+
/// This code needs to be copied into each crate.
9+
/// Working version, for production.
10+
#[cfg(not(test))]
11+
#[allow(dead_code)]
12+
pub(crate) type CurrentClock = clock::Working;
13+
14+
/// Stopped version, for testing.
15+
#[cfg(test)]
16+
#[allow(dead_code)]
17+
pub(crate) type CurrentClock = clock::Stopped;
18+
619
#[cfg(test)]
720
pub(crate) mod tests {
821
use bittorrent_primitives::info_hash::InfoHash;

packages/http-tracker-core/src/statistics/event/handler.rs

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,62 @@
11
use std::net::IpAddr;
22

3+
use torrust_tracker_metrics::label::LabelSet;
4+
use torrust_tracker_metrics::metric::MetricName;
5+
use torrust_tracker_primitives::DurationSinceUnixEpoch;
6+
37
use crate::event::Event;
48
use crate::statistics::repository::Repository;
59

610
/// # Panics
711
///
812
/// This function panics if the client IP address is not the same as the IP
913
/// version of the event.
10-
pub async fn handle_event(event: Event, stats_repository: &Repository) {
14+
pub async fn handle_event(event: Event, stats_repository: &Repository, now: DurationSinceUnixEpoch) {
1115
match event {
12-
Event::TcpAnnounce { connection } => match connection.client_ip_addr() {
13-
IpAddr::V4(_) => {
14-
stats_repository.increase_tcp4_announces().await;
15-
}
16-
IpAddr::V6(_) => {
17-
stats_repository.increase_tcp6_announces().await;
18-
}
19-
},
20-
Event::TcpScrape { connection } => match connection.client_ip_addr() {
21-
IpAddr::V4(_) => {
22-
stats_repository.increase_tcp4_scrapes().await;
16+
Event::TcpAnnounce { connection } => {
17+
// Global fixed metrics
18+
19+
match connection.client_ip_addr() {
20+
IpAddr::V4(_) => {
21+
stats_repository.increase_tcp4_announces().await;
22+
}
23+
IpAddr::V6(_) => {
24+
stats_repository.increase_tcp6_announces().await;
25+
}
2326
}
24-
IpAddr::V6(_) => {
25-
stats_repository.increase_tcp6_scrapes().await;
27+
28+
// Extendable metrics
29+
30+
stats_repository
31+
.increase_counter(
32+
&MetricName::new("http_tracker_core_announce_requests_received_total"),
33+
&LabelSet::from(connection),
34+
now,
35+
)
36+
.await;
37+
}
38+
Event::TcpScrape { connection } => {
39+
// Global fixed metrics
40+
41+
match connection.client_ip_addr() {
42+
IpAddr::V4(_) => {
43+
stats_repository.increase_tcp4_scrapes().await;
44+
}
45+
IpAddr::V6(_) => {
46+
stats_repository.increase_tcp6_scrapes().await;
47+
}
2648
}
27-
},
49+
50+
// Extendable metrics
51+
52+
stats_repository
53+
.increase_counter(
54+
&MetricName::new("http_tracker_core_scrape_requests_received_total"),
55+
&LabelSet::from(connection),
56+
now,
57+
)
58+
.await;
59+
}
2860
}
2961

3062
tracing::debug!("stats: {:?}", stats_repository.get_stats().await);
@@ -34,11 +66,13 @@ pub async fn handle_event(event: Event, stats_repository: &Repository) {
3466
mod tests {
3567
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
3668

69+
use torrust_tracker_clock::clock::Time;
3770
use torrust_tracker_primitives::service_binding::{Protocol, ServiceBinding};
3871

3972
use crate::event::{ConnectionContext, Event};
4073
use crate::statistics::event::handler::handle_event;
4174
use crate::statistics::repository::Repository;
75+
use crate::CurrentClock;
4276

4377
#[tokio::test]
4478
async fn should_increase_the_tcp4_announces_counter_when_it_receives_a_tcp4_announce_event() {
@@ -53,6 +87,7 @@ mod tests {
5387
),
5488
},
5589
&stats_repository,
90+
CurrentClock::now(),
5691
)
5792
.await;
5893

@@ -74,6 +109,7 @@ mod tests {
74109
),
75110
},
76111
&stats_repository,
112+
CurrentClock::now(),
77113
)
78114
.await;
79115

@@ -95,6 +131,7 @@ mod tests {
95131
),
96132
},
97133
&stats_repository,
134+
CurrentClock::now(),
98135
)
99136
.await;
100137

@@ -116,6 +153,7 @@ mod tests {
116153
),
117154
},
118155
&stats_repository,
156+
CurrentClock::now(),
119157
)
120158
.await;
121159

0 commit comments

Comments
 (0)