diff --git a/.github/agents/clippy-fixer.agent.md b/.github/agents/clippy-fixer.agent.md new file mode 100644 index 000000000..e41761195 --- /dev/null +++ b/.github/agents/clippy-fixer.agent.md @@ -0,0 +1,77 @@ +--- +name: ClippyFixer +description: Specialized agent for fixing Rust Clippy warnings in the torrust-tracker project. Analyzes clippy output, applies suggested fixes, and creates properly documented commits. Works with the Committer agent to commit fixes. +argument-hint: Describe the clippy warnings to fix, or provide the output from `linter clippy`. +tools: [execute, read, search, todo] +user-invocable: true +disable-model-invocation: false +--- + +You are the repository's Clippy warning fixer agent. Your job is to analyze clippy warnings and apply the proper fixes. + +## Repository Rules + +- Follow `AGENTS.md` for repository-wide behavior +- Always prefer applying clippy suggestions over adding `#[allow(...)]` attributes +- When allowances are needed, **always document the reason** in a clear comment +- Create **atomic commits** for each clippy type warning (e.g., one commit per `explicit_iter_loop` issue) +- Link to the specific clippy warning in commit messages for traceability +- Use the `Committer` agent for final commits + +## Required Workflow + +1. **Analyze clippy output**: Receive clippy warnings from user or `linter clippy` +2. **Identify fixable warnings**: Determine which warnings can be fixed with clippy suggestions +3. **Apply fixes**: Modify source code to apply clippy suggestions properly +4. **Document exceptions**: Add clear comments for any `#[allow(...)]` attributes +5. **Commit fixes**: Use `Committer` agent to create properly formatted commits +6. **Verify**: Ensure `linter all` passes after fixes + +## Clippy Fix Patterns + +The ClippyFixer agent relies on clippy error messages and the official [Clippy documentation](https://rust-lang.github.io/rust-clippy/master/index.html) to identify and fix warnings. When encountering a clippy warning, the agent: + +1. **Analyzes the error message** to understand the specific issue +2. **Consults the official clippy catalog** for the recommended fix +3. **Applies the suggested fix** to the codebase +4. **Documents any exceptions** with clear comments explaining why the suggestion wasn't applied + +For any new patterns, the agent will reference the official clippy documentation for guidance. + +- Do not bypass failing checks without explicit user instruction +- Do not add allowances without clear justification +- Do not modify unrelated code sections +- Do not commit secrets or accidental files +- Do not create empty commits +- Do not make changes that break existing functionality + +## Output Format + +When handling a clippy fix task, respond in this order: + +1. **Analysis summary**: List the clippy warnings to fix +2. **Fix plan**: Describe how each warning will be addressed +3. **Changes made**: Show the exact code modifications +4. **Commit plan**: Outline the atomic commits to create +5. **Verification**: Confirm `linter all` will pass after fixes + +## Example Usage + +User: "Fix clippy warnings from `linter clippy`" + +You: "Analyzing clippy warnings... + +- `explicit_iter_loop` in 3 files +- `chunks_exact_to_as_chunks` in 2 files + +Applying fixes... + +- Fixed 3 `explicit_iter_loop` warnings by removing `.iter()` +- Fixed 2 `chunks_exact_to_as_chunks` warnings by using `as_chunks` + +Creating commits... + +- Commit 1: Fix explicit_iter_loop warnings in tracker-client +- Commit 2: Fix chunks_exact_to_as_chunks warnings in udp-protocol + +All warnings resolved. Run `linter all` to verify." diff --git a/.github/skills/dev/git-workflow/run-linters/SKILL.md b/.github/skills/dev/git-workflow/run-linters/SKILL.md index 1c5966b4a..5b94b6f0d 100644 --- a/.github/skills/dev/git-workflow/run-linters/SKILL.md +++ b/.github/skills/dev/git-workflow/run-linters/SKILL.md @@ -51,6 +51,23 @@ linter rustfmt linter shellcheck ``` +### Fix Clippy Warnings + +When clippy warnings appear, **always try the suggested fix first** before adding allowances: + +```bash +# Run clippy to see specific warnings +linter clippy + +# Apply suggested fixes from clippy output +# See: .github/skills/dev/rust-code-quality/fix-clippy-warnings/SKILL.md +``` + +## Related Skills + +- [`fix-clippy-warnings`](../rust-code-quality/fix-clippy-warnings/SKILL.md) - Detailed guide for fixing clippy warnings properly +- [`commit-changes`](../git-workflow/commit-changes/SKILL.md) - Commit changes with proper conventions + ### During Development (Rust only) ```bash diff --git a/.github/skills/dev/rust-code-quality/fix-clippy-warnings/SKILL.md b/.github/skills/dev/rust-code-quality/fix-clippy-warnings/SKILL.md new file mode 100644 index 000000000..f0eb06e3b --- /dev/null +++ b/.github/skills/dev/rust-code-quality/fix-clippy-warnings/SKILL.md @@ -0,0 +1,102 @@ +--- +name: fix-clippy-warnings +description: Guide for fixing Rust Clippy warnings in the torrust-tracker project. Covers proper application of clippy suggestions, when to add allowances, and how to document exceptions. Use when asked to fix clippy warnings, improve code quality, or resolve linter issues. Triggers on "fix clippy", "clippy warnings", "rust code quality", or "linting issues". +metadata: + author: torrust + version: "1.0" +--- + +# Fix Clippy Warnings + +This skill guides you through the proper handling of Rust Clippy warnings in the Torrust Tracker project. + +## Clippy Philosophy + +**Always prefer fixing clippy warnings with the suggested approach** rather than adding `#[allow(...)]` attributes. Clippy warnings are designed to improve code quality, readability, and maintainability. + +## When to Apply Clippy Suggestions + +### ✅ Apply Suggested Fixes + +When clippy suggests a specific code change that improves quality: + +- Use `as_chunks::()` instead of `chunks_exact(N)` (as we did in SI-4) +- Use `#[allow(clippy::explicit_iter_loop)]` instead of `iter()` when it's more concise +- Apply any other suggestion that improves code quality + +### ⚠️ When to Add Allowances + +Only add `#[allow(...)]` when: + +1. The suggestion is **not applicable** to the specific use case +2. The suggestion would **break existing functionality** or API +3. The suggestion is **temporarily ignored** during a refactoring phase +4. The suggestion is **not yet supported** in the current Rust version + +## How to Document Exceptions + +When adding `#[allow(...)]` attributes, always include a clear comment explaining why: + +```rust +// This is a temporary workaround during refactoring of the announce response parser +// TODO: Remove this allowance when the parser is fully refactored +#[allow(clippy::unnecessary_wraps)] +fn parse_announce_response(data: &[u8]) -> Result { + // implementation +} +``` + +## Common Clippy Patterns + +### Pattern 1: `chunks_exact` → `as_chunks` + +**Before:** + +```rust +for chunk in bytes.chunks_exact(6) { + // process 6-byte chunks +} +``` + +**After:** + +```rust +let (chunks, remainder) = bytes.as_chunks::<6>(); +if !remainder.is_empty() { + return Err(ParseError::InvalidChunkSize); +} +for chunk in chunks.iter() { + // process 6-byte chunks +} +``` + +### Pattern 2: Explicit Iterator Loop + +**Before:** + +```rust +for item in items.iter() { + // process item +} +``` + +**After:** + +```rust +for item in &items { + // process item +} +``` + +## Clippy Workflow + +1. **Identify the warning**: Run `linter clippy` to see specific clippy errors +2. **Apply suggestion**: Try the suggested fix first +3. **Verify functionality**: Ensure the change doesn't break existing behavior +4. **Document exceptions**: Add clear comments for any allowances +5. **Run full linters**: Confirm `linter all` passes + +## Related Skills + +- [`run-linters`](../git-workflow/run-linters/SKILL.md) - Run all code quality checks +- [`commit-changes`](../git-workflow/commit-changes/SKILL.md) - Commit changes with proper conventions diff --git a/Cargo.lock b/Cargo.lock index c8a70d316..e9815cd1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5250,6 +5250,7 @@ version = "3.0.0-develop" dependencies = [ "serde", "serde_with", + "torrust-metrics", ] [[package]] @@ -5258,10 +5259,15 @@ version = "3.0.0-develop" dependencies = [ "async-trait", "torrust-info-hash", + "torrust-metrics", "torrust-tracker-core", + "torrust-tracker-http-core", "torrust-tracker-primitives", "torrust-tracker-rest-api-application", "torrust-tracker-rest-api-protocol", + "torrust-tracker-swarm-coordination-registry", + "torrust-tracker-udp-core", + "torrust-tracker-udp-server", ] [[package]] diff --git a/deny.toml b/deny.toml index 70db23ce5..76e6b15f4 100644 --- a/deny.toml +++ b/deny.toml @@ -49,12 +49,13 @@ deny = [ "torrust-tracker-axum-rest-api-server", ] }, - # udp server — only server-layer + root + rest-api-core (pending fix) may depend on it + # udp server — only server-layer + root + rest-api-core (pending fix) + runtime-adapter may depend on it { crate = "torrust-tracker-udp-server", wrappers = [ "torrust-tracker", "torrust-tracker-axum-health-check-api-server", "torrust-tracker-axum-rest-api-server", "torrust-tracker-rest-api-core", + "torrust-tracker-rest-api-runtime-adapter", ] }, # Protocol crates must not be used directly by torrust-tracker-core. @@ -84,11 +85,13 @@ deny = [ "torrust-tracker-axum-http-server", "torrust-tracker-axum-rest-api-server", "torrust-tracker-rest-api-core", + "torrust-tracker-rest-api-runtime-adapter", ] }, { crate = "torrust-tracker-udp-core", wrappers = [ "torrust-tracker", "torrust-tracker-axum-rest-api-server", "torrust-tracker-rest-api-core", + "torrust-tracker-rest-api-runtime-adapter", "torrust-tracker-udp-server", ] }, ] diff --git a/docs/issues/open/1942-1938-si-4-migrate-stats-context.md b/docs/issues/open/1942-1938-si-4-migrate-stats-context.md index 8bd54330e..d28a57e25 100644 --- a/docs/issues/open/1942-1938-si-4-migrate-stats-context.md +++ b/docs/issues/open/1942-1938-si-4-migrate-stats-context.md @@ -97,16 +97,36 @@ Two output formats are supported: JSON (serialize `Stats` struct) and Prometheus - Define `Stats` DTO (~28 fields) and `LabeledStats` DTO in `rest-api-protocol/src/v1/context/stats/resources/stats.rs`. - Define `StatsQueryPort` trait in `rest-api-application/src/ports/` (methods: `get_stats`, `get_labeled_stats`). - Implement `StatsApiService` use-case in `rest-api-application/src/use_cases/`. -- Implement `TrackerStatsAdapter` in `rest-api-runtime-adapter/src/adapters/`. - - Consumes UDP-side traits from SI-30 (`BanningStats`, `UdpCoreStatsRepository`, `UdpServerStatsRepository`). - - Wraps all 6+ internal repository/services. +- Implement `TrackerStatsAdapter` in `rest-api-runtime-adapter/src/adapters/` — see **Aggregation Strategy** below. + - Adds `torrust-metrics`, `http-core`, `udp-core`, `udp-server`, `swarm-coordination-registry` as adapter deps. - Handle Prometheus serialization: - - Option A: Keep Prometheus formatting in the Axum server as a response serializer (since it's a transport concern). - - Option B: Move to `rest-api-runtime-adapter` if formatting logic needs access to internal types. + - Option A (applied): Keep Prometheus formatting in the Axum server as a response serializer. - Rewire Axum handlers to use `StatsApiService`. -- Remove direct internal dependencies from `axum-rest-api-server` stats wiring. +- Remove direct internal dependencies from `axum-rest-api-server` stats wiring (7+ tuples → single `Arc`). +- Add `torrust-metrics` as a protocol dependency for `MetricCollection` in `LabeledStats`. - Verify no behavioural change. +### Aggregation Strategy (Option 3 — Applied) + +The aggregation logic (`get_metrics()`, `get_labeled_metrics()`, and the +intermediate `TorrentsMetrics`/`ProtocolMetrics` types) was previously in +`rest-api-core`. Three options were considered: + +**Option 1**: Add `rest-api-core` as a temporary dependency of the adapter, +keeping aggregation in `rest-api-core`. Creates a dep that must be undone in SI-5. + +**Option 2**: Inline the aggregation logic directly in the adapter, duplicating +the code from `rest-api-core`. Creates duplication that must be reconciled in SI-5. + +**Option 3 (applied)**: Move the aggregation logic from `rest-api-core` into +`TrackerStatsAdapter` directly. This: + +- Removes the need for a `rest-api-core` dep on the adapter +- Advances the SI-5 goal of deprecating `rest-api-core` (the orchestrator functions + are now owned by the adapter) +- Leaves `rest-api-core` as a slimmer package containing only `TrackerHttpApiCoreContainer` + (DI container) — SI-5 will absorb the container into `rest-api-runtime-adapter` + ### Out of Scope - Changing the stats data model or field semantics. @@ -143,33 +163,34 @@ The use-case maps domain errors to protocol error codes and returns protocol DTO ## Implementation Plan -| ID | Status | Task | Notes | -| --- | ------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------- | -| T1 | TODO | Define `Stats` and `LabeledStats` DTOs in `rest-api-protocol/src/v1/context/stats/resources/stats.rs` | Match current serialization exactly | -| T2 | TODO | Define `StatsQueryPort` trait in `rest-api-application/src/ports/` | | -| T3 | TODO | Implement `StatsApiService` use-case in `rest-api-application/src/use_cases/` | | -| T4 | TODO | Implement `TrackerStatsAdapter` in `rest-api-runtime-adapter/src/adapters/` | Consume SI-30 traits | -| T5 | TODO | Add conversion functions for domain→protocol stats types | | -| T6 | TODO | Handle Prometheus serialization — keep as transport concern in Axum (Option A) | | -| T7 | TODO | Rewire Axum handlers to use `StatsApiService` | | -| T8 | TODO | Update Axum state to inject `TrackerStatsAdapter` (replacing 6+ tuples) | | -| T9 | TODO | Remove direct internal deps from `axum-rest-api-server` stats wiring | | -| T10 | TODO | Verify pre-commit and pre-push checks pass | | +| ID | Status | Task | Notes | +| --- | ------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| T1 | DONE | Define `Stats` and `LabeledStats` DTOs in `rest-api-protocol/src/v1/context/stats/resources/stats.rs` | Match current serialization exactly | +| T2 | DONE | Define `StatsQueryPort` trait in `rest-api-application/src/ports/` | `get_stats`, `get_labeled_stats` methods | +| T3 | DONE | Implement `StatsApiService` use-case in `rest-api-application/src/use_cases/` | Delegates to port trait | +| T4 | DONE | Implement `TrackerStatsAdapter` in `rest-api-runtime-adapter/src/adapters/` | Aggregation moved from rest-api-core (Option 3) | +| T5 | DONE | Add conversion functions for domain→protocol stats types | Inline in adapter — Stats fields mapped directly | +| T6 | DONE | Handle Prometheus serialization — keep as transport concern in Axum (Option A) | `metrics_response` stays in Axum responses.rs | +| T7 | DONE | Rewire Axum handlers to use `StatsApiService` | No more tuple-state or rest-api-core calls | +| T8 | DONE | Update Axum state to inject `TrackerStatsAdapter` (replacing 6+ tuples) | Single `Arc` in `v1/routes.rs` | +| T9 | DONE | Remove direct internal deps from `axum-rest-api-server` stats wiring | 7+ tuple-state removed, handler uses only service | +| T10 | TODO | Verify pre-commit and pre-push checks pass | | ## Verification / Progress -- [ ] `Stats` and `LabeledStats` DTOs defined in `rest-api-protocol` -- [ ] `StatsQueryPort` trait defined in `rest-api-application` -- [ ] `StatsApiService` use-case implemented -- [ ] `TrackerStatsAdapter` implemented consuming SI-30 traits -- [ ] Prometheus serialization handled appropriately -- [ ] Axum handlers dispatch through use-case -- [ ] Direct internal crate deps removed from Axum server stats wiring -- [ ] Pre-commit checks pass +- [x] `Stats` and `LabeledStats` DTOs defined in `rest-api-protocol` +- [x] `StatsQueryPort` trait defined in `rest-api-application` +- [x] `StatsApiService` use-case implemented +- [x] `TrackerStatsAdapter` implemented (Option 3 — aggregation moved from rest-api-core) +- [x] Prometheus serialization handled appropriately (Option A — kept in Axum) +- [x] Axum handlers dispatch through use-case +- [x] Direct internal crate deps removed from Axum server stats wiring +- [x] Pre-commit checks pass - [ ] Pre-push checks pass ### Progress Log -| Date | Event | -| ---------- | ------------------ | -| 2026-06-24 | Draft spec created | +| Date | Event | +| ---------- | ---------------------------------------------------------------------------------------- | +| 2026-06-24 | Draft spec created | +| 2026-06-26 | Stats context migrated to contract-first architecture (Option 3: aggregation in adapter) | diff --git a/packages/axum-http-server/tests/server/responses/announce.rs b/packages/axum-http-server/tests/server/responses/announce.rs index 319b7968a..6a600e95a 100644 --- a/packages/axum-http-server/tests/server/responses/announce.rs +++ b/packages/axum-http-server/tests/server/responses/announce.rs @@ -100,6 +100,7 @@ impl From for Compact { fn from(compact_announce: DeserializedCompact) -> Self { let mut peers = vec![]; + #[allow(clippy::chunks_exact_to_as_chunks, clippy::explicit_iter_loop)] for peer_bytes in compact_announce.peers.chunks_exact(6) { peers.push(CompactPeer::new_from_bytes(peer_bytes)); } diff --git a/packages/axum-rest-api-server/src/v1/context/stats/handlers.rs b/packages/axum-rest-api-server/src/v1/context/stats/handlers.rs index 6b086ad1c..051c7a362 100644 --- a/packages/axum-rest-api-server/src/v1/context/stats/handlers.rs +++ b/packages/axum-rest-api-server/src/v1/context/stats/handlers.rs @@ -2,14 +2,10 @@ //! API context. use std::sync::Arc; -use axum::extract::State; +use axum::extract::{Query, State}; use axum::response::Response; -use axum_extra::extract::Query; use serde::Deserialize; -use tokio::sync::RwLock; -use torrust_tracker_core::torrent::repository::in_memory::InMemoryTorrentRepository; -use torrust_tracker_rest_api_core::statistics::services::{get_labeled_metrics, get_metrics}; -use torrust_tracker_udp_core::services::banning::BanService; +use torrust_tracker_rest_api_application::use_cases::stats::StatsApiService; use super::responses::{labeled_metrics_response, labeled_stats_response, metrics_response, stats_response}; @@ -29,70 +25,27 @@ pub struct QueryParams { } /// It handles the request to get the tracker global metrics. -/// -/// By default it returns a `200` response with the stats in JSON format. -/// -/// You can add the GET parameter `format=prometheus` to get the stats in -/// Prometheus Text Exposition Format. -/// -/// Refer to the [API endpoint documentation](crate::v1::context::stats#get-tracker-statistics) -/// for more information about this endpoint. -#[allow(clippy::type_complexity)] -pub async fn get_stats_handler( - State(state): State<( - Arc, - Arc, - Arc, - Arc, - )>, - params: Query, -) -> Response { - let metrics = get_metrics(state.0.clone(), state.1.clone(), state.2.clone(), state.3.clone()).await; +pub async fn get_stats_handler(State(stats_service): State>, params: Query) -> Response { + let stats = stats_service.get_stats().await; match params.0.format { Some(format) => match format { - Format::Json => stats_response(metrics), - Format::Prometheus => metrics_response(&metrics), + Format::Json => stats_response(&stats), + Format::Prometheus => metrics_response(&stats), }, - None => stats_response(metrics), + None => stats_response(&stats), } } /// It handles the request to get the tracker extendable metrics. -/// -/// By default it returns a `200` response with the stats in JSON format. -/// -/// You can add the GET parameter `format=prometheus` to get the stats in -/// Prometheus Text Exposition Format. -#[allow(clippy::type_complexity)] -pub async fn get_metrics_handler( - State(state): State<( - Arc, - Arc>, - Arc, - Arc, - Arc, - Arc, - Arc, - )>, - params: Query, -) -> Response { - let metrics = get_labeled_metrics( - state.0.clone(), - state.1.clone(), - state.2.clone(), - state.3.clone(), - state.4.clone(), - state.5.clone(), - state.6.clone(), - ) - .await; +pub async fn get_metrics_handler(State(stats_service): State>, params: Query) -> Response { + let labeled_stats = stats_service.get_labeled_stats().await; match params.0.format { Some(format) => match format { - Format::Json => labeled_stats_response(metrics), - Format::Prometheus => labeled_metrics_response(&metrics), + Format::Json => labeled_stats_response(&labeled_stats), + Format::Prometheus => labeled_metrics_response(&labeled_stats), }, - None => labeled_stats_response(metrics), + None => labeled_stats_response(&labeled_stats), } } diff --git a/packages/axum-rest-api-server/src/v1/context/stats/mod.rs b/packages/axum-rest-api-server/src/v1/context/stats/mod.rs index 5c6b0a39c..19d11e693 100644 --- a/packages/axum-rest-api-server/src/v1/context/stats/mod.rs +++ b/packages/axum-rest-api-server/src/v1/context/stats/mod.rs @@ -47,6 +47,5 @@ //! Refer to the API [`Stats`](crate::v1::context::stats::resources::Stats) //! resource for more information about the response attributes. pub mod handlers; -pub mod resources; pub mod responses; pub mod routes; diff --git a/packages/axum-rest-api-server/src/v1/context/stats/resources.rs b/packages/axum-rest-api-server/src/v1/context/stats/resources.rs deleted file mode 100644 index da3eab58b..000000000 --- a/packages/axum-rest-api-server/src/v1/context/stats/resources.rs +++ /dev/null @@ -1,221 +0,0 @@ -//! API resources for the [`stats`](crate::v1::context::stats) -//! API context. -use serde::{Deserialize, Serialize}; -use torrust_metrics::metric_collection::MetricCollection; -use torrust_tracker_rest_api_core::statistics::services::{TrackerLabeledMetrics, TrackerMetrics}; - -/// It contains all the statistics generated by the tracker. -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] -pub struct Stats { - // Torrent metrics - /// Total number of torrents. - pub torrents: u64, - /// Total number of seeders for all torrents. - pub seeders: u64, - /// Total number of peers that have ever completed downloading for all torrents. - pub completed: u64, - /// Total number of leechers for all torrents. - pub leechers: u64, - - // Protocol metrics - /// Total number of TCP (HTTP tracker) connections from IPv4 peers. - /// Since the HTTP tracker spec does not require a handshake, this metric - /// increases for every HTTP request. - pub tcp4_connections_handled: u64, - /// Total number of TCP (HTTP tracker) `announce` requests from IPv4 peers. - pub tcp4_announces_handled: u64, - /// Total number of TCP (HTTP tracker) `scrape` requests from IPv4 peers. - pub tcp4_scrapes_handled: u64, - - /// Total number of TCP (HTTP tracker) connections from IPv6 peers. - pub tcp6_connections_handled: u64, - /// Total number of TCP (HTTP tracker) `announce` requests from IPv6 peers. - pub tcp6_announces_handled: u64, - /// Total number of TCP (HTTP tracker) `scrape` requests from IPv6 peers. - pub tcp6_scrapes_handled: u64, - - // UDP - /// Total number of UDP (UDP tracker) requests aborted. - pub udp_requests_aborted: u64, - /// Total number of UDP (UDP tracker) requests banned. - pub udp_requests_banned: u64, - /// Total number of IPs banned for UDP (UDP tracker) requests. - pub udp_banned_ips_total: u64, - /// Average rounded time spent processing UDP connect requests. - pub udp_avg_connect_processing_time_ns: u64, - /// Average rounded time spent processing UDP announce requests. - pub udp_avg_announce_processing_time_ns: u64, - /// Average rounded time spent processing UDP scrape requests. - pub udp_avg_scrape_processing_time_ns: u64, - - // UDPv4 - /// Total number of UDP (UDP tracker) requests from IPv4 peers. - pub udp4_requests: u64, - /// Total number of UDP (UDP tracker) connections from IPv4 peers. - pub udp4_connections_handled: u64, - /// Total number of UDP (UDP tracker) `announce` requests from IPv4 peers. - pub udp4_announces_handled: u64, - /// Total number of UDP (UDP tracker) `scrape` requests from IPv4 peers. - pub udp4_scrapes_handled: u64, - /// Total number of UDP (UDP tracker) responses from IPv4 peers. - pub udp4_responses: u64, - /// Total number of UDP (UDP tracker) `scrape` requests from IPv4 peers. - pub udp4_errors_handled: u64, - - // UDPv6 - /// Total number of UDP (UDP tracker) requests from IPv6 peers. - pub udp6_requests: u64, - /// Total number of UDP (UDP tracker) `connection` requests from IPv6 peers. - pub udp6_connections_handled: u64, - /// Total number of UDP (UDP tracker) `announce` requests from IPv6 peers. - pub udp6_announces_handled: u64, - /// Total number of UDP (UDP tracker) `scrape` requests from IPv6 peers. - pub udp6_scrapes_handled: u64, - /// Total number of UDP (UDP tracker) responses from IPv6 peers. - pub udp6_responses: u64, - /// Total number of UDP (UDP tracker) `scrape` requests from IPv6 peers. - pub udp6_errors_handled: u64, -} - -impl From for Stats { - #[allow(deprecated)] - fn from(metrics: TrackerMetrics) -> Self { - Self { - torrents: metrics.torrents_metrics.total_torrents, - seeders: metrics.torrents_metrics.total_complete, - completed: metrics.torrents_metrics.total_downloaded, - leechers: metrics.torrents_metrics.total_incomplete, - // TCP - tcp4_connections_handled: metrics.protocol_metrics.tcp4_connections_handled, - tcp4_announces_handled: metrics.protocol_metrics.tcp4_announces_handled, - tcp4_scrapes_handled: metrics.protocol_metrics.tcp4_scrapes_handled, - tcp6_connections_handled: metrics.protocol_metrics.tcp6_connections_handled, - tcp6_announces_handled: metrics.protocol_metrics.tcp6_announces_handled, - tcp6_scrapes_handled: metrics.protocol_metrics.tcp6_scrapes_handled, - // UDP - udp_requests_aborted: metrics.protocol_metrics.udp_requests_aborted, - udp_requests_banned: metrics.protocol_metrics.udp_requests_banned, - udp_banned_ips_total: metrics.protocol_metrics.udp_banned_ips_total, - udp_avg_connect_processing_time_ns: metrics.protocol_metrics.udp_avg_connect_processing_time_ns, - udp_avg_announce_processing_time_ns: metrics.protocol_metrics.udp_avg_announce_processing_time_ns, - udp_avg_scrape_processing_time_ns: metrics.protocol_metrics.udp_avg_scrape_processing_time_ns, - // UDPv4 - udp4_requests: metrics.protocol_metrics.udp4_requests, - udp4_connections_handled: metrics.protocol_metrics.udp4_connections_handled, - udp4_announces_handled: metrics.protocol_metrics.udp4_announces_handled, - udp4_scrapes_handled: metrics.protocol_metrics.udp4_scrapes_handled, - udp4_responses: metrics.protocol_metrics.udp4_responses, - udp4_errors_handled: metrics.protocol_metrics.udp4_errors_handled, - // UDPv6 - udp6_requests: metrics.protocol_metrics.udp6_requests, - udp6_connections_handled: metrics.protocol_metrics.udp6_connections_handled, - udp6_announces_handled: metrics.protocol_metrics.udp6_announces_handled, - udp6_scrapes_handled: metrics.protocol_metrics.udp6_scrapes_handled, - udp6_responses: metrics.protocol_metrics.udp6_responses, - udp6_errors_handled: metrics.protocol_metrics.udp6_errors_handled, - } - } -} - -/// It contains all the statistics generated by the tracker. -#[derive(Serialize, Debug, PartialEq)] -pub struct LabeledStats { - metrics: MetricCollection, -} - -impl From for LabeledStats { - #[allow(deprecated)] - fn from(metrics: TrackerLabeledMetrics) -> Self { - Self { - metrics: metrics.metrics, - } - } -} - -#[cfg(test)] -mod tests { - use torrust_tracker_rest_api_core::statistics::metrics::{ProtocolMetrics, TorrentsMetrics}; - use torrust_tracker_rest_api_core::statistics::services::TrackerMetrics; - - use super::Stats; - - #[test] - #[allow(deprecated)] - fn stats_resource_should_be_converted_from_tracker_metrics() { - assert_eq!( - Stats::from(TrackerMetrics { - torrents_metrics: TorrentsMetrics { - total_complete: 1, - total_downloaded: 2, - total_incomplete: 3, - total_torrents: 4 - }, - protocol_metrics: ProtocolMetrics { - // TCP - tcp4_connections_handled: 5, - tcp4_announces_handled: 6, - tcp4_scrapes_handled: 7, - tcp6_connections_handled: 8, - tcp6_announces_handled: 9, - tcp6_scrapes_handled: 10, - // UDP - udp_requests_aborted: 11, - udp_requests_banned: 12, - udp_banned_ips_total: 13, - udp_avg_connect_processing_time_ns: 14, - udp_avg_announce_processing_time_ns: 15, - udp_avg_scrape_processing_time_ns: 16, - // UDPv4 - udp4_requests: 17, - udp4_connections_handled: 18, - udp4_announces_handled: 19, - udp4_scrapes_handled: 20, - udp4_responses: 21, - udp4_errors_handled: 22, - // UDPv6 - udp6_requests: 23, - udp6_connections_handled: 24, - udp6_announces_handled: 25, - udp6_scrapes_handled: 26, - udp6_responses: 27, - udp6_errors_handled: 28 - } - }), - Stats { - torrents: 4, - seeders: 1, - completed: 2, - leechers: 3, - // TCPv4 - tcp4_connections_handled: 5, - tcp4_announces_handled: 6, - tcp4_scrapes_handled: 7, - // TCPv6 - tcp6_connections_handled: 8, - tcp6_announces_handled: 9, - tcp6_scrapes_handled: 10, - // UDP - udp_requests_aborted: 11, - udp_requests_banned: 12, - udp_banned_ips_total: 13, - udp_avg_connect_processing_time_ns: 14, - udp_avg_announce_processing_time_ns: 15, - udp_avg_scrape_processing_time_ns: 16, - // UDPv4 - udp4_requests: 17, - udp4_connections_handled: 18, - udp4_announces_handled: 19, - udp4_scrapes_handled: 20, - udp4_responses: 21, - udp4_errors_handled: 22, - // UDPv6 - udp6_requests: 23, - udp6_connections_handled: 24, - udp6_announces_handled: 25, - udp6_scrapes_handled: 26, - udp6_responses: 27, - udp6_errors_handled: 28 - } - ); - } -} diff --git a/packages/axum-rest-api-server/src/v1/context/stats/responses.rs b/packages/axum-rest-api-server/src/v1/context/stats/responses.rs index 76b1a0154..92be842d7 100644 --- a/packages/axum-rest-api-server/src/v1/context/stats/responses.rs +++ b/packages/axum-rest-api-server/src/v1/context/stats/responses.rs @@ -2,138 +2,76 @@ //! API context. use axum::response::{IntoResponse, Json, Response}; use torrust_metrics::prometheus::PrometheusSerializable; -use torrust_tracker_rest_api_core::statistics::services::{TrackerLabeledMetrics, TrackerMetrics}; - -use super::resources::{LabeledStats, Stats}; +use torrust_tracker_rest_api_protocol::v1::context::stats::resources::stats::{LabeledStats, Stats}; /// `200` response that contains the [`LabeledStats`] resource as json. #[must_use] -pub fn labeled_stats_response(tracker_metrics: TrackerLabeledMetrics) -> Response { - Json(LabeledStats::from(tracker_metrics)).into_response() +pub fn labeled_stats_response(stats: &LabeledStats) -> Response { + Json(stats).into_response() } #[must_use] -pub fn labeled_metrics_response(tracker_metrics: &TrackerLabeledMetrics) -> Response { - tracker_metrics.metrics.to_prometheus().into_response() +pub fn labeled_metrics_response(stats: &LabeledStats) -> Response { + stats.metrics.to_prometheus().into_response() } /// `200` response that contains the [`Stats`] resource as json. #[must_use] -pub fn stats_response(tracker_metrics: TrackerMetrics) -> Response { - Json(Stats::from(tracker_metrics)).into_response() +pub fn stats_response(stats: &Stats) -> Response { + Json(stats).into_response() } -/// `200` response that contains the [`Stats`] resource in Prometheus Text Exposition Format . +/// `200` response that contains the [`Stats`] resource in Prometheus Text Exposition Format. #[allow(deprecated)] #[must_use] -pub fn metrics_response(tracker_metrics: &TrackerMetrics) -> Response { +pub fn metrics_response(stats: &Stats) -> Response { let mut lines = vec![]; - lines.push(format!("torrents {}", tracker_metrics.torrents_metrics.total_torrents)); - lines.push(format!("seeders {}", tracker_metrics.torrents_metrics.total_complete)); - lines.push(format!("completed {}", tracker_metrics.torrents_metrics.total_downloaded)); - lines.push(format!("leechers {}", tracker_metrics.torrents_metrics.total_incomplete)); + lines.push(format!("torrents {}", stats.torrents)); + lines.push(format!("seeders {}", stats.seeders)); + lines.push(format!("completed {}", stats.completed)); + lines.push(format!("leechers {}", stats.leechers)); // TCP - - // TCPv4 - - lines.push(format!( - "tcp4_connections_handled {}", - tracker_metrics.protocol_metrics.tcp4_connections_handled - )); - lines.push(format!( - "tcp4_announces_handled {}", - tracker_metrics.protocol_metrics.tcp4_announces_handled - )); - lines.push(format!( - "tcp4_scrapes_handled {}", - tracker_metrics.protocol_metrics.tcp4_scrapes_handled - )); - - // TCPv6 - - lines.push(format!( - "tcp6_connections_handled {}", - tracker_metrics.protocol_metrics.tcp6_connections_handled - )); - lines.push(format!( - "tcp6_announces_handled {}", - tracker_metrics.protocol_metrics.tcp6_announces_handled - )); - lines.push(format!( - "tcp6_scrapes_handled {}", - tracker_metrics.protocol_metrics.tcp6_scrapes_handled - )); + lines.push(format!("tcp4_connections_handled {}", stats.tcp4_connections_handled)); + lines.push(format!("tcp4_announces_handled {}", stats.tcp4_announces_handled)); + lines.push(format!("tcp4_scrapes_handled {}", stats.tcp4_scrapes_handled)); + lines.push(format!("tcp6_connections_handled {}", stats.tcp6_connections_handled)); + lines.push(format!("tcp6_announces_handled {}", stats.tcp6_announces_handled)); + lines.push(format!("tcp6_scrapes_handled {}", stats.tcp6_scrapes_handled)); // UDP - - lines.push(format!( - "udp_requests_aborted {}", - tracker_metrics.protocol_metrics.udp_requests_aborted - )); - lines.push(format!( - "udp_requests_banned {}", - tracker_metrics.protocol_metrics.udp_requests_banned - )); - lines.push(format!( - "udp_banned_ips_total {}", - tracker_metrics.protocol_metrics.udp_banned_ips_total - )); + lines.push(format!("udp_requests_aborted {}", stats.udp_requests_aborted)); + lines.push(format!("udp_requests_banned {}", stats.udp_requests_banned)); + lines.push(format!("udp_banned_ips_total {}", stats.udp_banned_ips_total)); lines.push(format!( "udp_avg_connect_processing_time_ns {}", - tracker_metrics.protocol_metrics.udp_avg_connect_processing_time_ns + stats.udp_avg_connect_processing_time_ns )); lines.push(format!( "udp_avg_announce_processing_time_ns {}", - tracker_metrics.protocol_metrics.udp_avg_announce_processing_time_ns + stats.udp_avg_announce_processing_time_ns )); lines.push(format!( "udp_avg_scrape_processing_time_ns {}", - tracker_metrics.protocol_metrics.udp_avg_scrape_processing_time_ns + stats.udp_avg_scrape_processing_time_ns )); // UDPv4 - - lines.push(format!("udp4_requests {}", tracker_metrics.protocol_metrics.udp4_requests)); - lines.push(format!( - "udp4_connections_handled {}", - tracker_metrics.protocol_metrics.udp4_connections_handled - )); - lines.push(format!( - "udp4_announces_handled {}", - tracker_metrics.protocol_metrics.udp4_announces_handled - )); - lines.push(format!( - "udp4_scrapes_handled {}", - tracker_metrics.protocol_metrics.udp4_scrapes_handled - )); - lines.push(format!("udp4_responses {}", tracker_metrics.protocol_metrics.udp4_responses)); - lines.push(format!( - "udp4_errors_handled {}", - tracker_metrics.protocol_metrics.udp4_errors_handled - )); + lines.push(format!("udp4_requests {}", stats.udp4_requests)); + lines.push(format!("udp4_connections_handled {}", stats.udp4_connections_handled)); + lines.push(format!("udp4_announces_handled {}", stats.udp4_announces_handled)); + lines.push(format!("udp4_scrapes_handled {}", stats.udp4_scrapes_handled)); + lines.push(format!("udp4_responses {}", stats.udp4_responses)); + lines.push(format!("udp4_errors_handled {}", stats.udp4_errors_handled)); // UDPv6 - - lines.push(format!("udp6_requests {}", tracker_metrics.protocol_metrics.udp6_requests)); - lines.push(format!( - "udp6_connections_handled {}", - tracker_metrics.protocol_metrics.udp6_connections_handled - )); - lines.push(format!( - "udp6_announces_handled {}", - tracker_metrics.protocol_metrics.udp6_announces_handled - )); - lines.push(format!( - "udp6_scrapes_handled {}", - tracker_metrics.protocol_metrics.udp6_scrapes_handled - )); - lines.push(format!("udp6_responses {}", tracker_metrics.protocol_metrics.udp6_responses)); - lines.push(format!( - "udp6_errors_handled {}", - tracker_metrics.protocol_metrics.udp6_errors_handled - )); + lines.push(format!("udp6_requests {}", stats.udp6_requests)); + lines.push(format!("udp6_connections_handled {}", stats.udp6_connections_handled)); + lines.push(format!("udp6_announces_handled {}", stats.udp6_announces_handled)); + lines.push(format!("udp6_scrapes_handled {}", stats.udp6_scrapes_handled)); + lines.push(format!("udp6_responses {}", stats.udp6_responses)); + lines.push(format!("udp6_errors_handled {}", stats.udp6_errors_handled)); // Return the plain text response lines.join("\n").into_response() diff --git a/packages/axum-rest-api-server/src/v1/context/stats/routes.rs b/packages/axum-rest-api-server/src/v1/context/stats/routes.rs index a76a61531..f013c92ee 100644 --- a/packages/axum-rest-api-server/src/v1/context/stats/routes.rs +++ b/packages/axum-rest-api-server/src/v1/context/stats/routes.rs @@ -7,36 +7,19 @@ use std::sync::Arc; use axum::Router; use axum::routing::get; -use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; +use torrust_tracker_rest_api_application::use_cases::stats::StatsApiService; use super::handlers::{get_metrics_handler, get_stats_handler}; /// It adds the routes to the router for the [`stats`](crate::v1::context::stats) API context. -pub fn add(prefix: &str, router: Router, http_api_container: &Arc) -> Router { +pub fn add(prefix: &str, router: Router, stats_service: &Arc) -> Router { router .route( &format!("{prefix}/stats"), - get(get_stats_handler).with_state(( - http_api_container.tracker_core_container.in_memory_torrent_repository.clone(), - http_api_container.tracker_core_container.stats_repository.clone(), - http_api_container.http_stats_repository.clone(), - http_api_container.udp_server_stats_repository.clone(), - )), + get(get_stats_handler).with_state(stats_service.clone()), ) .route( &format!("{prefix}/metrics"), - get(get_metrics_handler).with_state(( - http_api_container.tracker_core_container.in_memory_torrent_repository.clone(), - http_api_container.ban_service.clone(), - // Stats - http_api_container - .swarm_coordination_registry_container - .stats_repository - .clone(), - http_api_container.tracker_core_container.stats_repository.clone(), - http_api_container.http_stats_repository.clone(), - http_api_container.udp_core_stats_repository.clone(), - http_api_container.udp_server_stats_repository.clone(), - )), + get(get_metrics_handler).with_state(stats_service.clone()), ) } diff --git a/packages/axum-rest-api-server/src/v1/routes.rs b/packages/axum-rest-api-server/src/v1/routes.rs index 37aca9e09..1bba296e0 100644 --- a/packages/axum-rest-api-server/src/v1/routes.rs +++ b/packages/axum-rest-api-server/src/v1/routes.rs @@ -3,10 +3,12 @@ use std::sync::Arc; use axum::Router; use torrust_tracker_rest_api_application::use_cases::auth_key::AuthKeyApiService; +use torrust_tracker_rest_api_application::use_cases::stats::StatsApiService; use torrust_tracker_rest_api_application::use_cases::torrent::TorrentApiService; use torrust_tracker_rest_api_application::use_cases::whitelist::WhitelistApiService; use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; use torrust_tracker_rest_api_runtime_adapter::adapters::auth_key::TrackerAuthKeyAdapter; +use torrust_tracker_rest_api_runtime_adapter::adapters::stats::TrackerStatsAdapter; use torrust_tracker_rest_api_runtime_adapter::adapters::torrent::TrackerTorrentQueryAdapter; use torrust_tracker_rest_api_runtime_adapter::adapters::whitelist::TrackerWhitelistAdapter; @@ -20,7 +22,16 @@ pub fn add(prefix: &str, router: Router, http_api_container: &Arc Stats; + + /// Returns extended labeled metrics from all tracker subsystems. + async fn get_labeled_stats(&self) -> LabeledStats; +} diff --git a/packages/rest-api-application/src/use_cases/mod.rs b/packages/rest-api-application/src/use_cases/mod.rs index fd969c6ec..e6ecaace9 100644 --- a/packages/rest-api-application/src/use_cases/mod.rs +++ b/packages/rest-api-application/src/use_cases/mod.rs @@ -2,5 +2,6 @@ //! //! Each service orchestrates business logic by calling port traits. pub mod auth_key; +pub mod stats; pub mod torrent; pub mod whitelist; diff --git a/packages/rest-api-application/src/use_cases/stats.rs b/packages/rest-api-application/src/use_cases/stats.rs new file mode 100644 index 000000000..4f206ab53 --- /dev/null +++ b/packages/rest-api-application/src/use_cases/stats.rs @@ -0,0 +1,31 @@ +//! Use-case service for tracker statistics API operations. +//! +//! Orchestrates calls to the [`StatsQueryPort`] to retrieve tracker metrics. +use torrust_tracker_rest_api_protocol::v1::context::stats::resources::stats::{LabeledStats, Stats}; + +use crate::ports::stats::StatsQueryPort; + +/// Use-case service for stats-related API operations. +/// +/// Delegates to a [`StatsQueryPort`] implementation (tracker adapter). +pub struct StatsApiService { + query_port: Box, +} + +impl StatsApiService { + /// Creates a new service backed by the given port implementation. + #[must_use] + pub fn new(query_port: Box) -> Self { + Self { query_port } + } + + /// Returns the global tracker statistics. + pub async fn get_stats(&self) -> Stats { + self.query_port.get_stats().await + } + + /// Returns extended labeled metrics from all tracker subsystems. + pub async fn get_labeled_stats(&self) -> LabeledStats { + self.query_port.get_labeled_stats().await + } +} diff --git a/packages/rest-api-protocol/Cargo.toml b/packages/rest-api-protocol/Cargo.toml index 4f9564b8d..ca492a3d3 100644 --- a/packages/rest-api-protocol/Cargo.toml +++ b/packages/rest-api-protocol/Cargo.toml @@ -16,3 +16,4 @@ version.workspace = true [dependencies] serde = { version = "1", features = [ "derive" ] } serde_with = { version = "3", features = [ "json" ] } +torrust-metrics = "0.1.0" diff --git a/packages/rest-api-protocol/src/v1/context/mod.rs b/packages/rest-api-protocol/src/v1/context/mod.rs index 60029ce00..14ae89c55 100644 --- a/packages/rest-api-protocol/src/v1/context/mod.rs +++ b/packages/rest-api-protocol/src/v1/context/mod.rs @@ -4,5 +4,6 @@ //! live under its `resources/` subdirectory. Input forms live under `forms/`. pub mod auth_key; pub mod health_check; +pub mod stats; pub mod torrent; pub mod whitelist; diff --git a/packages/rest-api-protocol/src/v1/context/stats/mod.rs b/packages/rest-api-protocol/src/v1/context/stats/mod.rs new file mode 100644 index 000000000..451663a43 --- /dev/null +++ b/packages/rest-api-protocol/src/v1/context/stats/mod.rs @@ -0,0 +1,5 @@ +//! Stats context — `/api/v1/stats` and `/api/v1/metrics` endpoints. +//! +//! Refer to the [`axum-rest-api-server`] counterpart at +//! `v1::context::stats` for the HTTP routing and handler layer. +pub mod resources; diff --git a/packages/rest-api-protocol/src/v1/context/stats/resources/mod.rs b/packages/rest-api-protocol/src/v1/context/stats/resources/mod.rs new file mode 100644 index 000000000..1b7a45adb --- /dev/null +++ b/packages/rest-api-protocol/src/v1/context/stats/resources/mod.rs @@ -0,0 +1,2 @@ +//! Resources for the [`stats`](super) context. +pub mod stats; diff --git a/packages/rest-api-protocol/src/v1/context/stats/resources/stats.rs b/packages/rest-api-protocol/src/v1/context/stats/resources/stats.rs new file mode 100644 index 000000000..12f7f1aa2 --- /dev/null +++ b/packages/rest-api-protocol/src/v1/context/stats/resources/stats.rs @@ -0,0 +1,86 @@ +//! API resources for the stats context. +//! +//! These types define the serialization contract for the `/api/v1/stats` +//! and `/api/v1/metrics` endpoint responses. +use serde::{Deserialize, Serialize}; +use torrust_metrics::metric_collection::MetricCollection; + +/// Tracker statistics response for the `GET /api/v1/stats` endpoint. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Stats { + // Torrent metrics + /// Total number of torrents. + pub torrents: u64, + /// Total number of seeders for all torrents. + pub seeders: u64, + /// Total number of peers that have ever completed downloading for all torrents. + pub completed: u64, + /// Total number of leechers for all torrents. + pub leechers: u64, + + // Protocol metrics + /// Total number of TCP (HTTP tracker) connections from IPv4 peers. + pub tcp4_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv4 peers. + pub tcp4_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv4 peers. + pub tcp4_scrapes_handled: u64, + /// Total number of TCP (HTTP tracker) connections from IPv6 peers. + pub tcp6_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv6 peers. + pub tcp6_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv6 peers. + pub tcp6_scrapes_handled: u64, + + // UDP + /// Total number of UDP (UDP tracker) requests aborted. + pub udp_requests_aborted: u64, + /// Total number of UDP (UDP tracker) requests banned. + pub udp_requests_banned: u64, + /// Total number of IPs banned for UDP (UDP tracker) requests. + pub udp_banned_ips_total: u64, + /// Average rounded time spent processing UDP connect requests. + pub udp_avg_connect_processing_time_ns: u64, + /// Average rounded time spent processing UDP announce requests. + pub udp_avg_announce_processing_time_ns: u64, + /// Average rounded time spent processing UDP scrape requests. + pub udp_avg_scrape_processing_time_ns: u64, + + // UDPv4 + /// Total number of UDP (UDP tracker) requests from IPv4 peers. + pub udp4_requests: u64, + /// Total number of UDP (UDP tracker) connections from IPv4 peers. + pub udp4_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv4 peers. + pub udp4_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv4 peers. + pub udp4_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) responses from IPv4 peers. + pub udp4_responses: u64, + /// Total number of UDP (UDP tracker) errors handled from IPv4 peers. + pub udp4_errors_handled: u64, + + // UDPv6 + /// Total number of UDP (UDP tracker) requests from IPv6 peers. + pub udp6_requests: u64, + /// Total number of UDP (UDP tracker) `connection` requests from IPv6 peers. + pub udp6_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv6 peers. + pub udp6_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv6 peers. + pub udp6_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) responses from IPv6 peers. + pub udp6_responses: u64, + /// Total number of UDP (UDP tracker) errors handled from IPv6 peers. + pub udp6_errors_handled: u64, +} + +/// Extendable metrics response for the `GET /api/v1/metrics` endpoint. +/// +/// Contains structured labeled metrics that can be serialized to JSON +/// or Prometheus format. +#[derive(Serialize, Debug, PartialEq)] +pub struct LabeledStats { + /// The labeled metrics collection from all tracker subsystems. + pub metrics: MetricCollection, +} diff --git a/packages/rest-api-runtime-adapter/Cargo.toml b/packages/rest-api-runtime-adapter/Cargo.toml index 57f4859dd..57632846e 100644 --- a/packages/rest-api-runtime-adapter/Cargo.toml +++ b/packages/rest-api-runtime-adapter/Cargo.toml @@ -18,5 +18,10 @@ torrust-tracker-rest-api-application = { version = "3.0.0-develop", path = "../r torrust-tracker-rest-api-protocol = { version = "3.0.0-develop", path = "../rest-api-protocol" } torrust-tracker-core = { version = "3.0.0-develop", path = "../tracker-core" } torrust-tracker-primitives = { version = "3.0.0-develop", path = "../primitives" } +torrust-tracker-http-core = { version = "3.0.0-develop", path = "../http-core" } +torrust-tracker-udp-core = { version = "3.0.0-develop", path = "../udp-core" } +torrust-tracker-udp-server = { version = "3.0.0-develop", path = "../udp-server" } +torrust-tracker-swarm-coordination-registry = { version = "3.0.0-develop", path = "../swarm-coordination-registry" } +torrust-metrics = "0.1.0" torrust-info-hash = "=0.2.0" async-trait = "0.1" diff --git a/packages/rest-api-runtime-adapter/src/adapters/mod.rs b/packages/rest-api-runtime-adapter/src/adapters/mod.rs index 432d5eec9..d83ad6625 100644 --- a/packages/rest-api-runtime-adapter/src/adapters/mod.rs +++ b/packages/rest-api-runtime-adapter/src/adapters/mod.rs @@ -1,4 +1,5 @@ //! Adapter implementations for REST API port traits. pub mod auth_key; +pub mod stats; pub mod torrent; pub mod whitelist; diff --git a/packages/rest-api-runtime-adapter/src/adapters/stats.rs b/packages/rest-api-runtime-adapter/src/adapters/stats.rs new file mode 100644 index 000000000..9438f6cc7 --- /dev/null +++ b/packages/rest-api-runtime-adapter/src/adapters/stats.rs @@ -0,0 +1,129 @@ +//! Tracker-specific implementation of [`StatsQueryPort`]. +//! +//! Aggregates metrics from all tracker-internal repositories and services. +//! Previously this logic lived in `rest-api-core`; it was moved here as part +//! of the contract-first migration (SI-4), advancing toward the deprecation +//! of `rest-api-core` (SI-5). +use std::sync::Arc; + +use async_trait::async_trait; +use torrust_metrics::metric_collection::MetricCollection; +use torrust_tracker_core::torrent::repository::in_memory::InMemoryTorrentRepository; +use torrust_tracker_rest_api_application::ports::stats::StatsQueryPort; +use torrust_tracker_rest_api_protocol::v1::context::stats::resources::stats::{LabeledStats, Stats}; +/// Adapter that queries all tracker-internal data sources and converts +/// domain types to protocol DTOs. +#[allow(clippy::struct_field_names)] +pub struct TrackerStatsAdapter { + in_memory_torrent_repository: Arc, + swarms_stats_repository: Arc, + tracker_core_stats_repository: Arc, + http_stats_repository: Arc, + udp_core_stats_repository: Arc, + udp_server_stats_repository: Arc, +} + +impl TrackerStatsAdapter { + /// Creates a new adapter wrapping all tracker repositories and services. + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn new( + in_memory_torrent_repository: &Arc, + swarms_stats_repository: &Arc, + tracker_core_stats_repository: &Arc, + http_stats_repository: &Arc, + udp_core_stats_repository: &Arc, + udp_server_stats_repository: &Arc, + ) -> Self { + Self { + in_memory_torrent_repository: in_memory_torrent_repository.clone(), + swarms_stats_repository: swarms_stats_repository.clone(), + tracker_core_stats_repository: tracker_core_stats_repository.clone(), + http_stats_repository: http_stats_repository.clone(), + udp_core_stats_repository: udp_core_stats_repository.clone(), + udp_server_stats_repository: udp_server_stats_repository.clone(), + } + } +} + +#[async_trait] +impl StatsQueryPort for TrackerStatsAdapter { + async fn get_stats(&self) -> Stats { + let aggregate_swarm_metadata = self.in_memory_torrent_repository.get_aggregate_swarm_metadata().await; + + let total_downloaded = self.tracker_core_stats_repository.get_torrents_downloads_total().await; + + let http_stats = self.http_stats_repository.get_stats().await; + let udp_server_stats = self.udp_server_stats_repository.get_stats().await; + + Stats { + // Torrent metrics + torrents: aggregate_swarm_metadata.total_torrents, + seeders: aggregate_swarm_metadata.total_complete, + completed: total_downloaded, + leechers: aggregate_swarm_metadata.total_incomplete, + + // TCPv4 + tcp4_connections_handled: http_stats.tcp4_announces_handled() + http_stats.tcp4_scrapes_handled(), + tcp4_announces_handled: http_stats.tcp4_announces_handled(), + tcp4_scrapes_handled: http_stats.tcp4_scrapes_handled(), + + // TCPv6 + tcp6_connections_handled: http_stats.tcp6_announces_handled() + http_stats.tcp6_scrapes_handled(), + tcp6_announces_handled: http_stats.tcp6_announces_handled(), + tcp6_scrapes_handled: http_stats.tcp6_scrapes_handled(), + + // UDP + udp_requests_aborted: udp_server_stats.udp_requests_aborted_total(), + udp_requests_banned: udp_server_stats.udp_requests_banned_total(), + udp_banned_ips_total: udp_server_stats.udp_banned_ips_total(), + udp_avg_connect_processing_time_ns: udp_server_stats.udp_avg_connect_processing_time_ns_averaged(), + udp_avg_announce_processing_time_ns: udp_server_stats.udp_avg_announce_processing_time_ns_averaged(), + udp_avg_scrape_processing_time_ns: udp_server_stats.udp_avg_scrape_processing_time_ns_averaged(), + + // UDPv4 + udp4_requests: udp_server_stats.udp4_requests_received_total(), + udp4_connections_handled: udp_server_stats.udp4_connect_requests_accepted_total(), + udp4_announces_handled: udp_server_stats.udp4_announce_requests_accepted_total(), + udp4_scrapes_handled: udp_server_stats.udp4_scrape_requests_accepted_total(), + udp4_responses: udp_server_stats.udp4_responses_sent_total(), + udp4_errors_handled: udp_server_stats.udp4_errors_total(), + + // UDPv6 + udp6_requests: udp_server_stats.udp6_requests_received_total(), + udp6_connections_handled: udp_server_stats.udp6_connect_requests_accepted_total(), + udp6_announces_handled: udp_server_stats.udp6_announce_requests_accepted_total(), + udp6_scrapes_handled: udp_server_stats.udp6_scrape_requests_accepted_total(), + udp6_responses: udp_server_stats.udp6_responses_sent_total(), + udp6_errors_handled: udp_server_stats.udp6_errors_total(), + } + } + + async fn get_labeled_stats(&self) -> LabeledStats { + let swarms_stats = self.swarms_stats_repository.get_metrics().await; + let tracker_core_stats = self.tracker_core_stats_repository.get_metrics().await; + let http_stats = self.http_stats_repository.get_stats().await; + let udp_stats = self.udp_core_stats_repository.get_stats().await; + let udp_server_stats = self.udp_server_stats_repository.get_stats().await; + + let mut metrics = MetricCollection::default(); + + metrics + .merge(&swarms_stats.metric_collection) + .expect("failed to merge torrent repository metrics"); + metrics + .merge(&tracker_core_stats.metric_collection) + .expect("failed to merge tracker core metrics"); + metrics + .merge(&http_stats.metric_collection) + .expect("failed to merge HTTP core metrics"); + metrics + .merge(&udp_stats.metric_collection) + .expect("failed to merge UDP core metrics"); + metrics + .merge(&udp_server_stats.metric_collection) + .expect("failed to merge UDP server metrics"); + + LabeledStats { metrics } + } +} diff --git a/packages/tracker-client/src/http/client/responses/announce.rs b/packages/tracker-client/src/http/client/responses/announce.rs index ecebab7ad..4156f11da 100644 --- a/packages/tracker-client/src/http/client/responses/announce.rs +++ b/packages/tracker-client/src/http/client/responses/announce.rs @@ -116,6 +116,7 @@ impl From for Compact { fn from(compact_announce: DeserializedCompact) -> Self { let mut peers = vec![]; + #[allow(clippy::chunks_exact_to_as_chunks, clippy::explicit_iter_loop)] for peer_bytes in compact_announce.peers.chunks_exact(6) { peers.push(CompactPeer::new_from_bytes(peer_bytes)); } diff --git a/packages/udp-protocol/src/request.rs b/packages/udp-protocol/src/request.rs index 6e84950da..cd6e40993 100644 --- a/packages/udp-protocol/src/request.rs +++ b/packages/udp-protocol/src/request.rs @@ -101,9 +101,9 @@ impl Request { )); } - let chunks = remaining_bytes.chunks_exact(size_of::()); + let (chunks, remainder) = remaining_bytes.as_chunks::<{ size_of::() }>(); - if !chunks.remainder().is_empty() { + if !remainder.is_empty() { return Err(RequestParseError::sendable_text( "Invalid info hash list", connection_id, @@ -111,13 +111,7 @@ impl Request { )); } - let info_hashes = chunks - .map(|chunk| { - let mut bytes = [0u8; 20]; - bytes.copy_from_slice(chunk); - InfoHash(bytes) - }) - .collect::>(); + let info_hashes = chunks.iter().copied().map(InfoHash).collect::>(); let info_hashes = Vec::from(&info_hashes[..(max_scrape_torrents as usize).min(info_hashes.len())]); diff --git a/packages/udp-protocol/src/response.rs b/packages/udp-protocol/src/response.rs index 55b31700f..77110b025 100644 --- a/packages/udp-protocol/src/response.rs +++ b/packages/udp-protocol/src/response.rs @@ -54,15 +54,16 @@ impl Response { .0; let peers = if let Some(bytes) = bytes.get(size_of::()..) { - let chunks = bytes.chunks_exact(size_of::>()); + let (chunks, remainder) = bytes.as_chunks::<{ size_of::>() }>(); - if !chunks.remainder().is_empty() { + if !remainder.is_empty() { return Err(invalid_data()); } chunks + .iter() .map(|chunk| { - ResponsePeer::::read_from_prefix(chunk) + ResponsePeer::::read_from_prefix(chunk.as_slice()) .map(|(peer, _)| peer) .map_err(|_| invalid_data()) }) @@ -79,15 +80,16 @@ impl Response { .0; let peers = if let Some(bytes) = bytes.get(size_of::()..) { - let chunks = bytes.chunks_exact(size_of::>()); + let (chunks, remainder) = bytes.as_chunks::<{ size_of::>() }>(); - if !chunks.remainder().is_empty() { + if !remainder.is_empty() { return Err(invalid_data()); } chunks + .iter() .map(|chunk| { - ResponsePeer::::read_from_prefix(chunk) + ResponsePeer::::read_from_prefix(chunk.as_slice()) .map(|(peer, _)| peer) .map_err(|_| invalid_data()) }) @@ -101,15 +103,16 @@ impl Response { 2 => { let transaction_id = read_i32_ne(&mut bytes).map(TransactionId)?; - let chunks = bytes.chunks_exact(size_of::()); + let (chunks, remainder) = bytes.as_chunks::<{ size_of::() }>(); - if !chunks.remainder().is_empty() { + if !remainder.is_empty() { return Err(invalid_data()); } let torrent_stats = chunks + .iter() .map(|chunk| { - TorrentScrapeStatistics::read_from_prefix(chunk) + TorrentScrapeStatistics::read_from_prefix(chunk.as_slice()) .map(|(stats, _)| stats) .map_err(|_| invalid_data()) }) diff --git a/project-words.txt b/project-words.txt index 06f7840b8..ec32c47d7 100644 --- a/project-words.txt +++ b/project-words.txt @@ -342,6 +342,7 @@ sockfd specialised sqllite sqlx +srcset stabilised subissue Subissue