From ba43e1ae4ed0d52767c60994c5c1bdc7ec714413 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 Jul 2026 08:10:13 +0100 Subject: [PATCH 1/9] docs(issues): define registry migration design --- .../ISSUE.md | 113 ++++++++++++++++-- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md index b040d3170..019386988 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md @@ -7,7 +7,7 @@ github-issue: 2041 spec-path: docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md branch: "2041-migrate-runtime-service-registry-metadata" related-pr: null -last-updated-utc: 2026-07-29 15:10 +last-updated-utc: 2026-07-30 00:00 semantic-links: skill-links: - create-issue @@ -99,22 +99,108 @@ bind-IP classification and fixed registration delay. - #2035 bootstrap phase: every configured HTTP/UDP listener preserves and propagates its canonical configuration-instance identity during startup. +Both prerequisites are merged. #2036 provides tracker-owned `ServiceRole` and +`ConfigurationInstanceId` types. #2035 retains HTTP and UDP startup +containers as ordered `(ConfigurationInstanceId, Container)` pairs. This +issue must propagate the retained identifier rather than reconstructing one +from a bootstrap index. + +## Approved Design + +### Server Library Release + +This issue releases `torrust-server-lib` **0.2.0**. The current `0.1.0` API +publicly exposes `Arc>>` +and its unspecified iteration order. Replacing that raw storage API with +snapshots and queries is breaking, so a `0.1.x` release would not follow +pre-1.0 semantic versioning. All tracker dependency declarations and +`Cargo.lock` must explicitly upgrade to `0.2.0`; a `"0.1.0"` Cargo +requirement does not accept `0.2.0`. + +The standalone library change is deliberately small and application-agnostic: + +1. Make `ServiceRegistration` generic over immutable metadata. It stores the + final `ServiceBinding`, opaque application-owned metadata, and optional + health-check behavior. +2. Make `Registar` and its registration form generic over the same metadata. + Registration returns an acknowledgement only after insertion makes the + registration visible to registry snapshots. +3. Keep registry storage private. Remove the public raw registry alias and + `entries()` API rather than exposing a mutex or `HashMap` iteration as a + contract. +4. Provide cloned, side-effect-free registration snapshots and metadata-based + query support. Returned snapshots have a documented deterministic order by + final `ServiceBinding`; neither hash-map nor task/insertion order is part + of the API contract. +5. Expose optional health-check execution separately from metadata discovery. + A registration without health behavior remains queryable and produces no + health-check task. + +The tracker owns a typed runtime metadata value containing `ServiceRole` and +`ConfigurationInstanceId`. `torrust-server-lib` must not define tracker roles, +configuration identifiers, metrics policy, or tracker-specific metadata keys. + +### Registration and Readiness + +A local service is registry-ready only after it has successfully bound its +listener **and** received the registration-insertion acknowledgement. This is +a per-service boundary, not a new global application lifecycle coordinator. +`AppContainer` and `JobManager` retain their current composition and lifecycle +responsibilities. + +Consumers needing application readiness must wait for the exact configured +canonical identities in registry snapshots, rather than a registry-size +threshold, a startup delay, a log line, or a health check. This accommodates +applications that omit optional services and repeated `0.0.0.0:0` +configuration blocks. + +### Tracker Migration + +- HTTP and HTTPS registrations use `ServiceRole::HttpTracker`; their final + `ServiceBinding` distinguishes HTTP from HTTPS. +- UDP registrations use `ServiceRole::UdpTracker`. +- The REST API registers `ServiceRole::RestApi` with + `ConfigurationInstanceId::new(ServiceRole::RestApi, 0)`. +- The health-check API registers `ServiceRole::HealthCheckApi` with + `ConfigurationInstanceId::new(ServiceRole::HealthCheckApi, 0)` and has no + health-check behavior, preventing recursive self-checking. + +The health-check handler must read stable binding and role fields from the +metadata snapshot, then combine them with optional health-check execution +results. Its JSON contract remains compatible: `service_binding`, `binding`, +and `service_type` retain their established values. The existing HTTP/HTTPS +health-check URL behavior is outside this issue and must not change +incidentally. + +### Related-Issue Compatibility + +- **#2035:** use the configuration identifier retained with each container; + never infer service identity from an address or re-create it from a loop + index. +- **#2036:** use its canonical types directly; do not introduce strings or a + second tracker identity model as the source of truth. +- **#2039:** registry metadata is immutable runtime discovery data only. + Event producers must still carry canonical identity directly, and this issue + does not implement event or metrics-policy behavior. +- **#1419:** replace raw-registry polling, bind-IP classification, and fixed + registration delays with exact role/identity snapshot discovery. + ## Implementation Plan Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. -| ID | Status | Task | Notes / Expected Output | -| --- | ------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| T1 | BLOCKED | Confirm prerequisites | Verify #2036 identity types and #2035 bootstrap propagation are merged and have evidence. | -| T2 | TODO | Define generic registration metadata boundary | Design metadata representation in `torrust-server-lib` that remains independent of tracker-specific role variants. | -| T3 | TODO | Extend registration and query API | Store final binding, opaque metadata, and optional health behavior; add deterministic query API. | -| T4 | TODO | Establish readiness semantics | Acknowledge insertion or provide an equivalent boundary so consumers can reliably discover started services. | -| T5 | TODO | Release and upgrade server library | Publish the compatible standalone crate version and update tracker dependency. | -| T6 | TODO | Migrate tracker registrations | Register canonical role and instance identity for every local service. | -| T7 | TODO | Migrate health reporting | Build reports from registration metadata and execution results while preserving response JSON. | -| T8 | TODO | Migrate #1419 discovery helpers | Replace bind-IP endpoint classification and fixed registration delay with registry queries. | -| T9 | TODO | Add focused tests | Cover metadata, query ordering/selection, readiness, health compatibility, and registration identity. | -| T10 | TODO | Validate and record evidence | Run both-repository checks and manual port-zero scenarios; update evidence and acceptance review. | +| ID | Status | Task | Notes / Expected Output | +| --- | ------ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| T1 | DONE | Confirm prerequisites | #2036 and #2035 are merged; #2035's retained canonical identifiers must flow through registration. | +| T2 | DONE | Define generic registration metadata boundary | Approved: generic immutable metadata, optional health behavior, and tracker-owned typed role/identity metadata. | +| T3 | TODO | Extend registration and query API | Store final binding, opaque metadata, and optional health behavior; hide raw storage and add ordered snapshots. | +| T4 | DONE | Establish readiness semantics | Approved per-service insertion acknowledgement after bind; readiness consumers query exact expected identities. | +| T5 | TODO | Release and upgrade server library | Publish breaking `torrust-server-lib` 0.2.0 and update all tracker dependency declarations and lockfile. | +| T6 | TODO | Migrate tracker registrations | Register canonical role and instance identity for every local service. | +| T7 | TODO | Migrate health reporting | Build reports from registration metadata and execution results while preserving response JSON. | +| T8 | TODO | Migrate #1419 discovery helpers | Replace bind-IP endpoint classification and fixed registration delay with registry queries. | +| T9 | TODO | Add focused tests | Cover metadata, query ordering/selection, readiness, health compatibility, and registration identity. | +| T10 | TODO | Validate and record evidence | Run both-repository checks and manual port-zero scenarios; update evidence and acceptance review. | ## Progressive Verification Protocol @@ -146,6 +232,7 @@ For every code-changing task (T2-T9): - 2026-07-29 14:45 UTC - agent - Drafted by splitting the registry migration from #2036, which now owns canonical identity types only. Awaiting user review. - 2026-07-29 15:10 UTC - agent - User approved the specification; created GitHub issue #2041 and moved this specification to `docs/issues/open/`. +- 2026-07-30 00:00 UTC - user and agent - Confirmed that the standalone server-library release, publication, and tracker upgrade are in scope. Approved generic immutable metadata, per-service insertion-acknowledgement readiness, and a concrete `0.2.0` server-library API plan. Reviewed compatibility with #2035, #2036, #2039, and #1419. ## Acceptance Criteria From a4038530b39a46f2665219a9e009f7d52a117b2b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 Jul 2026 11:41:03 +0100 Subject: [PATCH 2/9] docs(issue-2041): record registry migration plan --- .../ISSUE.md | 35 +++++++----- .../evidence.md | 55 +++++++++++++++---- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md index 019386988..ce4ae902a 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md @@ -136,6 +136,11 @@ The standalone library change is deliberately small and application-agnostic: A registration without health behavior remains queryable and produces no health-check task. +Registrations are immutable records for the process lifetime in this delivery. +Dynamic restart, deregistration, replacement, liveness removal, and +re-registration are intentionally out of scope. The registry rejects duplicate +final bindings so a snapshot never represents two services at one listener. + The tracker owns a typed runtime metadata value containing `ServiceRole` and `ConfigurationInstanceId`. `torrust-server-lib` must not define tracker roles, configuration identifiers, metrics policy, or tracker-specific metadata keys. @@ -189,18 +194,18 @@ incidentally. Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. -| ID | Status | Task | Notes / Expected Output | -| --- | ------ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| T1 | DONE | Confirm prerequisites | #2036 and #2035 are merged; #2035's retained canonical identifiers must flow through registration. | -| T2 | DONE | Define generic registration metadata boundary | Approved: generic immutable metadata, optional health behavior, and tracker-owned typed role/identity metadata. | -| T3 | TODO | Extend registration and query API | Store final binding, opaque metadata, and optional health behavior; hide raw storage and add ordered snapshots. | -| T4 | DONE | Establish readiness semantics | Approved per-service insertion acknowledgement after bind; readiness consumers query exact expected identities. | -| T5 | TODO | Release and upgrade server library | Publish breaking `torrust-server-lib` 0.2.0 and update all tracker dependency declarations and lockfile. | -| T6 | TODO | Migrate tracker registrations | Register canonical role and instance identity for every local service. | -| T7 | TODO | Migrate health reporting | Build reports from registration metadata and execution results while preserving response JSON. | -| T8 | TODO | Migrate #1419 discovery helpers | Replace bind-IP endpoint classification and fixed registration delay with registry queries. | -| T9 | TODO | Add focused tests | Cover metadata, query ordering/selection, readiness, health compatibility, and registration identity. | -| T10 | TODO | Validate and record evidence | Run both-repository checks and manual port-zero scenarios; update evidence and acceptance review. | +| ID | Status | Task | Notes / Expected Output | +| --- | ----------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| T1 | DONE | Confirm prerequisites | #2036 and #2035 are merged; #2035's retained canonical identifiers must flow through registration. | +| T2 | DONE | Define generic registration metadata boundary | Approved: generic immutable metadata, optional health behavior, and tracker-owned typed role/identity metadata. | +| T3 | DONE | Extend registration and query API | `torrust-server-lib` 0.2.0 provides metadata, optional checks, insertion acknowledgement, and ordered snapshots. | +| T4 | DONE | Establish readiness semantics | Approved per-service insertion acknowledgement after bind; readiness consumers query exact expected identities. | +| T5 | DONE | Release and upgrade server library | Published `torrust-server-lib` 0.2.0; all tracker declarations and lockfile resolve the release. | +| T6 | DONE | Migrate tracker registrations | HTTP(S), UDP, REST API, and health-check API register canonical role and instance metadata. | +| T7 | DONE | Migrate health reporting | Health reports combine metadata binding/role with optional check execution and preserve JSON fields. | +| T8 | DONE | Migrate #1419 discovery helpers | Helpers await exact identities and query canonical roles; no bind-IP or map-order classification remains. | +| T9 | DONE | Add focused tests | Added health JSON compatibility and repeated port-zero identity-to-binding regressions. | +| T10 | IN_PROGRESS | Validate and record evidence | Focused compilation, tests, and linters passed; final full quality gate and manual scenarios remain. | ## Progressive Verification Protocol @@ -221,8 +226,8 @@ For every code-changing task (T2-T9): - [x] Spec reviewed and approved by user/maintainer - [x] GitHub issue created: #2041 - [ ] Spec-only PR merged into `develop` before implementation -- [ ] Prerequisites merged -- [ ] Implementation completed +- [x] Prerequisites merged +- [x] Implementation completed - [ ] Automatic verification completed (`linter all`, relevant tests in both repositories) - [ ] Manual verification scenarios executed and recorded - [ ] Acceptance criteria reviewed after implementation @@ -233,6 +238,8 @@ For every code-changing task (T2-T9): - 2026-07-29 14:45 UTC - agent - Drafted by splitting the registry migration from #2036, which now owns canonical identity types only. Awaiting user review. - 2026-07-29 15:10 UTC - agent - User approved the specification; created GitHub issue #2041 and moved this specification to `docs/issues/open/`. - 2026-07-30 00:00 UTC - user and agent - Confirmed that the standalone server-library release, publication, and tracker upgrade are in scope. Approved generic immutable metadata, per-service insertion-acknowledgement readiness, and a concrete `0.2.0` server-library API plan. Reviewed compatibility with #2035, #2036, #2039, and #1419. +- 2026-07-31 UTC - agent - Published `torrust-server-lib` 0.2.0 after a successful `cargo publish --dry-run`; pushed signed release commit `d17fdb1`. +- 2026-07-31 UTC - agent - Migrated tracker registrations and health reporting to typed runtime metadata. Replaced #1419 bind-IP/count-based helper behavior with exact canonical identity readiness and role queries. Focused tests, workspace compilation, and `linter all` passed; final validation and manual evidence remain pending. ## Acceptance Criteria diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md index c5769b528..eed336143 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md @@ -5,16 +5,51 @@ in the registry metadata migration. ## Task Evidence -| Task | Baseline Status | Post-change Status | Evidence | -| --- | --- | --- | --- | -| T2 | TODO | TODO | Generic registration metadata boundary. | -| T3 | TODO | TODO | Registration/query behavior. | -| T4 | TODO | TODO | Registration readiness behavior. | -| T5 | TODO | TODO | Released library integration. | -| T6 | TODO | TODO | Tracker service registrations. | -| T7 | TODO | TODO | Health-report compatibility. | -| T8 | TODO | TODO | #1419 endpoint discovery helpers. | -| T9 | TODO | TODO | Focused test coverage. | +| Task | Baseline Status | Post-change Status | Evidence | +| ---- | --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------ | +| T2 | NOT RECORDED | Automated PASS; manual TODO | API boundary reviewed and approved before implementation. | +| T3 | NOT RECORDED | Automated PASS; manual TODO | Server-lib tests cover acknowledgement, duplicate rejection, metadata snapshots, and deterministic ordering. | +| T4 | NOT RECORDED | Automated PASS; manual TODO | `register().await` is the insertion acknowledgement; integration helpers await exact identities. | +| T5 | NOT RECORDED | Automated PASS; manual TODO | `cargo publish --dry-run` and final publication of `torrust-server-lib` 0.2.0 succeeded. | +| T6 | NOT RECORDED | Automated PASS; manual TODO | Port-zero integration discovers every HTTP/UDP canonical instance identity. | +| T7 | NOT RECORDED | Automated PASS; manual TODO | Health contract tests assert preserved URL, binding, and service-type fields for HTTP, REST API, and UDP. | +| T8 | NOT RECORDED | Automated PASS; manual TODO | Integration helpers query roles/identities instead of raw map entries or bind IPs. | +| T9 | NOT RECORDED | Automated PASS; manual TODO | Focused server, health-contract, repeated-port-zero, and scaffold tests passed. | + +## Automated Local Verification + +The issue's evidence protocol asks for manual baseline and post-change probes +before each edit. This work started before those baselines were recorded, so no +manual baseline or manual post-change result is claimed retroactively. The +following are reproducible **automated** post-change checks only. M1-M3 remain +mandatory manual scenarios before this issue can be accepted. + +### T3-T5 - Generic registry API and released crate + +- Baseline: Not recorded before implementation. +- Post-change revision: `torrust-server-lib` commit `d17fdb1`. +- Commands: `cargo publish --dry-run`, `cargo publish`, `cargo machete --with-metadata`, `linter all`, and `cargo test --doc --workspace`. +- Observed result: dry-run packaged and verified 18 files; `torrust-server-lib` 0.2.0 published to crates.io. Dependency, lint, and doc-test checks passed. +- Comparison: The released API replaces raw map access with metadata snapshots and acknowledged insertion. +- Result: `DONE`. + +### T6-T8 - Runtime identities, health report, and integration discovery + +- Baseline: Not recorded before implementation. +- Post-change revision: tracker branch `2041-migrate-runtime-service-registry-metadata`. +- Commands: `cargo test -p torrust-tracker-axum-health-check-api-server --test integration` and `cargo test --test aggregate_stats_port_zero --test scaffold`. +- Observed result: all seven health-contract tests passed; repeated port-zero HTTP/UDP blocks registered distinct non-zero final bindings for exact canonical identities; scaffold and port-zero integration scenarios passed. +- Comparison: Helper behavior now waits for exact canonical identities and finds endpoints by role, rather than registry size, map ordering, or bind-IP conventions. +- Result: `DONE`. + +### T9 - Focused regression coverage + +- Baseline: Not recorded before implementation. +- Post-change revision: tracker branch `2041-migrate-runtime-service-registry-metadata`. +- Commands: `cargo check --workspace --all-targets`; focused server package tests; `cargo test --test aggregate_stats_fixed_ports --test aggregate_stats_port_zero --test scaffold`; and `linter all`. +- Observed result: all invoked checks passed. Health JSON tests assert `service_binding`, `binding`, and `service_type`; port-zero coverage asserts exact identity-to-final-binding correlation. +- Comparison: Regression coverage now protects the metadata and readiness contracts introduced by this issue. +- Result: `DONE`. ## Scenario Record Template From 28b60a7882b1a78aead7c09f6cadc77065a35ede Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 Jul 2026 11:42:45 +0100 Subject: [PATCH 3/9] feat(registry): migrate runtime service metadata --- Cargo.lock | 53 +++---- Cargo.toml | 2 +- .../axum-health-check-api-server/Cargo.toml | 3 +- .../src/environment.rs | 11 +- .../src/handlers.rs | 46 +++--- .../src/server.rs | 9 +- .../tests/server/contract.rs | 24 ++++ packages/axum-http-server/Cargo.toml | 2 +- packages/axum-http-server/src/server.rs | 27 ++-- .../src/testing/environment.rs | 13 +- packages/axum-rest-api-server/Cargo.toml | 2 +- packages/axum-rest-api-server/src/server.rs | 22 ++- .../src/testing/environment.rs | 5 +- packages/axum-server/Cargo.toml | 2 +- packages/primitives/src/lib.rs | 2 + .../src/runtime_service_metadata.rs | 34 +++++ packages/udp-server/Cargo.toml | 2 +- packages/udp-server/src/server/launcher.rs | 8 +- packages/udp-server/src/server/mod.rs | 13 +- packages/udp-server/src/server/states.rs | 9 +- .../udp-server/src/testing/environment.rs | 7 +- src/app.rs | 10 +- src/bootstrap/jobs/health_check_api.rs | 27 +++- src/bootstrap/jobs/http_tracker.rs | 26 +++- src/bootstrap/jobs/tracker_apis.rs | 25 +++- src/bootstrap/jobs/udp_tracker.rs | 5 +- src/container.rs | 13 +- tests/aggregate_stats_port_zero.rs | 21 +++ tests/common/mod.rs | 3 +- tests/common/workspace.rs | 131 ++++++++++-------- tests/scaffold.rs | 12 +- 31 files changed, 374 insertions(+), 195 deletions(-) create mode 100644 packages/primitives/src/runtime_service_metadata.rs diff --git a/Cargo.lock b/Cargo.lock index b87d50f73..9d6fbd57e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1349,7 +1349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2151,7 +2151,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3166,7 +3166,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3403,7 +3403,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower", - "tower-http 0.6.11", + "tower-http", "tower-service", "url", "wasm-bindgen", @@ -3516,7 +3516,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3575,7 +3575,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4331,10 +4331,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4787,14 +4787,14 @@ dependencies = [ [[package]] name = "torrust-server-lib" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c280c9aae89accb118b94c31bfbdb523668bc4a5ee213ecefb745f6fb8ac235b" +checksum = "5baa16bd7eb33812e7da8bf063f05e104b4749a3f6fe4eb69fbf660e4f1974fe" dependencies = [ "derive_more 2.1.1", "tokio", "torrust-net-primitives", - "tower-http 0.7.0", + "tower-http", "tracing", ] @@ -4863,9 +4863,10 @@ dependencies = [ "torrust-tracker-axum-rest-api-server", "torrust-tracker-axum-server", "torrust-tracker-configuration", + "torrust-tracker-primitives", "torrust-tracker-test-helpers", "torrust-tracker-udp-server", - "tower-http 0.7.0", + "tower-http", "tracing", "url", ] @@ -4904,7 +4905,7 @@ dependencies = [ "torrust-tracker-swarm-coordination-registry", "torrust-tracker-test-helpers", "tower", - "tower-http 0.7.0", + "tower-http", "tracing", "uuid", ] @@ -4943,7 +4944,7 @@ dependencies = [ "torrust-tracker-udp-core", "torrust-tracker-udp-server", "tower", - "tower-http 0.7.0", + "tower-http", "tracing", "url", "uuid", @@ -5370,38 +5371,22 @@ name = "tower-http" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-http" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" dependencies = [ "async-compression", "bitflags", "bytes", "futures-core", + "futures-util", "http", "http-body", - "percent-encoding", "pin-project-lite", "tokio", "tokio-util", + "tower", "tower-layer", "tower-service", "tracing", + "url", "uuid", ] @@ -5809,7 +5794,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ada409b88..327ccb4ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ torrust-tracker-axum-server = { version = "0.1.0", path = "packages/axum-server" torrust-tracker-rest-api-client = { version = "0.1.0", path = "packages/rest-api-client" } torrust-tracker-rest-api-runtime-adapter = { version = "0.1.0", path = "packages/rest-api-runtime-adapter" } torrust-tracker-rest-api-protocol = { version = "0.1.0", path = "packages/rest-api-protocol" } -torrust-server-lib = "0.1.0" +torrust-server-lib = "0.2.0" torrust-clock = "3.0.0" torrust-tracker-configuration = { version = "3.0.0", path = "packages/configuration" } torrust-tracker-primitives = { version = "3.0.0", path = "packages/primitives" } diff --git a/packages/axum-health-check-api-server/Cargo.toml b/packages/axum-health-check-api-server/Cargo.toml index 16a6de97a..692eff889 100644 --- a/packages/axum-health-check-api-server/Cargo.toml +++ b/packages/axum-health-check-api-server/Cargo.toml @@ -22,7 +22,8 @@ serde = { version = "1", features = [ "derive" ] } serde_json = { version = "1", features = [ "preserve_order" ] } tokio = { version = "1", features = [ "macros", "net", "rt-multi-thread", "signal", "sync" ] } torrust-tracker-axum-server = { version = "0.1.0", path = "../axum-server" } -torrust-server-lib = "0.1.0" +torrust-server-lib = "0.2.0" +torrust-tracker-primitives = { version = "3.0.0", path = "../primitives" } torrust-tracker-configuration = { version = "3.0.0", path = "../configuration" } torrust-net-primitives = "0.1.0" tower-http = { version = "0", features = [ "compression-full", "cors", "propagate-header", "request-id", "trace" ] } diff --git a/packages/axum-health-check-api-server/src/environment.rs b/packages/axum-health-check-api-server/src/environment.rs index 69c9073ae..ac54c3188 100644 --- a/packages/axum-health-check-api-server/src/environment.rs +++ b/packages/axum-health-check-api-server/src/environment.rs @@ -6,6 +6,7 @@ use tokio::task::JoinHandle; use torrust_server_lib::registar::Registar; use torrust_server_lib::signals::{self, Halted as SignalHalted, Started as SignalStarted}; use torrust_tracker_configuration::HealthCheckApi; +use torrust_tracker_primitives::RuntimeServiceMetadata; use crate::{HEALTH_CHECK_API_LOG_TARGET, server}; @@ -28,13 +29,13 @@ pub struct Stopped { } pub struct Environment { - pub registar: Registar, + pub registar: Registar, pub state: S, } impl Environment { #[must_use] - pub fn new(config: &Arc, registar: Registar) -> Self { + pub fn new(config: &Arc, registar: Registar) -> Self { let bind_to = config.bind_address; Self { @@ -53,14 +54,14 @@ impl Environment { let (tx_start, rx_start) = oneshot::channel::(); let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); - let register = self.registar.entries(); + let registar = self.registar.clone(); tracing::debug!(target: HEALTH_CHECK_API_LOG_TARGET, "Spawning task to launch the service ..."); let server = tokio::spawn(async move { tracing::debug!(target: HEALTH_CHECK_API_LOG_TARGET, "Starting the server in a spawned task ..."); - server::start(self.state.bind_to, tx_start, rx_halt, register) + server::start(self.state.bind_to, tx_start, rx_halt, registar) .await .expect("it should start the health check service"); @@ -85,7 +86,7 @@ impl Environment { } impl Environment { - pub async fn new(config: &Arc, registar: Registar) -> Self { + pub async fn new(config: &Arc, registar: Registar) -> Self { Environment::::new(config, registar).start().await } diff --git a/packages/axum-health-check-api-server/src/handlers.rs b/packages/axum-health-check-api-server/src/handlers.rs index 3b4a02475..c99ced9e3 100644 --- a/packages/axum-health-check-api-server/src/handlers.rs +++ b/packages/axum-health-check-api-server/src/handlers.rs @@ -1,8 +1,7 @@ -use std::collections::VecDeque; - use axum::Json; use axum::extract::State; -use torrust_server_lib::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistry}; +use torrust_server_lib::registar::Registar; +use torrust_tracker_primitives::RuntimeServiceMetadata; use tracing::{Level, instrument}; use super::resources::{CheckReport, Report}; @@ -12,30 +11,39 @@ use super::responses; /// /// Creates a vector [`CheckReport`] from the input set of [`CheckJob`], and then builds a report from the results. /// -#[instrument(skip(register), ret(level = Level::DEBUG))] -pub(crate) async fn health_check_handler(State(register): State) -> Json { - #[allow(unused_assignments)] - let mut checks: VecDeque = VecDeque::new(); - - { - let mutex = register.lock(); - - checks = mutex.await.values().map(ServiceRegistration::spawn_check).collect(); - } +#[instrument(skip(registar), ret(level = Level::DEBUG))] +pub(crate) async fn health_check_handler(State(registar): State>) -> Json { + let mut checks: Vec<_> = registar + .services() + .await + .into_iter() + .filter_map(|service| { + service.spawn_check().map(|health_check| { + ( + service.service_binding().clone(), + service.metadata().service_role().as_str().to_string(), + health_check, + ) + }) + }) + .collect(); // if we do not have any checks, lets return a `none` result. if checks.is_empty() { return responses::none(); } - let jobs = checks.drain(..).map(|c| { + let jobs = checks.drain(..).map(|(service_binding, service_type, health_check)| { tokio::spawn(async move { CheckReport { - service_binding: c.service_binding.url(), - binding: c.service_binding.bind_address(), - info: c.info.clone(), - service_type: c.service_type, - result: c.job.await.expect("it should be able to join into the checking function"), + service_binding: service_binding.url(), + binding: service_binding.bind_address(), + info: health_check.info, + service_type, + result: health_check + .job + .await + .expect("it should be able to join into the checking function"), } }) }); diff --git a/packages/axum-health-check-api-server/src/server.rs b/packages/axum-health-check-api-server/src/server.rs index 47a1a2710..77dc0e5b3 100644 --- a/packages/axum-health-check-api-server/src/server.rs +++ b/packages/axum-health-check-api-server/src/server.rs @@ -16,9 +16,10 @@ use serde_json::json; use tokio::sync::oneshot::{Receiver, Sender}; use torrust_net_primitives::service_binding::{Protocol, ServiceBinding}; use torrust_server_lib::logging::Latency; -use torrust_server_lib::registar::ServiceRegistry; +use torrust_server_lib::registar::Registar; use torrust_server_lib::signals::{Halted, Started}; use torrust_tracker_axum_server::signals::graceful_shutdown; +use torrust_tracker_primitives::RuntimeServiceMetadata; use tower_http::LatencyUnit; use tower_http::classify::ServerErrorsFailureClass; use tower_http::compression::CompressionLayer; @@ -35,17 +36,17 @@ use crate::handlers::health_check_handler; /// # Panics /// /// Will panic if binding to the socket address fails. -#[instrument(skip(bind_to, tx, rx_halt, register))] +#[instrument(skip(bind_to, tx, rx_halt, registar))] pub fn start( bind_to: SocketAddr, tx: Sender, rx_halt: Receiver, - register: ServiceRegistry, + registar: Registar, ) -> impl Future> { let router = Router::new() .route("/", get(|| async { Json(json!({})) })) .route("/health_check", get(health_check_handler)) - .with_state(register) + .with_state(registar) .layer(CompressionLayer::new()) .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) .layer(PropagateHeaderLayer::new(HeaderName::from_static("x-request-id"))) diff --git a/packages/axum-health-check-api-server/tests/server/contract.rs b/packages/axum-health-check-api-server/tests/server/contract.rs index 483108252..e314d4b5b 100644 --- a/packages/axum-health-check-api-server/tests/server/contract.rs +++ b/packages/axum-health-check-api-server/tests/server/contract.rs @@ -34,6 +34,7 @@ mod api { use torrust_tracker_axum_health_check_api_server::environment::Started; use torrust_tracker_axum_health_check_api_server::resources::{Report, Status}; use torrust_tracker_test_helpers::{configuration, logging}; + use url::Url; use crate::server::client::get; @@ -66,7 +67,12 @@ mod api { let details = report.details.first().expect("it should have some details"); + assert_eq!( + details.service_binding, + Url::parse(&format!("http://{}", service.bind_address())).unwrap() + ); assert_eq!(details.binding, service.bind_address()); + assert_eq!(details.service_type, "tracker_rest_api"); assert_eq!(details.result, Ok("200 OK".to_string())); @@ -117,7 +123,9 @@ mod api { let details = report.details.first().expect("it should have some details"); + assert_eq!(details.service_binding, Url::parse(&format!("http://{binding}")).unwrap()); assert_eq!(details.binding, binding); + assert_eq!(details.service_type, "tracker_rest_api"); assert!( details.result.as_ref().is_err_and(|e| e.contains("error sending request")), "Expected to contain, \"error sending request\", but have message \"{:?}\".", @@ -139,6 +147,7 @@ mod http { use torrust_tracker_axum_health_check_api_server::environment::Started; use torrust_tracker_axum_health_check_api_server::resources::{Report, Status}; use torrust_tracker_test_helpers::{configuration, logging}; + use url::Url; use crate::server::client::get; @@ -174,7 +183,12 @@ mod http { let details = report.details.first().expect("it should have some details"); + assert_eq!( + details.service_binding, + Url::parse(&format!("http://{}", service.bind_address())).unwrap() + ); assert_eq!(details.binding, *service.bind_address()); + assert_eq!(details.service_type, "http_tracker"); assert_eq!(details.result, Ok("200 OK".to_string())); assert_eq!( @@ -230,7 +244,9 @@ mod http { let details = report.details.first().expect("it should have some details"); + assert_eq!(details.service_binding, Url::parse(&format!("http://{binding}")).unwrap()); assert_eq!(details.binding, binding); + assert_eq!(details.service_type, "http_tracker"); assert!( details.result.as_ref().is_err_and(|e| e.contains("error sending request")), "Expected to contain, \"error sending request\", but have message \"{:?}\".", @@ -252,6 +268,7 @@ mod udp { use torrust_tracker_axum_health_check_api_server::environment::Started; use torrust_tracker_axum_health_check_api_server::resources::{Report, Status}; use torrust_tracker_test_helpers::{configuration, logging}; + use url::Url; use crate::server::client::get; @@ -286,7 +303,12 @@ mod udp { let details = report.details.first().expect("it should have some details"); + assert_eq!( + details.service_binding, + Url::parse(&format!("udp://{}", service.bind_address())).unwrap() + ); assert_eq!(details.binding, service.bind_address()); + assert_eq!(details.service_type, "udp_tracker"); assert_eq!(details.result, Ok("Connected".to_string())); assert_eq!( @@ -335,7 +357,9 @@ mod udp { let details = report.details.first().expect("it should have some details"); + assert_eq!(details.service_binding, Url::parse(&format!("udp://{binding}")).unwrap()); assert_eq!(details.binding, binding); + assert_eq!(details.service_type, "udp_tracker"); assert_eq!(details.result, Err("Timed Out".to_string())); assert_eq!(details.info, format!("checking the udp tracker health check at: {binding}")); diff --git a/packages/axum-http-server/Cargo.toml b/packages/axum-http-server/Cargo.toml index 0890ca151..cc5295325 100644 --- a/packages/axum-http-server/Cargo.toml +++ b/packages/axum-http-server/Cargo.toml @@ -29,7 +29,7 @@ serde = { version = "1", features = [ "derive" ] } tokio = { version = "1", features = [ "macros", "net", "rt-multi-thread", "signal", "sync" ] } tokio-util = "0.7.15" torrust-tracker-axum-server = { version = "0.1.0", path = "../axum-server" } -torrust-server-lib = "0.1.0" +torrust-server-lib = "0.2.0" torrust-clock = "3.0.0" torrust-tracker-configuration = { version = "3.0.0", path = "../configuration" } torrust-net-primitives = "0.1.0" diff --git a/packages/axum-http-server/src/server.rs b/packages/axum-http-server/src/server.rs index aadc458a3..ae6f544f7 100644 --- a/packages/axum-http-server/src/server.rs +++ b/packages/axum-http-server/src/server.rs @@ -15,7 +15,7 @@ use torrust_server_lib::signals::{Halted, Started}; use torrust_tracker_axum_server::custom_axum_server::{self, TimeoutAcceptor}; use torrust_tracker_axum_server::signals::graceful_shutdown; use torrust_tracker_http_core::container::HttpTrackerCoreContainer; -use torrust_tracker_primitives::ServiceRole; +use torrust_tracker_primitives::RuntimeServiceMetadata; use tracing::instrument; use super::v1::routes::router; @@ -208,7 +208,8 @@ impl HttpServer { pub async fn start( self, http_tracker_container: Arc, - form: ServiceRegistrationForm, + form: ServiceRegistrationForm, + metadata: RuntimeServiceMetadata, ) -> Result, Error> { let (tx_start, rx_start) = tokio::sync::oneshot::channel::(); let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); @@ -228,8 +229,9 @@ impl HttpServer { let listen_url = started.service_binding; let binding = started.address; - form.send(ServiceRegistration::new(listen_url, check_fn)) - .expect("it should be able to send service registration"); + form.register(ServiceRegistration::new(listen_url, metadata, Some(check_fn))) + .await + .expect("it should be able to register the started service"); Ok(HttpServer { state: Running { @@ -281,12 +283,7 @@ pub fn check_fn(service_binding: &ServiceBinding) -> ServiceHealthCheckJob { } }); - ServiceHealthCheckJob::new( - service_binding.clone(), - info, - ServiceRole::HttpTracker.as_str().to_string(), - job, - ) + ServiceHealthCheckJob::new(info, job) } #[cfg(test)] @@ -305,6 +302,7 @@ mod tests { use torrust_tracker_http_core::services::scrape::ScrapeService; use torrust_tracker_http_core::statistics::event::listener::run_event_listener; use torrust_tracker_http_core::statistics::repository::Repository; + use torrust_tracker_primitives::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole}; use torrust_tracker_swarm_coordination_registry::container::SwarmCoordinationRegistryContainer; use torrust_tracker_test_helpers::configuration::ephemeral_public; @@ -407,7 +405,14 @@ mod tests { let stopped = HttpServer::new(Launcher::new(bind_to, tls, http_tracker_config.ipv6_v6only)); let started = stopped - .start(http_tracker_container, register.give_form()) + .start( + http_tracker_container, + register.give_form(), + RuntimeServiceMetadata::new( + ServiceRole::HttpTracker, + ConfigurationInstanceId::new(ServiceRole::HttpTracker, 0), + ), + ) .await .expect("it should start the server"); let stopped = started.stop().await.expect("it should stop the server"); diff --git a/packages/axum-http-server/src/testing/environment.rs b/packages/axum-http-server/src/testing/environment.rs index 5affa12a8..2f90337b3 100644 --- a/packages/axum-http-server/src/testing/environment.rs +++ b/packages/axum-http-server/src/testing/environment.rs @@ -9,7 +9,7 @@ use torrust_tracker_configuration::{Core, HttpTracker}; use torrust_tracker_core::container::TrackerCoreContainer; use torrust_tracker_http_core::container::HttpTrackerCoreContainer; use torrust_tracker_http_core::statistics::event::listener::run_event_listener; -use torrust_tracker_primitives::peer; +use torrust_tracker_primitives::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole, peer}; use torrust_tracker_swarm_coordination_registry::container::SwarmCoordinationRegistryContainer; use crate::server::{HttpServer, Launcher, Running, Stopped}; @@ -18,7 +18,7 @@ pub type Started = Environment; pub struct Environment { pub container: Arc, - pub registar: Registar, + pub registar: Registar, pub server: HttpServer, pub event_listener_job: Option>, pub cancellation_token: CancellationToken, @@ -86,7 +86,14 @@ impl Environment { // Start the server let server = self .server - .start(self.container.http_tracker_core_container.clone(), self.registar.give_form()) + .start( + self.container.http_tracker_core_container.clone(), + self.registar.give_form(), + RuntimeServiceMetadata::new( + ServiceRole::HttpTracker, + ConfigurationInstanceId::new(ServiceRole::HttpTracker, 0), + ), + ) .await .expect("Failed to start the HTTP tracker server"); diff --git a/packages/axum-rest-api-server/Cargo.toml b/packages/axum-rest-api-server/Cargo.toml index 6fd497b0f..0c2d6c304 100644 --- a/packages/axum-rest-api-server/Cargo.toml +++ b/packages/axum-rest-api-server/Cargo.toml @@ -34,7 +34,7 @@ torrust-tracker-rest-api-client = { version = "0.1.0", path = "../rest-api-clien torrust-tracker-rest-api-application = { version = "0.1.0", path = "../rest-api-application" } torrust-tracker-rest-api-protocol = { version = "0.1.0", path = "../rest-api-protocol" } torrust-tracker-rest-api-runtime-adapter = { version = "0.1.0", path = "../rest-api-runtime-adapter" } -torrust-server-lib = "0.1.0" +torrust-server-lib = "0.2.0" torrust-clock = "3.0.0" torrust-tracker-configuration = { version = "3.0.0", path = "../configuration" } torrust-metrics = "0.1.0" diff --git a/packages/axum-rest-api-server/src/server.rs b/packages/axum-rest-api-server/src/server.rs index 6a6d498b4..133df5acb 100644 --- a/packages/axum-rest-api-server/src/server.rs +++ b/packages/axum-rest-api-server/src/server.rs @@ -40,7 +40,7 @@ use torrust_server_lib::signals::{Halted, Started}; use torrust_tracker_axum_server::custom_axum_server::{self, TimeoutAcceptor}; use torrust_tracker_axum_server::signals::graceful_shutdown; use torrust_tracker_configuration::AccessTokens; -use torrust_tracker_primitives::ServiceRole; +use torrust_tracker_primitives::RuntimeServiceMetadata; use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use tracing::{Level, instrument}; @@ -128,7 +128,8 @@ impl ApiServer { pub async fn start( self, http_api_container: Arc, - form: ServiceRegistrationForm, + form: ServiceRegistrationForm, + metadata: RuntimeServiceMetadata, access_tokens: Arc, ) -> Result, Error> { let (tx_start, rx_start) = tokio::sync::oneshot::channel::(); @@ -148,8 +149,9 @@ impl ApiServer { let api_server = match rx_start.await { Ok(started) => { - form.send(ServiceRegistration::new(started.service_binding, check_fn)) - .expect("it should be able to send service registration"); + form.register(ServiceRegistration::new(started.service_binding, metadata, Some(check_fn))) + .await + .expect("it should be able to register the started service"); ApiServer { state: Running::new(started.address, tx_halt, task), @@ -206,7 +208,7 @@ pub fn check_fn(service_binding: &ServiceBinding) -> ServiceHealthCheckJob { Err(err) => Err(err.to_string()), } }); - ServiceHealthCheckJob::new(service_binding.clone(), info, ServiceRole::RestApi.as_str().to_string(), job) + ServiceHealthCheckJob::new(info, job) } /// A struct responsible for starting the API server. @@ -309,6 +311,7 @@ mod tests { use torrust_server_lib::registar::Registar; use torrust_tracker_axum_server::tls::make_rust_tls; use torrust_tracker_configuration::{Configuration, logging}; + use torrust_tracker_primitives::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole}; use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use torrust_tracker_test_helpers::configuration::ephemeral_public; @@ -348,14 +351,19 @@ mod tests { let stopped = ApiServer::new(Launcher::new(bind_to, tls)); - let register = &Registar::default(); + let register = &Registar::::default(); let http_api_container = TrackerHttpApiCoreContainer::initialize(&core_config, &http_tracker_config, &udp_tracker_config, &http_api_config) .await; let started = stopped - .start(http_api_container, register.give_form(), access_tokens) + .start( + http_api_container, + register.give_form(), + RuntimeServiceMetadata::new(ServiceRole::RestApi, ConfigurationInstanceId::new(ServiceRole::RestApi, 0)), + access_tokens, + ) .await .expect("it should start the server"); let stopped = started.stop().await.expect("it should stop the server"); diff --git a/packages/axum-rest-api-server/src/testing/environment.rs b/packages/axum-rest-api-server/src/testing/environment.rs index 4322d9399..86a5ac314 100644 --- a/packages/axum-rest-api-server/src/testing/environment.rs +++ b/packages/axum-rest-api-server/src/testing/environment.rs @@ -7,7 +7,7 @@ use torrust_tracker_axum_server::tls::make_rust_tls; use torrust_tracker_configuration::{Configuration, logging}; use torrust_tracker_core::container::TrackerCoreContainer; use torrust_tracker_http_core::container::HttpTrackerCoreContainer; -use torrust_tracker_primitives::peer; +use torrust_tracker_primitives::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole, peer}; use torrust_tracker_rest_api_client::connection_info::{ConnectionInfo, Origin}; use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use torrust_tracker_swarm_coordination_registry::container::SwarmCoordinationRegistryContainer; @@ -23,7 +23,7 @@ where S: std::fmt::Debug + std::fmt::Display, { pub container: Arc, - pub registar: Registar, + pub registar: Registar, pub server: ApiServer, } @@ -89,6 +89,7 @@ impl Environment { .start( self.container.tracker_http_api_core_container.clone(), self.registar.give_form(), + RuntimeServiceMetadata::new(ServiceRole::RestApi, ConfigurationInstanceId::new(ServiceRole::RestApi, 0)), access_tokens, ) .await diff --git a/packages/axum-server/Cargo.toml b/packages/axum-server/Cargo.toml index 7f849185c..a5519213d 100644 --- a/packages/axum-server/Cargo.toml +++ b/packages/axum-server/Cargo.toml @@ -23,7 +23,7 @@ hyper-util = { version = "0", features = [ "http1", "http2", "tokio" ] } pin-project-lite = "0" thiserror = "2" tokio = { version = "1", features = [ "macros", "net", "rt-multi-thread", "signal", "sync" ] } -torrust-server-lib = "0.1.0" +torrust-server-lib = "0.2.0" torrust-tracker-configuration = { version = "3.0.0", path = "../configuration" } torrust-located-error = "3.0.0" tower = { version = "0", features = [ "timeout" ] } diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index f24f7462c..51f183721 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -18,6 +18,7 @@ pub mod peer; )] pub mod peer_id; pub mod policy; +pub mod runtime_service_metadata; pub mod scrape; pub mod service_role; pub mod swarm_metadata; @@ -30,6 +31,7 @@ pub use driver::Driver; pub use mode::PrivateMode; pub use number_of_bytes::NumberOfBytes; pub use policy::TrackerPolicy; +pub use runtime_service_metadata::RuntimeServiceMetadata; pub use scrape::ScrapeData; pub use service_role::ServiceRole; /// Duration since the Unix Epoch. diff --git a/packages/primitives/src/runtime_service_metadata.rs b/packages/primitives/src/runtime_service_metadata.rs new file mode 100644 index 000000000..73da1ff1c --- /dev/null +++ b/packages/primitives/src/runtime_service_metadata.rs @@ -0,0 +1,34 @@ +use crate::{ConfigurationInstanceId, ServiceRole}; + +/// Immutable tracker metadata attached to a started local service registration. +/// +/// The registry owns neither the role nor the configuration identity; it stores +/// this tracker-owned value without assigning it application semantics. +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone, Copy)] +pub struct RuntimeServiceMetadata { + service_role: ServiceRole, + configuration_instance_id: ConfigurationInstanceId, +} + +impl RuntimeServiceMetadata { + /// Creates metadata for a canonical tracker service instance. + #[must_use] + pub const fn new(service_role: ServiceRole, configuration_instance_id: ConfigurationInstanceId) -> Self { + Self { + service_role, + configuration_instance_id, + } + } + + /// Returns the role implemented by the started listener. + #[must_use] + pub const fn service_role(self) -> ServiceRole { + self.service_role + } + + /// Returns the source configuration instance for the listener. + #[must_use] + pub const fn configuration_instance_id(self) -> ConfigurationInstanceId { + self.configuration_instance_id + } +} diff --git a/packages/udp-server/Cargo.toml b/packages/udp-server/Cargo.toml index 00b615b61..62746734f 100644 --- a/packages/udp-server/Cargo.toml +++ b/packages/udp-server/Cargo.toml @@ -28,7 +28,7 @@ serde = "1.0.219" thiserror = "2" tokio = { version = "1", features = [ "macros", "net", "rt-multi-thread", "signal", "sync" ] } tokio-util = "0.7.15" -torrust-server-lib = "0.1.0" +torrust-server-lib = "0.2.0" torrust-clock = "3.0.0" torrust-tracker-configuration = { version = "3.0.0", path = "../configuration" } torrust-tracker-events = { version = "0.1.0", path = "../events" } diff --git a/packages/udp-server/src/server/launcher.rs b/packages/udp-server/src/server/launcher.rs index 8c3573a93..2cbe8bd1d 100644 --- a/packages/udp-server/src/server/launcher.rs +++ b/packages/udp-server/src/server/launcher.rs @@ -11,7 +11,6 @@ use torrust_server_lib::logging::STARTED_ON; use torrust_server_lib::registar::ServiceHealthCheckJob; use torrust_server_lib::signals::{Halted, Started, shutdown_signal_with_message}; use torrust_tracker_client::udp::client::check; -use torrust_tracker_primitives::ServiceRole; use torrust_tracker_udp_core::container::UdpTrackerCoreContainer; use torrust_tracker_udp_core::event::ConnectionContext; use torrust_tracker_udp_core::{self, ConnectionIdValidationPolicy, UDP_TRACKER_LOG_TARGET}; @@ -132,12 +131,7 @@ impl Launcher { let job = tokio::spawn(async move { check(&service_binding_clone).await }); - ServiceHealthCheckJob::new( - service_binding.clone(), - info, - ServiceRole::UdpTracker.as_str().to_string(), - job, - ) + ServiceHealthCheckJob::new(info, job) } // issue-spec: docs/issues/drafts/simplify-udp-server-main-loop.md diff --git a/packages/udp-server/src/server/mod.rs b/packages/udp-server/src/server/mod.rs index 2d49713b9..5ae9c50de 100644 --- a/packages/udp-server/src/server/mod.rs +++ b/packages/udp-server/src/server/mod.rs @@ -57,6 +57,7 @@ mod tests { use torrust_server_lib::registar::Registar; use torrust_tracker_configuration::{Configuration, logging}; + use torrust_tracker_primitives::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole}; use torrust_tracker_test_helpers::configuration::ephemeral_public; use torrust_tracker_udp_core::container::UdpTrackerCoreContainer; @@ -92,7 +93,7 @@ mod tests { let udp_trackers = cfg.udp_trackers.clone().expect("missing UDP trackers configuration"); let config = &udp_trackers[0]; let bind_to = config.bind_address; - let register = &Registar::default(); + let register = &Registar::::default(); let stopped = Server::new(Spawner::new(bind_to)); @@ -104,6 +105,10 @@ mod tests { udp_tracker_core_container, udp_tracker_server_container, register.give_form(), + RuntimeServiceMetadata::new( + ServiceRole::UdpTracker, + ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0), + ), config.cookie_lifetime, torrust_tracker_udp_core::ConnectionIdValidationPolicy::Strict, ) @@ -133,7 +138,7 @@ mod tests { initialize_global_services(&cfg); let bind_to = udp_tracker_config.bind_address; - let register = &Registar::default(); + let register = &Registar::::default(); let stopped = Server::new(Spawner::new(bind_to)); @@ -145,6 +150,10 @@ mod tests { udp_tracker_core_container, udp_tracker_server_container, register.give_form(), + RuntimeServiceMetadata::new( + ServiceRole::UdpTracker, + ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0), + ), udp_tracker_config.cookie_lifetime, torrust_tracker_udp_core::ConnectionIdValidationPolicy::Strict, ) diff --git a/packages/udp-server/src/server/states.rs b/packages/udp-server/src/server/states.rs index 8cd435e49..2b39c41b1 100644 --- a/packages/udp-server/src/server/states.rs +++ b/packages/udp-server/src/server/states.rs @@ -8,6 +8,7 @@ use derive_more::derive::Display; use tokio::task::JoinHandle; use torrust_server_lib::registar::{ServiceRegistration, ServiceRegistrationForm}; use torrust_server_lib::signals::{Halted, Started}; +use torrust_tracker_primitives::RuntimeServiceMetadata; use torrust_tracker_udp_core::container::UdpTrackerCoreContainer; use torrust_tracker_udp_core::{ConnectionIdValidationPolicy, UDP_TRACKER_LOG_TARGET}; use tracing::{Level, instrument}; @@ -66,7 +67,8 @@ impl Server { self, udp_tracker_core_container: Arc, udp_tracker_server_container: Arc, - form: ServiceRegistrationForm, + form: ServiceRegistrationForm, + metadata: RuntimeServiceMetadata, cookie_lifetime: Duration, connection_id_validation: ConnectionIdValidationPolicy, ) -> Result, std::io::Error> { @@ -90,8 +92,9 @@ impl Server { let service_binding = started.service_binding; let local_addr = started.address; - form.send(ServiceRegistration::new(service_binding, Launcher::check)) - .expect("it should be able to send service registration"); + form.register(ServiceRegistration::new(service_binding, metadata, Some(Launcher::check))) + .await + .expect("it should be able to register the started service"); let running_udp_server: Server = Server { state: Running { diff --git a/packages/udp-server/src/testing/environment.rs b/packages/udp-server/src/testing/environment.rs index c7dd42e24..880313da1 100644 --- a/packages/udp-server/src/testing/environment.rs +++ b/packages/udp-server/src/testing/environment.rs @@ -7,6 +7,7 @@ use tokio_util::sync::CancellationToken; use torrust_server_lib::registar::Registar; use torrust_tracker_configuration::{Core, UdpTracker}; use torrust_tracker_core::container::TrackerCoreContainer; +use torrust_tracker_primitives::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole}; use torrust_tracker_swarm_coordination_registry::container::SwarmCoordinationRegistryContainer; use torrust_tracker_udp_core::ConnectionIdValidationPolicy; use torrust_tracker_udp_core::container::UdpTrackerCoreContainer; @@ -26,7 +27,7 @@ where S: std::fmt::Debug + std::fmt::Display, { pub container: Arc, - pub registar: Registar, + pub registar: Registar, pub server: Server, pub udp_core_event_listener_job: Option>, pub udp_server_stats_event_listener_job: Option>, @@ -109,6 +110,10 @@ impl Environment { self.container.udp_tracker_core_container.clone(), self.container.udp_tracker_server_container.clone(), self.registar.give_form(), + RuntimeServiceMetadata::new( + ServiceRole::UdpTracker, + ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0), + ), cookie_lifetime, self.connection_id_validation, ) diff --git a/src/app.rs b/src/app.rs index 7143d1fc3..1b3558894 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,6 +25,7 @@ use std::sync::Arc; use torrust_clock::clock::Time; use torrust_tracker_configuration::{Configuration, HttpTracker, UdpTracker}; +use torrust_tracker_primitives::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole}; use tracing::instrument; use crate::CurrentClock; @@ -238,7 +239,7 @@ async fn start_udp_instance( app_container: &Arc, job_manager: &mut JobManager, ) { - let udp_tracker_container = app_container + let (configuration_instance_id, udp_tracker_container) = app_container .udp_tracker_container(idx) .expect("Could not create UDP tracker container"); let udp_tracker_server_container = app_container.udp_tracker_server_container(); @@ -248,6 +249,7 @@ async fn start_udp_instance( udp_tracker_container, udp_tracker_server_container, app_container.registar.give_form(), + RuntimeServiceMetadata::new(ServiceRole::UdpTracker, configuration_instance_id), ) .await; @@ -270,7 +272,7 @@ async fn start_http_instance( app_container: &Arc, job_manager: &mut JobManager, ) { - let http_tracker_container = app_container + let (configuration_instance_id, http_tracker_container) = app_container .http_tracker_container(idx) .expect("Could not create HTTP tracker container"); @@ -278,6 +280,7 @@ async fn start_http_instance( idx, http_tracker_container, app_container.registar.give_form(), + RuntimeServiceMetadata::new(ServiceRole::HttpTracker, configuration_instance_id), torrust_tracker_axum_http_server::Version::V1, ) .await @@ -294,6 +297,7 @@ async fn start_the_http_api(config: &Configuration, app_container: &Arc, job_manager: &mut JobManager) { - let handle = health_check_api::start_job(&config.health_check_api, app_container.registar.entries()).await; + let handle = health_check_api::start_job(&config.health_check_api, app_container.registar.as_ref().clone()).await; job_manager.push("health_check_api", handle); } diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index 6fc15f294..14bc18f6c 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -17,10 +17,11 @@ use tokio::sync::oneshot; use tokio::task::JoinHandle; use torrust_server_lib::logging::STARTED_ON; -use torrust_server_lib::registar::ServiceRegistry; +use torrust_server_lib::registar::{Registar, ServiceRegistration}; use torrust_server_lib::signals::{Halted, Started}; use torrust_tracker_axum_health_check_api_server::{HEALTH_CHECK_API_LOG_TARGET, server}; use torrust_tracker_configuration::HealthCheckApi; +use torrust_tracker_primitives::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole}; use tracing::instrument; /// This function starts a new Health Check API server with the provided @@ -34,8 +35,8 @@ use tracing::instrument; /// /// It would panic if unable to send the `ApiServerJobStarted` notice. #[allow(clippy::async_yields_async)] -#[instrument(skip(config, register))] -pub async fn start_job(config: &HealthCheckApi, register: ServiceRegistry) -> JoinHandle<()> { +#[instrument(skip(config, registar))] +pub async fn start_job(config: &HealthCheckApi, registar: Registar) -> JoinHandle<()> { let bind_addr = config.bind_address; let (tx_start, rx_start) = oneshot::channel::(); @@ -44,10 +45,11 @@ pub async fn start_job(config: &HealthCheckApi, register: ServiceRegistry) -> Jo let protocol = "http"; // Run the API server + let health_check_api_registar = registar.clone(); let join_handle = tokio::spawn(async move { tracing::info!(target: HEALTH_CHECK_API_LOG_TARGET, "Starting on: {protocol}://{}", bind_addr); - let handle = server::start(bind_addr, tx_start, rx_halt, register); + let handle = server::start(bind_addr, tx_start, rx_halt, health_check_api_registar); if matches!(handle.await, Ok(())) { tracing::info!(target: HEALTH_CHECK_API_LOG_TARGET, "Stopped server running on: {protocol}://{}", bind_addr); @@ -56,7 +58,22 @@ pub async fn start_job(config: &HealthCheckApi, register: ServiceRegistry) -> Jo // Wait until the server sends the started message match rx_start.await { - Ok(msg) => tracing::info!(target: HEALTH_CHECK_API_LOG_TARGET, "{STARTED_ON}: {protocol}://{}", msg.address), + Ok(msg) => { + registar + .give_form() + .register(ServiceRegistration::new( + msg.service_binding, + RuntimeServiceMetadata::new( + ServiceRole::HealthCheckApi, + ConfigurationInstanceId::new(ServiceRole::HealthCheckApi, 0), + ), + None, + )) + .await + .expect("it should be able to register the started health check API"); + + tracing::info!(target: HEALTH_CHECK_API_LOG_TARGET, "{STARTED_ON}: {protocol}://{}", msg.address); + } Err(e) => panic!("the Health Check API server was dropped: {e}"), } diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 2849dd7da..755fe9cba 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -20,6 +20,7 @@ use torrust_tracker_axum_http_server::Version; use torrust_tracker_axum_http_server::server::{HttpServer, Launcher}; use torrust_tracker_axum_server::tls::make_rust_tls; use torrust_tracker_http_core::container::HttpTrackerCoreContainer; +use torrust_tracker_primitives::RuntimeServiceMetadata; use tracing::instrument; /// It starts a new HTTP server with the provided configuration and version. @@ -34,7 +35,8 @@ use tracing::instrument; pub async fn start_job( idx: usize, http_tracker_container: Arc, - form: ServiceRegistrationForm, + form: ServiceRegistrationForm, + metadata: RuntimeServiceMetadata, version: Version, ) -> Option> { let socket = http_tracker_container.http_tracker_config.bind_address; @@ -57,7 +59,7 @@ pub async fn start_job( }; match version { - Version::V1 => Some(start_v1(socket, tls, http_tracker_container, form).await), + Version::V1 => Some(start_v1(socket, tls, http_tracker_container, form, metadata).await), } } @@ -67,14 +69,15 @@ async fn start_v1( socket: SocketAddr, tls: Option, http_tracker_container: Arc, - form: ServiceRegistrationForm, + form: ServiceRegistrationForm, + metadata: RuntimeServiceMetadata, ) -> JoinHandle<()> { let server = HttpServer::new(Launcher::new( socket, tls, http_tracker_container.http_tracker_config.ipv6_v6only, )) - .start(http_tracker_container, form) + .start(http_tracker_container, form, metadata) .await .expect("it should be able to start to the http tracker"); @@ -116,8 +119,17 @@ mod tests { let version = Version::V1; - start_job(0, http_tracker_container, Registar::default().give_form(), version) - .await - .expect("it should be able to join to the http tracker start-job"); + start_job( + 0, + http_tracker_container, + Registar::default().give_form(), + torrust_tracker_primitives::RuntimeServiceMetadata::new( + torrust_tracker_primitives::ServiceRole::HttpTracker, + torrust_tracker_primitives::ConfigurationInstanceId::new(torrust_tracker_primitives::ServiceRole::HttpTracker, 0), + ), + version, + ) + .await + .expect("it should be able to join to the http tracker start-job"); } } diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index f86ba5d23..3a5d0ea8a 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -30,6 +30,7 @@ use torrust_tracker_axum_rest_api_server::Version; use torrust_tracker_axum_rest_api_server::server::{ApiServer, Launcher}; use torrust_tracker_axum_server::tls::make_rust_tls; use torrust_tracker_configuration::AccessTokens; +use torrust_tracker_primitives::RuntimeServiceMetadata; use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use tracing::instrument; @@ -56,7 +57,8 @@ pub struct ApiServerJobStarted(); #[instrument(skip(http_api_container, form))] pub async fn start_job( http_api_container: Arc, - form: ServiceRegistrationForm, + form: ServiceRegistrationForm, + metadata: RuntimeServiceMetadata, version: Version, ) -> Option> { let bind_to = http_api_container.http_api_config.bind_address; @@ -74,7 +76,7 @@ pub async fn start_job( let access_tokens = Arc::new(http_api_container.http_api_config.access_tokens.clone()); match version { - Version::V1 => Some(start_v1(bind_to, tls, http_api_container, form, access_tokens).await), + Version::V1 => Some(start_v1(bind_to, tls, http_api_container, form, metadata, access_tokens).await), } } @@ -84,11 +86,12 @@ async fn start_v1( socket: SocketAddr, tls: Option, http_api_container: Arc, - form: ServiceRegistrationForm, + form: ServiceRegistrationForm, + metadata: RuntimeServiceMetadata, access_tokens: Arc, ) -> JoinHandle<()> { let server = ApiServer::new(Launcher::new(socket, tls)) - .start(http_api_container, form, access_tokens) + .start(http_api_container, form, metadata, access_tokens) .await .expect("it should be able to start to the tracker api"); @@ -132,8 +135,16 @@ mod tests { let version = Version::V1; - start_job(http_api_container, Registar::default().give_form(), version) - .await - .expect("it should be able to join to the tracker api start-job"); + start_job( + http_api_container, + Registar::default().give_form(), + torrust_tracker_primitives::RuntimeServiceMetadata::new( + torrust_tracker_primitives::ServiceRole::RestApi, + torrust_tracker_primitives::ConfigurationInstanceId::new(torrust_tracker_primitives::ServiceRole::RestApi, 0), + ), + version, + ) + .await + .expect("it should be able to join to the tracker api start-job"); } } diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index d9734d045..2bcca3678 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use tokio::task::JoinHandle; use torrust_server_lib::registar::ServiceRegistrationForm; +use torrust_tracker_primitives::RuntimeServiceMetadata; use torrust_tracker_udp_core::container::UdpTrackerCoreContainer; use torrust_tracker_udp_core::{ConnectionIdValidationPolicy, UDP_TRACKER_LOG_TARGET}; use torrust_tracker_udp_server::container::UdpTrackerServerContainer; @@ -33,7 +34,8 @@ pub async fn start_job( idx: usize, udp_tracker_core_container: Arc, udp_tracker_server_container: Arc, - form: ServiceRegistrationForm, + form: ServiceRegistrationForm, + metadata: RuntimeServiceMetadata, ) -> JoinHandle<()> { let bind_to = udp_tracker_core_container.udp_tracker_config.bind_address; let cookie_lifetime = udp_tracker_core_container.udp_tracker_config.cookie_lifetime; @@ -57,6 +59,7 @@ pub async fn start_job( udp_tracker_core_container, udp_tracker_server_container, form, + metadata, cookie_lifetime, connection_id_validation, ) diff --git a/src/container.rs b/src/container.rs index 8e5071964..dd4bdf97b 100644 --- a/src/container.rs +++ b/src/container.rs @@ -26,7 +26,7 @@ pub struct AppContainer { pub http_api_config: Arc>, // Registar - pub registar: Arc, + pub registar: Arc>, // Swarm Coordination Registry Container pub swarm_coordination_registry_container: Arc, @@ -133,10 +133,13 @@ impl AppContainer { /// /// Return an error if there is no HTTP tracker container at the given /// configuration index. - pub fn http_tracker_container(&self, index: usize) -> Result, Error> { + pub fn http_tracker_container( + &self, + index: usize, + ) -> Result<(ConfigurationInstanceId, Arc), Error> { self.http_tracker_instance_containers.get(index).map_or_else( || Err(Error::MissingHttpTrackerCoreContainer { index }), - |(_id, container)| Ok(container.clone()), + |(id, container)| Ok((*id, container.clone())), ) } @@ -144,10 +147,10 @@ impl AppContainer { /// /// Return an error if there is no UDP tracker container at the given /// configuration index. - pub fn udp_tracker_container(&self, index: usize) -> Result, Error> { + pub fn udp_tracker_container(&self, index: usize) -> Result<(ConfigurationInstanceId, Arc), Error> { self.udp_tracker_instance_containers.get(index).map_or_else( || Err(Error::MissingUdpTrackerCoreContainer { index }), - |(_id, container)| Ok(container.clone()), + |(id, container)| Ok((*id, container.clone())), ) } diff --git a/tests/aggregate_stats_port_zero.rs b/tests/aggregate_stats_port_zero.rs index d5fb5c054..c414e6992 100644 --- a/tests/aggregate_stats_port_zero.rs +++ b/tests/aggregate_stats_port_zero.rs @@ -10,6 +10,7 @@ mod common; use torrust_clock::clock; +use torrust_tracker_primitives::{ConfigurationInstanceId, ServiceRole}; /// This code needs to be copied into each crate. /// Working version, for production. @@ -73,10 +74,30 @@ async fn stats_scenarios() { let (app_container, _jobs) = common::start_tracker_with_config(&workspace).await; duplicate_port_zero_instances_should_receive_distinct_configurations(&app_container); + duplicate_port_zero_instances_should_retain_runtime_identity(&app_container).await; two_http_trackers_on_port_zero_should_aggregate_announces_from_both_listeners(&app_container).await; two_udp_trackers_on_port_zero_should_aggregate_announces_from_both_listeners(&app_container).await; } +/// Repeated configuration blocks must retain their canonical identity after +/// receiving their distinct operating-system-assigned final bindings. +async fn duplicate_port_zero_instances_should_retain_runtime_identity( + app_container: &std::sync::Arc, +) { + for service_role in [ServiceRole::HttpTracker, ServiceRole::UdpTracker] { + let first = common::service_binding_for_identity(app_container, ConfigurationInstanceId::new(service_role, 0)) + .await + .expect("first configured instance should be registered"); + let second = common::service_binding_for_identity(app_container, ConfigurationInstanceId::new(service_role, 1)) + .await + .expect("second configured instance should be registered"); + + assert_ne!(first.bind_address().port(), 0); + assert_ne!(second.bind_address().port(), 0); + assert_ne!(first.bind_address(), second.bind_address()); + } +} + /// Duplicate port-zero configuration blocks each receive their own container /// with distinct settings, proving the bootstrap fix prevents the /// address-keyed collision. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 974a20a8d..dc61dfdac 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -28,5 +28,6 @@ pub use announce::{http_announce, udp_announce}; pub use statistics::{PartialGlobalStatistics, get_tracker_statistics}; #[allow(unused_imports)] pub use workspace::{ - EphemeralTrackerWorkspace, http_api_url, http_tracker_urls, start_tracker_with_config, udp_socket_addr, udp_tracker_urls, + EphemeralTrackerWorkspace, http_api_url, http_tracker_urls, service_binding_for_identity, start_tracker_with_config, + udp_socket_addr, udp_tracker_urls, }; diff --git a/tests/common/workspace.rs b/tests/common/workspace.rs index 343504e37..02275cec5 100644 --- a/tests/common/workspace.rs +++ b/tests/common/workspace.rs @@ -5,9 +5,11 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use tempfile::TempDir; +use torrust_net_primitives::service_binding::ServiceBinding; use torrust_tracker_lib::app; use torrust_tracker_lib::bootstrap::jobs::manager::JobManager; use torrust_tracker_lib::container::AppContainer; +use torrust_tracker_primitives::{ConfigurationInstanceId, ServiceRole}; use url::Url; /// A temporary workspace for an integration test. @@ -50,17 +52,7 @@ impl EphemeralTrackerWorkspace { /// tests in this binary must not run concurrently with other tests /// that modify the same variable. /// -/// A short delay is added after startup to allow services to register -/// in the registar and bind to OS-assigned ports. pub async fn start_tracker_with_config(workspace: &EphemeralTrackerWorkspace) -> (Arc, JobManager) { - // We require at least two services to be registered before proceeding. - // This covers the common case of one HTTP tracker plus one UDP tracker. - // We intentionally do NOT wait for all services (HTTP API, health check, - // etc.) because scenarios only need the tracker listeners to be ready. - // Configurations with fewer services (e.g., health-check only) should - // use a lower threshold or bypass this wait. - const MIN_REGISTERED_SERVICES: usize = 2; - // SAFETY: This binary must be the only test executable setting // `TORRUST_TRACKER_CONFIG_TOML_PATH`. Cargo may run different // integration-test binaries in parallel, but each binary is a @@ -75,26 +67,24 @@ pub async fn start_tracker_with_config(workspace: &EphemeralTrackerWorkspace) -> let (container, jobs) = app::run().await; - // Wait for services to register in the registar and bind to ports. - // Polls the registar instead of using a fixed sleep to avoid - // flakiness on slow machines and unnecessary delay on fast ones. - // - // TODO: This gate can pass before the specific services scenarios need are - // registered (e.g., if HTTP API + health check register first). Consider - // waiting on concrete predicates per test binary when flakiness appears. - // Tracked by #1430. + // Each service acknowledges registry insertion only after binding its + // final listener. Wait for the exact configuration identities, rather than + // a map-size threshold or a registration delay. + let expected_identities = expected_service_identities(&container); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); loop { - let entries = container.registar.entries(); - let map = entries.lock().await; - if map.len() >= MIN_REGISTERED_SERVICES { + let services = container.registar.services().await; + if expected_identities.iter().all(|identity| { + services + .iter() + .any(|service| service.metadata().configuration_instance_id() == *identity) + }) { break; } - drop(map); assert!( std::time::Instant::now() < deadline, - "timeout waiting for services to register in the registar" + "timeout waiting for configured services to register in the registar" ); tokio::time::sleep(std::time::Duration::from_millis(50)).await; } @@ -104,26 +94,22 @@ pub async fn start_tracker_with_config(workspace: &EphemeralTrackerWorkspace) -> /// Returns the HTTP tracker URLs from the registar. /// -/// HTTP trackers bind to `0.0.0.0` (unspecified). The REST API and health -/// check bind to `127.0.0.1` (loopback). We identify trackers by their -/// unspecified IP, which is deterministic regardless of hash-map ordering. -/// Wildcard addresses are converted to `127.0.0.1` for client requests. +/// Uses the canonical HTTP tracker role, not a bind-IP convention. Wildcard +/// addresses are converted to `127.0.0.1` for client requests. pub async fn http_tracker_urls(container: &AppContainer) -> Vec { - let reg = container.registar.entries(); - let map = reg.lock().await; - map.keys() - .filter(|b| { - b.protocol() == torrust_net_primitives::service_binding::Protocol::HTTP && b.bind_address().ip().is_unspecified() - }) - .map(|b| loopback_url(b.bind_address())) + container + .registar + .services_matching(|metadata| metadata.service_role() == ServiceRole::HttpTracker) + .await + .iter() + .map(|service| loopback_url(service.service_binding().bind_address())) .collect() } /// Returns the UDP tracker URLs from the registar. /// -/// UDP trackers bind to `0.0.0.0` (unspecified). We identify them by their -/// unspecified IP, which is deterministic regardless of hash-map ordering. -/// Wildcard addresses are converted to `127.0.0.1` for client requests. +/// Uses the canonical UDP tracker role, not a bind-IP convention. Wildcard +/// addresses are converted to `127.0.0.1` for client requests. // // Each integration-test binary compiles this module independently. Not all // binaries call every function here, so the compiler emits dead_code warnings @@ -131,30 +117,65 @@ pub async fn http_tracker_urls(container: &AppContainer) -> Vec { // false positives without hiding genuine dead code in the workspace as a whole. #[allow(dead_code)] pub async fn udp_tracker_urls(container: &AppContainer) -> Vec { - let reg = container.registar.entries(); - let map = reg.lock().await; - map.keys() - .filter(|b| { - b.protocol() == torrust_net_primitives::service_binding::Protocol::UDP && b.bind_address().ip().is_unspecified() - }) - .map(|b| udp_loopback_url(b.bind_address())) + container + .registar + .services_matching(|metadata| metadata.service_role() == ServiceRole::UdpTracker) + .await + .iter() + .map(|service| udp_loopback_url(service.service_binding().bind_address())) .collect() } /// Returns the HTTP API URL from the registar. /// -/// The REST API binds to `127.0.0.1` (loopback), unlike the HTTP trackers -/// which bind to `0.0.0.0`. We filter specifically for the REST API bind IP -/// (`127.0.0.1`) to avoid matching the health-check API on `127.0.0.2`. +/// Uses the canonical REST API role, not a bind-IP convention. pub async fn http_api_url(container: &AppContainer) -> Option { - let reg = container.registar.entries(); - let map = reg.lock().await; - map.keys() - .find(|b| { - b.protocol() == torrust_net_primitives::service_binding::Protocol::HTTP - && b.bind_address().ip() == std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST) - }) - .map(|b| loopback_url(b.bind_address())) + container + .registar + .services_matching(|metadata| metadata.service_role() == ServiceRole::RestApi) + .await + .first() + .map(|service| loopback_url(service.service_binding().bind_address())) +} + +/// Returns the final binding for one exact canonical configuration identity. +/// +/// This is side-effect free: registry visibility acknowledges that the service +/// has bound this listener. +#[allow(dead_code)] +pub async fn service_binding_for_identity( + container: &AppContainer, + configuration_instance_id: ConfigurationInstanceId, +) -> Option { + container + .registar + .services_matching(|metadata| metadata.configuration_instance_id() == configuration_instance_id) + .await + .into_iter() + .next() + .map(|service| service.service_binding().clone()) +} + +fn expected_service_identities(container: &AppContainer) -> Vec { + let mut identities: Vec<_> = container + .http_tracker_instance_containers + .iter() + .map(|(identity, _)| *identity) + .chain( + container + .udp_tracker_instance_containers + .iter() + .map(|(identity, _)| *identity), + ) + .collect(); + + if container.http_api_config.is_some() { + identities.push(ConfigurationInstanceId::new(ServiceRole::RestApi, 0)); + } + + identities.push(ConfigurationInstanceId::new(ServiceRole::HealthCheckApi, 0)); + + identities } /// Convert a socket address to a connectable loopback URL. diff --git a/tests/scaffold.rs b/tests/scaffold.rs index 28fd03885..2693b5ea2 100644 --- a/tests/scaffold.rs +++ b/tests/scaffold.rs @@ -30,16 +30,14 @@ //! //! - Port `0` for all service bind addresses. //! - Isolated temporary workspace per suite (`EphemeralTrackerWorkspace`). -//! - A small startup delay to allow async service registration. +//! - Registration-acknowledgement readiness for every configured service. //! - Sequential scenarios that account for accumulated state. //! -//! ## Temporary Limitation +//! ## Endpoint Discovery //! -//! Endpoint discovery currently distinguishes services by test-only bind-IP conventions. This -//! sample must not be copied as a general service-discovery pattern until the bootstrap and runtime -//! registry prerequisite issues are implemented. See -//! `docs/issues/open/2035-fix-duplicate-port-zero-tracker-instance-bootstrap/ISSUE.md` and -//! `docs/issues/open/2036-add-runtime-service-registry-metadata/ISSUE.md`. +//! Endpoint discovery uses side-effect-free runtime-registry snapshots. Helpers +//! select services by canonical role or exact configuration identity rather +//! than bind-IP conventions, registration delays, or registry-map ordering. //! //! # Example: Running this test //! From e95153036dce673aa861904a26111896b570e6ae Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 Jul 2026 13:48:46 +0100 Subject: [PATCH 4/9] refactor(primitives): derive runtime role from identity --- .../ISSUE.md | 8 +++++--- packages/axum-http-server/src/server.rs | 5 +---- .../src/testing/environment.rs | 5 +---- packages/axum-rest-api-server/src/server.rs | 2 +- .../src/testing/environment.rs | 2 +- .../src/runtime_service_metadata.rs | 20 +++++++++++++++---- packages/udp-server/src/server/mod.rs | 10 ++-------- .../udp-server/src/testing/environment.rs | 5 +---- src/app.rs | 6 +++--- src/bootstrap/jobs/health_check_api.rs | 5 +---- src/bootstrap/jobs/http_tracker.rs | 6 +++--- src/bootstrap/jobs/tracker_apis.rs | 6 +++--- 12 files changed, 38 insertions(+), 42 deletions(-) diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md index ce4ae902a..fa92422f3 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md @@ -141,9 +141,11 @@ Dynamic restart, deregistration, replacement, liveness removal, and re-registration are intentionally out of scope. The registry rejects duplicate final bindings so a snapshot never represents two services at one listener. -The tracker owns a typed runtime metadata value containing `ServiceRole` and -`ConfigurationInstanceId`. `torrust-server-lib` must not define tracker roles, -configuration identifiers, metrics policy, or tracker-specific metadata keys. +The tracker owns a typed runtime metadata value containing the canonical +`ConfigurationInstanceId`; its `ServiceRole` is derived from that identity, so +the metadata cannot represent inconsistent role and identity values. +`torrust-server-lib` must not define tracker roles, configuration identifiers, +metrics policy, or tracker-specific metadata keys. ### Registration and Readiness diff --git a/packages/axum-http-server/src/server.rs b/packages/axum-http-server/src/server.rs index ae6f544f7..4209df9b7 100644 --- a/packages/axum-http-server/src/server.rs +++ b/packages/axum-http-server/src/server.rs @@ -408,10 +408,7 @@ mod tests { .start( http_tracker_container, register.give_form(), - RuntimeServiceMetadata::new( - ServiceRole::HttpTracker, - ConfigurationInstanceId::new(ServiceRole::HttpTracker, 0), - ), + RuntimeServiceMetadata::new(ConfigurationInstanceId::new(ServiceRole::HttpTracker, 0)), ) .await .expect("it should start the server"); diff --git a/packages/axum-http-server/src/testing/environment.rs b/packages/axum-http-server/src/testing/environment.rs index 2f90337b3..a5060d5a7 100644 --- a/packages/axum-http-server/src/testing/environment.rs +++ b/packages/axum-http-server/src/testing/environment.rs @@ -89,10 +89,7 @@ impl Environment { .start( self.container.http_tracker_core_container.clone(), self.registar.give_form(), - RuntimeServiceMetadata::new( - ServiceRole::HttpTracker, - ConfigurationInstanceId::new(ServiceRole::HttpTracker, 0), - ), + RuntimeServiceMetadata::new(ConfigurationInstanceId::new(ServiceRole::HttpTracker, 0)), ) .await .expect("Failed to start the HTTP tracker server"); diff --git a/packages/axum-rest-api-server/src/server.rs b/packages/axum-rest-api-server/src/server.rs index 133df5acb..5c519835c 100644 --- a/packages/axum-rest-api-server/src/server.rs +++ b/packages/axum-rest-api-server/src/server.rs @@ -361,7 +361,7 @@ mod tests { .start( http_api_container, register.give_form(), - RuntimeServiceMetadata::new(ServiceRole::RestApi, ConfigurationInstanceId::new(ServiceRole::RestApi, 0)), + RuntimeServiceMetadata::new(ConfigurationInstanceId::new(ServiceRole::RestApi, 0)), access_tokens, ) .await diff --git a/packages/axum-rest-api-server/src/testing/environment.rs b/packages/axum-rest-api-server/src/testing/environment.rs index 86a5ac314..73758f3fb 100644 --- a/packages/axum-rest-api-server/src/testing/environment.rs +++ b/packages/axum-rest-api-server/src/testing/environment.rs @@ -89,7 +89,7 @@ impl Environment { .start( self.container.tracker_http_api_core_container.clone(), self.registar.give_form(), - RuntimeServiceMetadata::new(ServiceRole::RestApi, ConfigurationInstanceId::new(ServiceRole::RestApi, 0)), + RuntimeServiceMetadata::new(ConfigurationInstanceId::new(ServiceRole::RestApi, 0)), access_tokens, ) .await diff --git a/packages/primitives/src/runtime_service_metadata.rs b/packages/primitives/src/runtime_service_metadata.rs index 73da1ff1c..597942414 100644 --- a/packages/primitives/src/runtime_service_metadata.rs +++ b/packages/primitives/src/runtime_service_metadata.rs @@ -6,16 +6,14 @@ use crate::{ConfigurationInstanceId, ServiceRole}; /// this tracker-owned value without assigning it application semantics. #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone, Copy)] pub struct RuntimeServiceMetadata { - service_role: ServiceRole, configuration_instance_id: ConfigurationInstanceId, } impl RuntimeServiceMetadata { /// Creates metadata for a canonical tracker service instance. #[must_use] - pub const fn new(service_role: ServiceRole, configuration_instance_id: ConfigurationInstanceId) -> Self { + pub const fn new(configuration_instance_id: ConfigurationInstanceId) -> Self { Self { - service_role, configuration_instance_id, } } @@ -23,7 +21,7 @@ impl RuntimeServiceMetadata { /// Returns the role implemented by the started listener. #[must_use] pub const fn service_role(self) -> ServiceRole { - self.service_role + self.configuration_instance_id.service_role() } /// Returns the source configuration instance for the listener. @@ -32,3 +30,17 @@ impl RuntimeServiceMetadata { self.configuration_instance_id } } + +#[cfg(test)] +mod tests { + use crate::{ConfigurationInstanceId, RuntimeServiceMetadata, ServiceRole}; + + #[test] + fn it_should_derive_the_role_from_the_configuration_instance_identity() { + let configuration_instance_id = ConfigurationInstanceId::new(ServiceRole::UdpTracker, 1); + let metadata = RuntimeServiceMetadata::new(configuration_instance_id); + + assert_eq!(metadata.service_role(), ServiceRole::UdpTracker); + assert_eq!(metadata.configuration_instance_id(), configuration_instance_id); + } +} diff --git a/packages/udp-server/src/server/mod.rs b/packages/udp-server/src/server/mod.rs index 5ae9c50de..c23eef2da 100644 --- a/packages/udp-server/src/server/mod.rs +++ b/packages/udp-server/src/server/mod.rs @@ -105,10 +105,7 @@ mod tests { udp_tracker_core_container, udp_tracker_server_container, register.give_form(), - RuntimeServiceMetadata::new( - ServiceRole::UdpTracker, - ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0), - ), + RuntimeServiceMetadata::new(ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0)), config.cookie_lifetime, torrust_tracker_udp_core::ConnectionIdValidationPolicy::Strict, ) @@ -150,10 +147,7 @@ mod tests { udp_tracker_core_container, udp_tracker_server_container, register.give_form(), - RuntimeServiceMetadata::new( - ServiceRole::UdpTracker, - ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0), - ), + RuntimeServiceMetadata::new(ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0)), udp_tracker_config.cookie_lifetime, torrust_tracker_udp_core::ConnectionIdValidationPolicy::Strict, ) diff --git a/packages/udp-server/src/testing/environment.rs b/packages/udp-server/src/testing/environment.rs index 880313da1..7fd0cb555 100644 --- a/packages/udp-server/src/testing/environment.rs +++ b/packages/udp-server/src/testing/environment.rs @@ -110,10 +110,7 @@ impl Environment { self.container.udp_tracker_core_container.clone(), self.container.udp_tracker_server_container.clone(), self.registar.give_form(), - RuntimeServiceMetadata::new( - ServiceRole::UdpTracker, - ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0), - ), + RuntimeServiceMetadata::new(ConfigurationInstanceId::new(ServiceRole::UdpTracker, 0)), cookie_lifetime, self.connection_id_validation, ) diff --git a/src/app.rs b/src/app.rs index 1b3558894..3edee5cdc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -249,7 +249,7 @@ async fn start_udp_instance( udp_tracker_container, udp_tracker_server_container, app_container.registar.give_form(), - RuntimeServiceMetadata::new(ServiceRole::UdpTracker, configuration_instance_id), + RuntimeServiceMetadata::new(configuration_instance_id), ) .await; @@ -280,7 +280,7 @@ async fn start_http_instance( idx, http_tracker_container, app_container.registar.give_form(), - RuntimeServiceMetadata::new(ServiceRole::HttpTracker, configuration_instance_id), + RuntimeServiceMetadata::new(configuration_instance_id), torrust_tracker_axum_http_server::Version::V1, ) .await @@ -297,7 +297,7 @@ async fn start_the_http_api(config: &Configuration, app_container: &Arc Date: Fri, 31 Jul 2026 15:48:59 +0100 Subject: [PATCH 5/9] docs: document local HTTPS verification --- .../run-tracker-locally/SKILL.md | 57 +++++++ ...fix-https-tracker-health-check-protocol.md | 139 ++++++++++++++++++ .../ISSUE.md | 36 ++--- .../evidence.md | 103 ++++++++++++- project-words.txt | 1 + 5 files changed, 316 insertions(+), 20 deletions(-) create mode 100644 docs/issues/drafts/fix-https-tracker-health-check-protocol.md diff --git a/.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md b/.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md index 9856bd772..7a5767b83 100644 --- a/.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md +++ b/.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md @@ -110,6 +110,63 @@ netstat -ulnp 2>/dev/null | grep -E '6969|6970' netstat -tlnp 2>/dev/null | grep -E '7070|7071|1212' ``` +## Running a Local HTTPS Tracker + +For local TLS verification, create a temporary configuration and certificate +under `.tmp/`. The directory is git-ignored, so do not place test keys in +`share/` or commit them. + +1. Copy or create a configuration based on the development configuration. Give + an HTTP tracker a port-zero binding if the final runtime binding is part of + the behavior under test: + +```toml +[[http_trackers]] +bind_address = "0.0.0.0:0" + +# Schema 2.0 uses the historical `tsl_config` spelling. +[http_trackers.tsl_config] +ssl_cert_path = ".tmp/localhost.crt" +ssl_key_path = ".tmp/localhost.key" +``` + +1. Generate a short-lived self-signed certificate for local use. Include SANs + for both `localhost` and `127.0.0.1` so a loopback client can validate it + when supplied with the certificate: + +```bash +openssl req -x509 -out .tmp/localhost.crt -keyout .tmp/localhost.key \ + -newkey rsa:2048 -nodes -sha256 -days 1 \ + -subj '/CN=localhost' \ + -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1' \ + -addext 'keyUsage=digitalSignature' \ + -addext 'extendedKeyUsage=serverAuth' +``` + +1. Start the tracker with the temporary configuration: + +```bash +TORRUST_TRACKER_CONFIG_TOML_PATH="$PWD/.tmp/local-tls.toml" cargo run --bin torrust-tracker +``` + +Read the startup log to obtain the final port assigned to a `:0` binding. +It will report an `https://` URL when TLS is enabled. + +1. Probe the listener. `--insecure` is appropriate only for this temporary + self-signed local certificate: + +```bash +curl --fail --silent --show-error --insecure https://127.0.0.1:/health_check +``` + +1. Stop the tracker and remove or retain the `.tmp/` files as local-only test + artifacts. Restore any temporary configuration edits before committing. + +> **Known limitation:** the aggregate health-check service currently builds +> HTTP-tracker probes with an `http://` URL even when a registered listener is +> HTTPS. A direct HTTPS probe verifies the TLS listener; do not treat that +> separate health-check defect as a TLS-startup failure. + ## Database Storage By default, development tracker uses SQLite3. The database file is stored in: diff --git a/docs/issues/drafts/fix-https-tracker-health-check-protocol.md b/docs/issues/drafts/fix-https-tracker-health-check-protocol.md new file mode 100644 index 000000000..02f52cb94 --- /dev/null +++ b/docs/issues/drafts/fix-https-tracker-health-check-protocol.md @@ -0,0 +1,139 @@ +--- +doc-type: issue +issue-type: bug +status: draft +priority: p2 +github-issue: null +spec-path: docs/issues/drafts/fix-https-tracker-health-check-protocol.md +branch: "{issue-number}-fix-https-tracker-health-check-protocol" +related-pr: null +last-updated-utc: 2026-07-31 14:00 +semantic-links: + skill-links: + - create-issue + related-artifacts: + - .github/skills/dev/planning/create-issue/SKILL.md + - packages/axum-http-server/src/server.rs +--- + + + +# Issue #[To be assigned] - Fix HTTPS tracker health-check protocol + +## Goal + +Make the HTTP tracker health-check job probe a registered listener with the +same transport protocol as its `ServiceBinding`, so HTTPS listeners report +their real health status. + +## Background + +During manual verification for #2041, a TLS-enabled HTTP tracker successfully +bound as `https://0.0.0.0:60057/` and directly returned `{"status":"Ok"}` from +its `/health_check` endpoint. The aggregate health API correctly exposed that +HTTPS `service_binding`, its final socket address, and +`service_type="http_tracker"`, but reported an error for the service. + +`packages/axum-http-server/src/server.rs` currently builds every HTTP-tracker +health-check URL as `http://{binding}/health_check`. For an HTTPS registration, +this probes plain HTTP on the TLS port and fails. The issue was pre-existing and +outside #2041's registry-metadata scope. + +## Scope + +### In Scope + +- Derive the HTTP tracker health-check URL scheme from `ServiceBinding`. +- Preserve HTTP tracker health-check behavior for ordinary HTTP listeners. +- Add regression coverage for HTTPS listener health checks. +- Verify the aggregate health API reports `Ok` for an operational local HTTPS + tracker using a temporary self-signed certificate. + +### Out of Scope + +- Changing TLS certificate loading or certificate validation policy. +- Changing the health API response schema. +- Changing runtime registry metadata or service identity behavior introduced by + #2041. + +## Implementation Plan + +Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. + +| ID | Status | Task | Notes / Expected Output | +| --- | ------ | ----------------------------------------- | ------------------------------------------------------------------- | +| T1 | TODO | Add failing HTTPS health-check regression | Prove an HTTPS registration is not probed as plain HTTP. | +| T2 | TODO | Derive check URL from service binding | Use the binding's protocol and address without altering HTTP paths. | +| T3 | TODO | Validate health-report behavior | Aggregate report marks healthy local HTTP and HTTPS services `Ok`. | +| T4 | TODO | Document verification evidence | Record automated and manual results. | + +## Progress Tracking + +### Workflow Checkpoints + +- [x] Spec drafted in `docs/issues/drafts/` +- [ ] Spec reviewed and approved by user/maintainer +- [ ] GitHub issue created and issue number added to this spec +- [ ] Implementation completed +- [ ] Automatic verification completed (`linter all`, relevant tests, and any pre-push checks) +- [ ] Manual verification scenarios executed and recorded (status + evidence) +- [ ] Acceptance criteria reviewed after implementation and updated with evidence +- [ ] Reviewer validated acceptance criteria and updated checkboxes +- [ ] Committer verified spec progress is up to date before commit +- [ ] Issue closed and spec moved from `docs/issues/open/` to `docs/issues/closed/` + +### Progress Log + +- 2026-07-31 14:00 UTC - agent - Drafted from the manual TLS verification observation in #2041. Awaiting user review before GitHub issue creation. + +## Acceptance Criteria + +- [ ] AC1: An HTTPS HTTP-tracker registration is health-checked through an + `https://` URL, not an `http://` URL. +- [ ] AC2: An operational HTTPS listener yields a successful entry in the + aggregate health report. +- [ ] AC3: Existing HTTP tracker health checks continue to pass. +- [ ] `linter all` exits with code `0`. +- [ ] Relevant tests pass. +- [ ] Manual verification scenarios are executed and documented (status + evidence). +- [ ] Acceptance criteria are re-reviewed after implementation and reflect actual behavior. +- [ ] Documentation is updated when behavior/workflow changes. + +## Verification Plan + +### Automatic Checks + +- `cargo test -p torrust-tracker-axum-http-server` +- `cargo test -p torrust-tracker-axum-health-check-api-server --test integration` +- `linter all` +- Relevant pre-commit and pre-push checks + +### Manual Verification Scenarios + +Status values: `TODO`, `IN_PROGRESS`, `DONE`, `FAILED`, `BLOCKED`. + +| ID | Scenario | Command/Steps | Expected Result | Status | Evidence | +| --- | -------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- | ------ | -------- | +| M1 | Health-report HTTPS listener | Start local TLS tracker with a temporary self-signed cert | Health report has `Ok` for the HTTPS tracker entry. | TODO | | +| M2 | Preserve HTTP listener health checking | Start ordinary local HTTP tracker | HTTP tracker entry remains `Ok`. | TODO | | + +### Acceptance Verification + +| AC ID | Status (`TODO`/`DONE`) | Evidence | +| ----- | ---------------------- | -------- | +| AC1 | TODO | | +| AC2 | TODO | | +| AC3 | TODO | | + +## Risks and Trade-offs + +- The direct service binding URL is the canonical source of transport. Avoid + reintroducing protocol inference from addresses or configuration fields. +- Self-signed certificates are suitable only for local manual verification; + production TLS trust policy is out of scope. + +## References + +- Related issue: #2041 +- Affected implementation: `packages/axum-http-server/src/server.rs` +- Local TLS workflow: `.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md` diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md index fa92422f3..975937c31 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md @@ -196,18 +196,18 @@ incidentally. Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. -| ID | Status | Task | Notes / Expected Output | -| --- | ----------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| T1 | DONE | Confirm prerequisites | #2036 and #2035 are merged; #2035's retained canonical identifiers must flow through registration. | -| T2 | DONE | Define generic registration metadata boundary | Approved: generic immutable metadata, optional health behavior, and tracker-owned typed role/identity metadata. | -| T3 | DONE | Extend registration and query API | `torrust-server-lib` 0.2.0 provides metadata, optional checks, insertion acknowledgement, and ordered snapshots. | -| T4 | DONE | Establish readiness semantics | Approved per-service insertion acknowledgement after bind; readiness consumers query exact expected identities. | -| T5 | DONE | Release and upgrade server library | Published `torrust-server-lib` 0.2.0; all tracker declarations and lockfile resolve the release. | -| T6 | DONE | Migrate tracker registrations | HTTP(S), UDP, REST API, and health-check API register canonical role and instance metadata. | -| T7 | DONE | Migrate health reporting | Health reports combine metadata binding/role with optional check execution and preserve JSON fields. | -| T8 | DONE | Migrate #1419 discovery helpers | Helpers await exact identities and query canonical roles; no bind-IP or map-order classification remains. | -| T9 | DONE | Add focused tests | Added health JSON compatibility and repeated port-zero identity-to-binding regressions. | -| T10 | IN_PROGRESS | Validate and record evidence | Focused compilation, tests, and linters passed; final full quality gate and manual scenarios remain. | +| ID | Status | Task | Notes / Expected Output | +| --- | ------ | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| T1 | DONE | Confirm prerequisites | #2036 and #2035 are merged; #2035's retained canonical identifiers must flow through registration. | +| T2 | DONE | Define generic registration metadata boundary | Approved: generic immutable metadata, optional health behavior, and tracker-owned typed role/identity metadata. | +| T3 | DONE | Extend registration and query API | `torrust-server-lib` 0.2.0 provides metadata, optional checks, insertion acknowledgement, and ordered snapshots. | +| T4 | DONE | Establish readiness semantics | Approved per-service insertion acknowledgement after bind; readiness consumers query exact expected identities. | +| T5 | DONE | Release and upgrade server library | Published `torrust-server-lib` 0.2.0; all tracker declarations and lockfile resolve the release. | +| T6 | DONE | Migrate tracker registrations | HTTP(S), UDP, REST API, and health-check API register canonical role and instance metadata. | +| T7 | DONE | Migrate health reporting | Health reports combine metadata binding/role with optional check execution and preserve JSON fields. | +| T8 | DONE | Migrate #1419 discovery helpers | Helpers await exact identities and query canonical roles; no bind-IP or map-order classification remains. | +| T9 | DONE | Add focused tests | Added health JSON compatibility and repeated port-zero identity-to-binding regressions. | +| T10 | DONE | Validate and record evidence | Focused/full pre-commit validation and manual HTTP/HTTPS/UDP/REST/health port-zero probes passed; the pre-existing HTTPS aggregate health-check defect is documented separately. | ## Progressive Verification Protocol @@ -242,6 +242,8 @@ For every code-changing task (T2-T9): - 2026-07-30 00:00 UTC - user and agent - Confirmed that the standalone server-library release, publication, and tracker upgrade are in scope. Approved generic immutable metadata, per-service insertion-acknowledgement readiness, and a concrete `0.2.0` server-library API plan. Reviewed compatibility with #2035, #2036, #2039, and #1419. - 2026-07-31 UTC - agent - Published `torrust-server-lib` 0.2.0 after a successful `cargo publish --dry-run`; pushed signed release commit `d17fdb1`. - 2026-07-31 UTC - agent - Migrated tracker registrations and health reporting to typed runtime metadata. Replaced #1419 bind-IP/count-based helper behavior with exact canonical identity readiness and role queries. Focused tests, workspace compilation, and `linter all` passed; final validation and manual evidence remain pending. +- 2026-07-31 UTC - agent - Manually started the tracker with repeated HTTP/UDP port-zero listeners plus REST and health APIs. Recorded distinct final bindings, canonical metadata correlation in startup logs, successful HTTP/UDP probes, and a compatible `Ok` health report in `evidence.md`. HTTPS remains manually unverified because the probe configuration omitted TLS material. +- 2026-07-31 UTC - agent - Manually started a second port-zero HTTP listener with a temporary self-signed TLS certificate. Direct HTTPS health probing passed and the registry health report preserved its HTTPS binding, HTTP-tracker role, and final address. The report's pre-existing HTTP-scheme health probe for HTTPS is documented as a separate draft bug. ## Acceptance Criteria @@ -272,11 +274,11 @@ For every code-changing task (T2-T9): ### Manual Verification Scenarios -| ID | Scenario | Expected Result | Status | Evidence | -| --- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------ | -------- | -| M1 | Start HTTP, HTTPS, REST API, health API, and UDP services with port zero. | Registry queries distinguish canonical role, instance identity, and final binding. | TODO | | -| M2 | Start repeated HTTP and UDP `0.0.0.0:0` configuration blocks. | Each final listener is correlated with the intended configuration instance. | TODO | | -| M3 | Run health checks after registry migration. | Health response preserves existing JSON fields and values. | TODO | | +| ID | Scenario | Expected Result | Status | Evidence | +| --- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------- | +| M1 | Start HTTP, HTTPS, REST API, health API, and UDP services with port zero. | Registry queries distinguish canonical role, instance identity, and final binding. | DONE | [evidence.md](evidence.md) — direct HTTPS probe passed; known aggregate health-check limitation recorded separately. | +| M2 | Start repeated HTTP and UDP `0.0.0.0:0` configuration blocks. | Each final listener is correlated with the intended configuration instance. | DONE | [evidence.md](evidence.md) | +| M3 | Run health checks after registry migration. | Health response preserves existing JSON fields and values. | DONE | [evidence.md](evidence.md) | ## References diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md index eed336143..1b1d9fb38 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md @@ -20,9 +20,9 @@ in the registry metadata migration. The issue's evidence protocol asks for manual baseline and post-change probes before each edit. This work started before those baselines were recorded, so no -manual baseline or manual post-change result is claimed retroactively. The -following are reproducible **automated** post-change checks only. M1-M3 remain -mandatory manual scenarios before this issue can be accepted. +manual baseline is available. The following are reproducible **automated** +post-change checks. The completed manual post-change probe is recorded below; +all M1-M3 services and identity-discovery scenarios are now covered. ### T3-T5 - Generic registry API and released crate @@ -51,6 +51,103 @@ mandatory manual scenarios before this issue can be accepted. - Comparison: Regression coverage now protects the metadata and readiness contracts introduced by this issue. - Result: `DONE`. +## Manual Post-Change Verification + +The manual baseline was not captured before implementation. The following +post-change probe was performed against a locally started tracker and records +the actual configuration, commands, and output. + +### M1-M3 - Port-zero service startup and health report + +#### Baseline + +- Not recorded before implementation. + +#### Post-change + +- Revisions: tracker commit `28b60a78` and follow-up invariant refactor + `e9515303`. +- Configuration: `.tmp/issue-2041-manual.toml` configured two HTTP and two UDP + listeners at `0.0.0.0:0`, a REST API at `127.0.0.1:18081`, and a health API + at `127.0.0.1:18080`. TLS/HTTPS was not configured for this probe. +- Start command: + `TORRUST_TRACKER_CONFIG_TOML_PATH="$PWD/.tmp/issue-2041-manual.toml" cargo run --bin torrust-tracker`. +- Startup output: distinct final bindings were assigned and logged with their + canonical metadata: `UdpTracker(0)=0.0.0.0:49980`, + `UdpTracker(1)=0.0.0.0:57094`, `HttpTracker(0)=0.0.0.0:59065`, + `HttpTracker(1)=0.0.0.0:44209`, `RestApi(0)=127.0.0.1:18081`, and + `HealthCheckApi(0)=127.0.0.1:18080`. +- Health query: `curl --fail --silent --show-error http://127.0.0.1:18080/health_check`. +- Observed health report: `status` was `Ok`. It reported five checkable + services in deterministic protocol/binding order: both UDP listeners with + `service_type="udp_tracker"`, both HTTP listeners with + `service_type="http_tracker"`, and the REST API with + `service_type="tracker_rest_api"`. Every report entry preserved matching + `service_binding` URL and `binding` socket address. The health API itself was + correctly omitted because it is metadata-only and must not recursively check + itself. +- Service probes: + - `curl --fail --silent --show-error http://127.0.0.1:59065/health_check` → `{"status":"Ok"}`. + - `curl --fail --silent --show-error http://127.0.0.1:44209/health_check` → `{"status":"Ok"}`. + - `curl --fail --silent --show-error http://127.0.0.1:18081/api/health_check` → `{"status":"Ok"}`. + - `cargo run -p torrust-tracker-client --bin tracker_client -- udp announce udp://127.0.0.1:49980/announce 0123456789abcdef0123456789abcdef01234567` → successful IPv4 announce response. + - The same announce command against `udp://127.0.0.1:57094/announce` → successful IPv4 announce response. +- Comparison: exact configuration identities were correlated with non-zero, + distinct final bindings without bind-IP classification, registry-map order, + or a startup delay. The health-report JSON retained the compatibility fields. +- Result: `DONE` for HTTP, UDP, REST API, health API, repeated port-zero + identity, and health compatibility. + +### M1 - HTTPS port-zero listener + +#### Baseline + +- Not recorded before implementation. + +#### Post-change + +- Revision: tracker commits `28b60a78` and `e9515303`. +- Temporary TLS material: generated a one-day self-signed RSA certificate and + key in the ignored `.tmp/` directory. The certificate contained SAN entries + for `localhost` and `127.0.0.1`, allowing a local direct probe. +- Temporary configuration: added a schema-2.0 + `[http_trackers.tsl_config]` section to the second repeated HTTP + `0.0.0.0:0` listener in `.tmp/issue-2041-manual.toml`. It referenced the + temporary certificate and key. The configuration was restored afterwards. +- Certificate command: + `openssl req -x509 -out .tmp/issue-2041-manual.crt -keyout .tmp/issue-2041-manual.key -newkey rsa:2048 -nodes -sha256 -days 1 -subj '/CN=localhost' -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1' -addext 'keyUsage=digitalSignature' -addext 'extendedKeyUsage=serverAuth'`. +- Start command: + `TORRUST_TRACKER_CONFIG_TOML_PATH="$PWD/.tmp/issue-2041-manual.toml" cargo run --bin torrust-tracker`. +- Startup output: `HttpTracker(0)` bound as + `http://0.0.0.0:58997`; `HttpTracker(1)` bound as + `https://0.0.0.0:60057`. The latter used the temporary certificate and key. + The same run also bound `UdpTracker(0)=0.0.0.0:42524`, + `UdpTracker(1)=0.0.0.0:54809`, `RestApi(0)=127.0.0.1:18081`, and + `HealthCheckApi(0)=127.0.0.1:18080`. +- Registry/health-report query: + `curl --fail --silent --show-error http://127.0.0.1:18080/health_check`. + The report contained the HTTPS entry with + `service_binding="https://0.0.0.0:60057/"`, + `binding="0.0.0.0:60057"`, and `service_type="http_tracker"`. +- Direct TLS probe: + `curl --fail --silent --show-error --insecure https://127.0.0.1:60057/health_check`. + The response was `{"status":"Ok"}`. +- Known unrelated limitation observed: the aggregate health report had + `status="Error"` for the HTTPS listener because + `packages/axum-http-server/src/server.rs` constructs the check URL with a + hard-coded `http://` scheme. Its report detail attempted + `http://0.0.0.0:60057/health_check` despite correctly exposing the service's + HTTPS binding. This is pre-existing behavior explicitly outside this issue's + scope; it is tracked by the draft issue + `docs/issues/drafts/fix-https-tracker-health-check-protocol.md`. +- Comparison: same-role repeated HTTP configuration instances were + distinguished by canonical `HttpTracker` identities and their separately + assigned final HTTP and HTTPS bindings. The direct TLS probe confirms that + the HTTPS listener itself was operational. +- Result: `DONE`. The registry-metadata behavior and the M1 service-startup + requirement are verified. The unrelated aggregate HTTPS health-check defect + is documented separately. + ## Scenario Record Template ```markdown diff --git a/project-words.txt b/project-words.txt index 863a4996f..61f3db162 100644 --- a/project-words.txt +++ b/project-words.txt @@ -111,6 +111,7 @@ Xtorrent Xunlei acgnxtracker actix +addext adduser adminadmin adrs From 8d3a44295bd9d0d054a43071223113a33c2ea109 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 Jul 2026 16:30:24 +0100 Subject: [PATCH 6/9] docs(issue-2041): clarify verification status --- ...fix-https-tracker-health-check-protocol.md | 38 ++++++++++--------- .../ISSUE.md | 25 ++++++------ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/docs/issues/drafts/fix-https-tracker-health-check-protocol.md b/docs/issues/drafts/fix-https-tracker-health-check-protocol.md index 02f52cb94..8bfef9a97 100644 --- a/docs/issues/drafts/fix-https-tracker-health-check-protocol.md +++ b/docs/issues/drafts/fix-https-tracker-health-check-protocol.md @@ -46,12 +46,13 @@ outside #2041's registry-metadata scope. - Derive the HTTP tracker health-check URL scheme from `ServiceBinding`. - Preserve HTTP tracker health-check behavior for ordinary HTTP listeners. - Add regression coverage for HTTPS listener health checks. -- Verify the aggregate health API reports `Ok` for an operational local HTTPS - tracker using a temporary self-signed certificate. +- Establish a test strategy for a TLS certificate trusted by the health-check + client, then verify the aggregate health API reports `Ok` for an operational + HTTPS tracker. ### Out of Scope -- Changing TLS certificate loading or certificate validation policy. +- Changing production TLS certificate loading or certificate validation policy. - Changing the health API response schema. - Changing runtime registry metadata or service identity behavior introduced by #2041. @@ -60,12 +61,13 @@ outside #2041's registry-metadata scope. Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. -| ID | Status | Task | Notes / Expected Output | -| --- | ------ | ----------------------------------------- | ------------------------------------------------------------------- | -| T1 | TODO | Add failing HTTPS health-check regression | Prove an HTTPS registration is not probed as plain HTTP. | -| T2 | TODO | Derive check URL from service binding | Use the binding's protocol and address without altering HTTP paths. | -| T3 | TODO | Validate health-report behavior | Aggregate report marks healthy local HTTP and HTTPS services `Ok`. | -| T4 | TODO | Document verification evidence | Record automated and manual results. | +| ID | Status | Task | Notes / Expected Output | +| --- | ------ | ----------------------------------------- | ---------------------------------------------------------------------------------------------- | +| T1 | TODO | Add failing HTTPS health-check regression | Prove an HTTPS registration is not probed as plain HTTP. | +| T2 | TODO | Derive check URL from service binding | Use the binding's protocol and address without altering HTTP paths. | +| T3 | TODO | Define trusted-TLS test strategy | Use test-only client trust or a trusted test certificate; do not weaken production validation. | +| T4 | TODO | Validate health-report behavior | Aggregate report marks healthy local HTTP and HTTPS services `Ok`. | +| T5 | TODO | Document verification evidence | Record automated and manual results. | ## Progress Tracking @@ -90,8 +92,9 @@ Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. - [ ] AC1: An HTTPS HTTP-tracker registration is health-checked through an `https://` URL, not an `http://` URL. -- [ ] AC2: An operational HTTPS listener yields a successful entry in the - aggregate health report. +- [ ] AC2: An operational HTTPS listener using a certificate trusted by the + health-check client yields a successful entry in the aggregate health + report. - [ ] AC3: Existing HTTP tracker health checks continue to pass. - [ ] `linter all` exits with code `0`. - [ ] Relevant tests pass. @@ -112,10 +115,10 @@ Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. Status values: `TODO`, `IN_PROGRESS`, `DONE`, `FAILED`, `BLOCKED`. -| ID | Scenario | Command/Steps | Expected Result | Status | Evidence | -| --- | -------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- | ------ | -------- | -| M1 | Health-report HTTPS listener | Start local TLS tracker with a temporary self-signed cert | Health report has `Ok` for the HTTPS tracker entry. | TODO | | -| M2 | Preserve HTTP listener health checking | Start ordinary local HTTP tracker | HTTP tracker entry remains `Ok`. | TODO | | +| ID | Scenario | Command/Steps | Expected Result | Status | Evidence | +| --- | -------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------- | ------ | -------- | +| M1 | Health-report HTTPS listener | Start local TLS tracker with a certificate trusted by the health-check client | Health report has `Ok` for the HTTPS tracker entry. | TODO | | +| M2 | Preserve HTTP listener health checking | Start ordinary local HTTP tracker | HTTP tracker entry remains `Ok`. | TODO | | ### Acceptance Verification @@ -129,8 +132,9 @@ Status values: `TODO`, `IN_PROGRESS`, `DONE`, `FAILED`, `BLOCKED`. - The direct service binding URL is the canonical source of transport. Avoid reintroducing protocol inference from addresses or configuration fields. -- Self-signed certificates are suitable only for local manual verification; - production TLS trust policy is out of scope. +- A self-signed certificate works for a direct `curl --insecure` probe, but + default `reqwest` validation rejects it. The implementation must not weaken + production certificate validation to make the test pass. ## References diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md index 975937c31..3b9a0524e 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md @@ -196,18 +196,18 @@ incidentally. Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. -| ID | Status | Task | Notes / Expected Output | -| --- | ------ | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| T1 | DONE | Confirm prerequisites | #2036 and #2035 are merged; #2035's retained canonical identifiers must flow through registration. | -| T2 | DONE | Define generic registration metadata boundary | Approved: generic immutable metadata, optional health behavior, and tracker-owned typed role/identity metadata. | -| T3 | DONE | Extend registration and query API | `torrust-server-lib` 0.2.0 provides metadata, optional checks, insertion acknowledgement, and ordered snapshots. | -| T4 | DONE | Establish readiness semantics | Approved per-service insertion acknowledgement after bind; readiness consumers query exact expected identities. | -| T5 | DONE | Release and upgrade server library | Published `torrust-server-lib` 0.2.0; all tracker declarations and lockfile resolve the release. | -| T6 | DONE | Migrate tracker registrations | HTTP(S), UDP, REST API, and health-check API register canonical role and instance metadata. | -| T7 | DONE | Migrate health reporting | Health reports combine metadata binding/role with optional check execution and preserve JSON fields. | -| T8 | DONE | Migrate #1419 discovery helpers | Helpers await exact identities and query canonical roles; no bind-IP or map-order classification remains. | -| T9 | DONE | Add focused tests | Added health JSON compatibility and repeated port-zero identity-to-binding regressions. | -| T10 | DONE | Validate and record evidence | Focused/full pre-commit validation and manual HTTP/HTTPS/UDP/REST/health port-zero probes passed; the pre-existing HTTPS aggregate health-check defect is documented separately. | +| ID | Status | Task | Notes / Expected Output | +| --- | ----------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| T1 | DONE | Confirm prerequisites | #2036 and #2035 are merged; #2035's retained canonical identifiers must flow through registration. | +| T2 | DONE | Define generic registration metadata boundary | Approved: generic immutable metadata, optional health behavior, and tracker-owned typed role/identity metadata. | +| T3 | DONE | Extend registration and query API | `torrust-server-lib` 0.2.0 provides metadata, optional checks, insertion acknowledgement, and ordered snapshots. | +| T4 | DONE | Establish readiness semantics | Approved per-service insertion acknowledgement after bind; readiness consumers query exact expected identities. | +| T5 | DONE | Release and upgrade server library | Published `torrust-server-lib` 0.2.0; all tracker declarations and lockfile resolve the release. | +| T6 | DONE | Migrate tracker registrations | HTTP(S), UDP, REST API, and health-check API register canonical role and instance metadata. | +| T7 | DONE | Migrate health reporting | Health reports combine metadata binding/role with optional check execution and preserve JSON fields. | +| T8 | DONE | Migrate #1419 discovery helpers | Helpers await exact identities and query canonical roles; no bind-IP or map-order classification remains. | +| T9 | DONE | Add focused tests | Added health JSON compatibility and repeated port-zero identity-to-binding regressions. | +| T10 | IN_PROGRESS | Validate and record evidence | Focused/full pre-commit validation and manual HTTP/HTTPS/UDP/REST/health port-zero probes passed; recorded per-task manual baseline/post-change evidence remains incomplete. | ## Progressive Verification Protocol @@ -244,6 +244,7 @@ For every code-changing task (T2-T9): - 2026-07-31 UTC - agent - Migrated tracker registrations and health reporting to typed runtime metadata. Replaced #1419 bind-IP/count-based helper behavior with exact canonical identity readiness and role queries. Focused tests, workspace compilation, and `linter all` passed; final validation and manual evidence remain pending. - 2026-07-31 UTC - agent - Manually started the tracker with repeated HTTP/UDP port-zero listeners plus REST and health APIs. Recorded distinct final bindings, canonical metadata correlation in startup logs, successful HTTP/UDP probes, and a compatible `Ok` health report in `evidence.md`. HTTPS remains manually unverified because the probe configuration omitted TLS material. - 2026-07-31 UTC - agent - Manually started a second port-zero HTTP listener with a temporary self-signed TLS certificate. Direct HTTPS health probing passed and the registry health report preserved its HTTPS binding, HTTP-tracker role, and final address. The report's pre-existing HTTP-scheme health probe for HTTPS is documented as a separate draft bug. +- 2026-07-31 UTC - agent - Independent completion review confirmed AC1-AC7 have code and focused-test support. T10 remains in progress because the recorded evidence does not provide manual baseline/post-change scenarios for every code-changing task, as required by AC9 and the progressive verification protocol. ## Acceptance Criteria From d7684051bb4ee23c6b7008615d57e85a5d8b71dc Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 Jul 2026 16:47:04 +0100 Subject: [PATCH 7/9] docs(logging): define structured runtime fields --- .../structured-runtime-logging/SKILL.md | 76 +++++++++++++++++++ .../ISSUE.md | 22 ++++++ 2 files changed, 98 insertions(+) create mode 100644 .github/skills/dev/logging/structured-runtime-logging/SKILL.md diff --git a/.github/skills/dev/logging/structured-runtime-logging/SKILL.md b/.github/skills/dev/logging/structured-runtime-logging/SKILL.md new file mode 100644 index 000000000..59b3443d6 --- /dev/null +++ b/.github/skills/dev/logging/structured-runtime-logging/SKILL.md @@ -0,0 +1,76 @@ +--- +name: structured-runtime-logging +description: "Use when adding or changing logs for runtime service identity, service startup, listener bindings, or tracing instrumentation. Prefer explicit structured tracing fields over Rust Debug-formatted metadata." +metadata: + author: torrust + version: "1.0" +--- + +# Structured Runtime Logging + +When logging runtime service identity, emit stable tracing fields instead of +recording `RuntimeServiceMetadata`, `ConfigurationInstanceId`, or related +structs through `Debug` formatting. + +Use the canonical fields: + +- `service_role` — the canonical role identifier, such as `http_tracker`. +- `instance_index` — the canonical zero-based configuration instance index. +- `service_binding` — the final protocol and bound socket address, after the + listener has successfully bound. + +## Correct Form + +Exclude metadata from automatic `#[instrument]` capture and add canonical +fields explicitly: + +```rust +#[instrument( + skip(metadata), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ) +)] +``` + +When a listener binds, log its final `service_binding` as an explicit field. + +```rust +tracing::info!( + service_binding = %service_binding.url(), + "Started HTTP tracker" +); +``` + +The resulting event has stable, queryable fields: + +```text +INFO start_job{service_role="http_tracker" instance_index=1}: Started HTTP tracker service_binding=http://0.0.0.0:7171 +``` + +## Incorrect Form + +Do not let `#[instrument]` capture the metadata parameter automatically, and +do not log the metadata with `?` or `%` formatting: + +```rust +#[instrument] +async fn start(metadata: RuntimeServiceMetadata) { + tracing::info!(?metadata, "Started HTTP tracker"); +} +``` + +This creates log output coupled to the Rust struct's `Debug` representation, +such as `metadata=RuntimeServiceMetadata { configuration_instance_id: ... }`. +It is not a stable, queryable log contract. + +For example, automatic span capture and `?metadata` produce implementation +detail in the log instead of canonical fields: + +```text +INFO start_job{idx=1 metadata=RuntimeServiceMetadata { configuration_instance_id: ConfigurationInstanceId { service_role: HttpTracker, instance_index: 1 } }}: Started HTTP tracker metadata=RuntimeServiceMetadata { configuration_instance_id: ConfigurationInstanceId { service_role: HttpTracker, instance_index: 1 } } +``` + +Do not make Rust field names, struct nesting, or a `Debug` implementation an +observability contract. diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md index 3b9a0524e..8b15d9c70 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md @@ -80,6 +80,10 @@ bind-IP classification and fixed registration delay. preserving the existing JSON contract. - Replace #1419 test helpers' bind-IP classification and fixed startup delay with role/identity-based registry discovery. +- Log runtime service identity as stable tracing fields rather than a debug + rendering of `RuntimeServiceMetadata`. +- Add a focused logging skill documenting the structured-field convention for + runtime identity. - Add progressive automatic and manual verification evidence for each code-changing task. @@ -192,6 +196,19 @@ incidentally. - **#1419:** replace raw-registry polling, bind-IP classification, and fixed registration delays with exact role/identity snapshot discovery. +### Runtime Identity Logging + +Runtime service identity must be emitted as stable tracing fields, not through +the `Debug` representation of `RuntimeServiceMetadata` or +`ConfigurationInstanceId`. Startup spans and events must record the canonical +`service_role` and `instance_index` explicitly. Events describing a successfully +bound listener must also record the final `service_binding`. + +This keeps logs machine-queryable and prevents internal Rust field names or +debug-format changes from becoming an accidental observability contract. This +is a logging convention, not an architectural decision; it is documented by +the `structured-runtime-logging` skill rather than an ADR. + ## Implementation Plan Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. @@ -208,6 +225,7 @@ Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. | T8 | DONE | Migrate #1419 discovery helpers | Helpers await exact identities and query canonical roles; no bind-IP or map-order classification remains. | | T9 | DONE | Add focused tests | Added health JSON compatibility and repeated port-zero identity-to-binding regressions. | | T10 | IN_PROGRESS | Validate and record evidence | Focused/full pre-commit validation and manual HTTP/HTTPS/UDP/REST/health port-zero probes passed; recorded per-task manual baseline/post-change evidence remains incomplete. | +| T11 | IN_PROGRESS | Structure runtime identity logging | Replace metadata debug capture with canonical tracing fields and add the focused logging convention skill. | ## Progressive Verification Protocol @@ -245,6 +263,7 @@ For every code-changing task (T2-T9): - 2026-07-31 UTC - agent - Manually started the tracker with repeated HTTP/UDP port-zero listeners plus REST and health APIs. Recorded distinct final bindings, canonical metadata correlation in startup logs, successful HTTP/UDP probes, and a compatible `Ok` health report in `evidence.md`. HTTPS remains manually unverified because the probe configuration omitted TLS material. - 2026-07-31 UTC - agent - Manually started a second port-zero HTTP listener with a temporary self-signed TLS certificate. Direct HTTPS health probing passed and the registry health report preserved its HTTPS binding, HTTP-tracker role, and final address. The report's pre-existing HTTP-scheme health probe for HTTPS is documented as a separate draft bug. - 2026-07-31 UTC - agent - Independent completion review confirmed AC1-AC7 have code and focused-test support. T10 remains in progress because the recorded evidence does not provide manual baseline/post-change scenarios for every code-changing task, as required by AC9 and the progressive verification protocol. +- 2026-07-31 UTC - user and agent - Added runtime identity logging to this PR's scope. Startup logs will expose canonical role, instance index, and final service binding as tracing fields rather than debug-rendered metadata. This convention is documented in a focused skill; no ADR is needed. ## Acceptance Criteria @@ -263,6 +282,8 @@ For every code-changing task (T2-T9): - [ ] AC8: Both repository validation suites pass. - [ ] AC9: Manual verification evidence is recorded for every code-changing task. +- [ ] AC10: Runtime service identity is emitted as explicit, stable tracing + fields rather than debug-formatted metadata. ## Verification Plan @@ -272,6 +293,7 @@ For every code-changing task (T2-T9): - Tracker registry/health-check tests. - `cargo test --test stats --test scaffold` after #1419 helper migration. - `linter all` in both repositories. +- Focused structured-log assertions for HTTP, UDP, and REST API startup paths. ### Manual Verification Scenarios From 148b6b09cfd4fe3ca05eb4f6dc1091b47a83d78e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 Jul 2026 17:33:54 +0100 Subject: [PATCH 8/9] feat(logging): structure runtime service identity --- .../ISSUE.md | 7 +-- .../evidence.md | 50 +++++++++++++++++++ packages/axum-http-server/src/server.rs | 13 ++++- packages/axum-rest-api-server/src/server.rs | 12 ++++- packages/udp-server/src/server/states.rs | 12 ++++- src/app.rs | 2 - src/bootstrap/jobs/health_check_api.rs | 8 +++ src/bootstrap/jobs/http_tracker.rs | 19 +++++-- src/bootstrap/jobs/tracker_apis.rs | 16 +++++- src/bootstrap/jobs/udp_tracker.rs | 10 ++-- 10 files changed, 130 insertions(+), 19 deletions(-) diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md index 8b15d9c70..03ccebee8 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md @@ -225,7 +225,7 @@ Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. | T8 | DONE | Migrate #1419 discovery helpers | Helpers await exact identities and query canonical roles; no bind-IP or map-order classification remains. | | T9 | DONE | Add focused tests | Added health JSON compatibility and repeated port-zero identity-to-binding regressions. | | T10 | IN_PROGRESS | Validate and record evidence | Focused/full pre-commit validation and manual HTTP/HTTPS/UDP/REST/health port-zero probes passed; recorded per-task manual baseline/post-change evidence remains incomplete. | -| T11 | IN_PROGRESS | Structure runtime identity logging | Replace metadata debug capture with canonical tracing fields and add the focused logging convention skill. | +| T11 | DONE | Structure runtime identity logging | Replaced metadata debug capture with canonical tracing fields and added the focused logging convention skill. | ## Progressive Verification Protocol @@ -264,6 +264,7 @@ For every code-changing task (T2-T9): - 2026-07-31 UTC - agent - Manually started a second port-zero HTTP listener with a temporary self-signed TLS certificate. Direct HTTPS health probing passed and the registry health report preserved its HTTPS binding, HTTP-tracker role, and final address. The report's pre-existing HTTP-scheme health probe for HTTPS is documented as a separate draft bug. - 2026-07-31 UTC - agent - Independent completion review confirmed AC1-AC7 have code and focused-test support. T10 remains in progress because the recorded evidence does not provide manual baseline/post-change scenarios for every code-changing task, as required by AC9 and the progressive verification protocol. - 2026-07-31 UTC - user and agent - Added runtime identity logging to this PR's scope. Startup logs will expose canonical role, instance index, and final service binding as tracing fields rather than debug-rendered metadata. This convention is documented in a focused skill; no ADR is needed. +- 2026-07-31 UTC - agent - Replaced automatic `RuntimeServiceMetadata` capture in HTTP, UDP, and REST startup spans with explicit `service_role` and `instance_index` fields. Added post-bind events with `service_binding` for HTTP, UDP, REST, and health APIs. Focused server, health integration, port-zero/scaffold, and lint checks passed. The manual probe must use Ctrl+C rather than `timeout`, because the tracker currently handles SIGINT but not SIGTERM; that behavior is outside this issue and belongs to the shutdown overhaul (#1488). ## Acceptance Criteria @@ -282,8 +283,8 @@ For every code-changing task (T2-T9): - [ ] AC8: Both repository validation suites pass. - [ ] AC9: Manual verification evidence is recorded for every code-changing task. -- [ ] AC10: Runtime service identity is emitted as explicit, stable tracing - fields rather than debug-formatted metadata. +- [x] AC10: Runtime service identity is emitted as explicit, stable tracing + fields rather than debug-formatted metadata. ## Verification Plan diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md index 1b1d9fb38..d321c4516 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md @@ -15,6 +15,7 @@ in the registry metadata migration. | T7 | NOT RECORDED | Automated PASS; manual TODO | Health contract tests assert preserved URL, binding, and service-type fields for HTTP, REST API, and UDP. | | T8 | NOT RECORDED | Automated PASS; manual TODO | Integration helpers query roles/identities instead of raw map entries or bind IPs. | | T9 | NOT RECORDED | Automated PASS; manual TODO | Focused server, health-contract, repeated-port-zero, and scaffold tests passed. | +| T11 | NOT RECORDED | Automated PASS; manual TODO | Startup spans use canonical tracing fields and post-bind events include the final service binding. | ## Automated Local Verification @@ -51,6 +52,55 @@ all M1-M3 services and identity-discovery scenarios are now covered. - Comparison: Regression coverage now protects the metadata and readiness contracts introduced by this issue. - Result: `DONE`. +### T11 - Structured runtime identity logging + +#### Baseline + +- Not recorded before implementation. + +#### Post-change + +- Revision: tracker branch `2041-migrate-runtime-service-registry-metadata`, + after documentation commit `d7684051`. +- Changed behavior: HTTP, UDP, and REST startup spans skip automatic + `RuntimeServiceMetadata` capture and explicitly emit `service_role` and + `instance_index`. HTTP, UDP, REST, and health API startup paths emit a + post-bind event with `service_binding`. +- Commands: `cargo test -p torrust-tracker-axum-http-server -p + torrust-tracker-udp-server -p torrust-tracker-axum-rest-api-server -p + torrust-tracker --lib`; `cargo test -p + torrust-tracker-axum-health-check-api-server --test integration`; `cargo test + --test aggregate_stats_port_zero --test scaffold`; `linter all`; and `git + diff --check`. +- Observed result: HTTP server (21 tests), REST API server (1 test), UDP server + (125 tests), tracker library (58 tests), health integration (7 tests), and + port-zero/scaffold integration tests passed. All linters and whitespace checks + passed. +- Shutdown note: an attempted `timeout 20s cargo run ...` probe did not stop + the tracker because `timeout` sends SIGTERM while the current tracker entry + point listens for SIGINT via Ctrl+C. `src/main.rs` and the relevant shutdown + orchestration are unchanged from `develop`; sending SIGINT stopped the process. + Manual logging verification must therefore start the tracker normally and use + Ctrl+C. SIGTERM support is outside #2041 and belongs to shutdown-overhaul + issue #1488. +- Manual command: `cargo run --quiet`, followed by Ctrl+C after startup. +- Observed startup output included explicit, queryable fields without a + `metadata=RuntimeServiceMetadata` rendering. Representative entries were: + `start_job{service_role="udp_tracker" instance_index=0}` followed by + `Started UDP tracker service_binding=udp://0.0.0.0:6868`; + `start_job{version=V1 service_role="http_tracker" instance_index=1}` followed + by `Started HTTP tracker service_binding=http://0.0.0.0:7171/`; and + `start_job{version=V1 service_role="tracker_rest_api" instance_index=0}` + followed by `Started tracker API service_binding=http://0.0.0.0:1212/`. The + health API emitted `service_role="health_check_api" instance_index=0 + service_binding=http://127.0.0.1:1313/`. +- Observed shutdown result: Ctrl+C logged `Torrust tracker shutting down ...`, + each managed job completed gracefully, and the process ended with `Torrust + tracker successfully shutdown.` +- Comparison: startup logging no longer depends on nested Rust `Debug` output + for metadata identity. The canonical fields and final binding are explicit. +- Result: `DONE`. + ## Manual Post-Change Verification The manual baseline was not captured before implementation. The following diff --git a/packages/axum-http-server/src/server.rs b/packages/axum-http-server/src/server.rs index 4209df9b7..3f27999fc 100644 --- a/packages/axum-http-server/src/server.rs +++ b/packages/axum-http-server/src/server.rs @@ -205,6 +205,13 @@ impl HttpServer { /// /// It would panic spawned HTTP server launcher cannot send the bound `SocketAddr` /// back to the main thread. + #[instrument( + skip(self, http_tracker_container, form, metadata), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ) + )] pub async fn start( self, http_tracker_container: Arc, @@ -226,10 +233,12 @@ impl HttpServer { let started = rx_start.await.expect("it should be able to start the service"); - let listen_url = started.service_binding; + let service_binding = started.service_binding; let binding = started.address; - form.register(ServiceRegistration::new(listen_url, metadata, Some(check_fn))) + tracing::info!(service_binding = %service_binding, "Started HTTP tracker"); + + form.register(ServiceRegistration::new(service_binding, metadata, Some(check_fn))) .await .expect("it should be able to register the started service"); diff --git a/packages/axum-rest-api-server/src/server.rs b/packages/axum-rest-api-server/src/server.rs index 5c519835c..8eefef748 100644 --- a/packages/axum-rest-api-server/src/server.rs +++ b/packages/axum-rest-api-server/src/server.rs @@ -124,7 +124,15 @@ impl ApiServer { /// # Panics /// /// It would panic if the bound socket address cannot be sent back to this starter. - #[instrument(skip(self, http_api_container, form, access_tokens), err, ret(Display, level = Level::INFO))] + #[instrument( + skip(self, http_api_container, form, metadata, access_tokens), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ), + err, + ret(Display, level = Level::INFO) + )] pub async fn start( self, http_api_container: Arc, @@ -149,6 +157,8 @@ impl ApiServer { let api_server = match rx_start.await { Ok(started) => { + tracing::info!(target: API_LOG_TARGET, service_binding = %started.service_binding, "Started tracker API"); + form.register(ServiceRegistration::new(started.service_binding, metadata, Some(check_fn))) .await .expect("it should be able to register the started service"); diff --git a/packages/udp-server/src/server/states.rs b/packages/udp-server/src/server/states.rs index 2b39c41b1..a96753785 100644 --- a/packages/udp-server/src/server/states.rs +++ b/packages/udp-server/src/server/states.rs @@ -62,7 +62,15 @@ impl Server { /// # Panics /// /// It panics if unable to receive the bound socket address from service. - #[instrument(skip(self, udp_tracker_core_container, udp_tracker_server_container, form), err, ret(Display, level = Level::INFO))] + #[instrument( + skip(self, udp_tracker_core_container, udp_tracker_server_container, form, metadata), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ), + err, + ret(Display, level = Level::INFO) + )] pub async fn start( self, udp_tracker_core_container: Arc, @@ -92,6 +100,8 @@ impl Server { let service_binding = started.service_binding; let local_addr = started.address; + tracing::info!(target: UDP_TRACKER_LOG_TARGET, service_binding = %service_binding, "Started UDP tracker"); + form.register(ServiceRegistration::new(service_binding, metadata, Some(Launcher::check))) .await .expect("it should be able to register the started service"); diff --git a/src/app.rs b/src/app.rs index 3edee5cdc..657146617 100644 --- a/src/app.rs +++ b/src/app.rs @@ -245,7 +245,6 @@ async fn start_udp_instance( let udp_tracker_server_container = app_container.udp_tracker_server_container(); let handle = udp_tracker::start_job( - idx, udp_tracker_container, udp_tracker_server_container, app_container.registar.give_form(), @@ -277,7 +276,6 @@ async fn start_http_instance( .expect("Could not create HTTP tracker container"); if let Some(handle) = http_tracker::start_job( - idx, http_tracker_container, app_container.registar.give_form(), RuntimeServiceMetadata::new(configuration_instance_id), diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index cb1bb8a8b..b7a7d6b77 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -59,6 +59,14 @@ pub async fn start_job(config: &HealthCheckApi, registar: Registar { + tracing::info!( + target: HEALTH_CHECK_API_LOG_TARGET, + service_role = ServiceRole::HealthCheckApi.as_str(), + instance_index = 0, + service_binding = %msg.service_binding, + "Started health check API" + ); + registar .give_form() .register(ServiceRegistration::new( diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index dc4e17aab..def7188be 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -31,9 +31,14 @@ use tracing::instrument; /// # Panics /// /// It would panic if the `config::HttpTracker` struct would contain inappropriate values. -#[instrument(skip(http_tracker_container, form))] +#[instrument( + skip(http_tracker_container, form, metadata), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ) +)] pub async fn start_job( - idx: usize, http_tracker_container: Arc, form: ServiceRegistrationForm, metadata: RuntimeServiceMetadata, @@ -42,7 +47,6 @@ pub async fn start_job( let socket = http_tracker_container.http_tracker_config.bind_address; tracing::info!( - instance_index = idx, bind_address = %socket, tracker_usage_statistics = http_tracker_container.http_tracker_config.tracker_usage_statistics, "Starting HTTP tracker instance" @@ -64,7 +68,13 @@ pub async fn start_job( } #[allow(clippy::async_yields_async)] -#[instrument(skip(socket, tls, http_tracker_container, form))] +#[instrument( + skip(socket, tls, http_tracker_container, form, metadata), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ) +)] async fn start_v1( socket: SocketAddr, tls: Option, @@ -120,7 +130,6 @@ mod tests { let version = Version::V1; start_job( - 0, http_tracker_container, Registar::default().give_form(), torrust_tracker_primitives::RuntimeServiceMetadata::new(torrust_tracker_primitives::ConfigurationInstanceId::new( diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index 10403b5bd..77eb0622e 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -54,7 +54,13 @@ pub struct ApiServerJobStarted(); /// It would panic if unable to send the `ApiServerJobStarted` notice. /// /// -#[instrument(skip(http_api_container, form))] +#[instrument( + skip(http_api_container, form, metadata), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ) +)] pub async fn start_job( http_api_container: Arc, form: ServiceRegistrationForm, @@ -81,7 +87,13 @@ pub async fn start_job( } #[allow(clippy::async_yields_async)] -#[instrument(skip(socket, tls, http_api_container, form, access_tokens))] +#[instrument( + skip(socket, tls, http_api_container, form, metadata, access_tokens), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ) +)] async fn start_v1( socket: SocketAddr, tls: Option, diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index 2bcca3678..967a05fcf 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -29,9 +29,14 @@ use tracing::instrument; /// It will panic if the task did not finish successfully. #[must_use] #[allow(clippy::async_yields_async)] -#[instrument(skip(udp_tracker_core_container, udp_tracker_server_container, form))] +#[instrument( + skip(udp_tracker_core_container, udp_tracker_server_container, form, metadata), + fields( + service_role = metadata.service_role().as_str(), + instance_index = metadata.configuration_instance_id().instance_index(), + ) +)] pub async fn start_job( - idx: usize, udp_tracker_core_container: Arc, udp_tracker_server_container: Arc, form: ServiceRegistrationForm, @@ -41,7 +46,6 @@ pub async fn start_job( let cookie_lifetime = udp_tracker_core_container.udp_tracker_config.cookie_lifetime; tracing::info!( - instance_index = idx, bind_address = %bind_to, tracker_usage_statistics = udp_tracker_core_container.udp_tracker_config.tracker_usage_statistics, "Starting UDP tracker instance" From b0bc51ed8803923d1f0292e8af05b78fc7622e1a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 Jul 2026 18:10:39 +0100 Subject: [PATCH 9/9] docs(issue-2041): format logging evidence --- .../ISSUE.md | 2 +- .../evidence.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md index 03ccebee8..82b9b4c3f 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/ISSUE.md @@ -284,7 +284,7 @@ For every code-changing task (T2-T9): - [ ] AC9: Manual verification evidence is recorded for every code-changing task. - [x] AC10: Runtime service identity is emitted as explicit, stable tracing - fields rather than debug-formatted metadata. + fields rather than debug-formatted metadata. ## Verification Plan diff --git a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md index d321c4516..70029c6b2 100644 --- a/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md +++ b/docs/issues/open/2041-migrate-runtime-service-registry-metadata/evidence.md @@ -15,7 +15,7 @@ in the registry metadata migration. | T7 | NOT RECORDED | Automated PASS; manual TODO | Health contract tests assert preserved URL, binding, and service-type fields for HTTP, REST API, and UDP. | | T8 | NOT RECORDED | Automated PASS; manual TODO | Integration helpers query roles/identities instead of raw map entries or bind IPs. | | T9 | NOT RECORDED | Automated PASS; manual TODO | Focused server, health-contract, repeated-port-zero, and scaffold tests passed. | -| T11 | NOT RECORDED | Automated PASS; manual TODO | Startup spans use canonical tracing fields and post-bind events include the final service binding. | +| T11 | NOT RECORDED | Automated PASS; manual TODO | Startup spans use canonical tracing fields and post-bind events include the final service binding. | ## Automated Local Verification @@ -67,11 +67,11 @@ all M1-M3 services and identity-discovery scenarios are now covered. `instance_index`. HTTP, UDP, REST, and health API startup paths emit a post-bind event with `service_binding`. - Commands: `cargo test -p torrust-tracker-axum-http-server -p - torrust-tracker-udp-server -p torrust-tracker-axum-rest-api-server -p - torrust-tracker --lib`; `cargo test -p - torrust-tracker-axum-health-check-api-server --test integration`; `cargo test - --test aggregate_stats_port_zero --test scaffold`; `linter all`; and `git - diff --check`. +torrust-tracker-udp-server -p torrust-tracker-axum-rest-api-server -p +torrust-tracker --lib`; `cargo test -p +torrust-tracker-axum-health-check-api-server --test integration`; `cargo test +--test aggregate_stats_port_zero --test scaffold`; `linter all`; and `git +diff --check`. - Observed result: HTTP server (21 tests), REST API server (1 test), UDP server (125 tests), tracker library (58 tests), health integration (7 tests), and port-zero/scaffold integration tests passed. All linters and whitespace checks @@ -93,10 +93,10 @@ all M1-M3 services and identity-discovery scenarios are now covered. `start_job{version=V1 service_role="tracker_rest_api" instance_index=0}` followed by `Started tracker API service_binding=http://0.0.0.0:1212/`. The health API emitted `service_role="health_check_api" instance_index=0 - service_binding=http://127.0.0.1:1313/`. +service_binding=http://127.0.0.1:1313/`. - Observed shutdown result: Ctrl+C logged `Torrust tracker shutting down ...`, each managed job completed gracefully, and the process ended with `Torrust - tracker successfully shutdown.` +tracker successfully shutdown.` - Comparison: startup logging no longer depends on nested Rust `Debug` output for metadata identity. The canonical fields and final binding are explicit. - Result: `DONE`.