From f3be08f6b690df79e5953708f11ad64a9ba94508 Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 26 Aug 2026 23:38:20 +0200 Subject: [PATCH] move otel_http_client from swarm-queue-client into swarm-controller --- Cargo.lock | 7 ++-- Cargo.toml | 4 +-- hive-jobq-metrics/src/lib.rs | 10 +++--- swarm-controller/Cargo.toml | 18 +++++++---- swarm-controller/src/main.rs | 1 + .../src/otel_http_client.rs | 32 +++++++++++-------- swarm-controller/src/vcs_metrics.rs | 24 +++++++------- swarm-queue-client/Cargo.toml | 18 ----------- swarm-queue-client/src/lib.rs | 8 ----- 9 files changed, 52 insertions(+), 70 deletions(-) rename {swarm-queue-client => swarm-controller}/src/otel_http_client.rs (76%) diff --git a/Cargo.lock b/Cargo.lock index 8b7f5a70..88c8eb6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4575,8 +4575,10 @@ version = "0.1.0" dependencies = [ "anyhow", "async-nats", + "async-trait", "axum", "base64", + "bytes", "forgejo-api", "futures-util", "hive-jobq", @@ -4584,6 +4586,7 @@ dependencies = [ "hive-jobq-wire", "hive-types", "hmac 0.13.0", + "http", "opentelemetry", "opentelemetry-http", "opentelemetry-otlp", @@ -4629,10 +4632,6 @@ name = "swarm-queue-client" version = "0.1.0" dependencies = [ "async-nats", - "async-trait", - "bytes", - "http", - "opentelemetry-http", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e20a2254..4733a5dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,8 +147,8 @@ opentelemetry-otlp = { version = "0.32", default-features = false, features = [ # The trait an OTLP exporter's HTTP transport is built on # (`opentelemetry_otlp::WithHttpConfig::with_http_client`) — pulled in # directly (not just transitively via `opentelemetry-otlp` above) by -# `swarm-queue-client`'s `otel-auth` feature, which implements the trait -# itself rather than using the crate's own blanket `reqwest`/ +# `swarm-controller`'s own `otel_http_client` module, which implements the +# trait itself rather than using the crate's own blanket `reqwest`/ # `reqwest-blocking` impls (this workspace's implementation needs to mint # a fresh bearer token per request, which no blanket impl can do). No # extra cargo features requested here for exactly that reason — the diff --git a/hive-jobq-metrics/src/lib.rs b/hive-jobq-metrics/src/lib.rs index 60373b1c..9c45d453 100644 --- a/hive-jobq-metrics/src/lib.rs +++ b/hive-jobq-metrics/src/lib.rs @@ -34,9 +34,9 @@ use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider}; /// Type-erases a caller's concrete [`HttpClient`] impl so [`spawn_exporter`] /// can accept "any client, or none" without this crate depending on any one -/// caller's concrete type (`swarm-controller`'s -/// `swarm_queue_client::otel_http_client::AuthenticatedHttpClient` today, -/// conceivably something else from a future caller) — the same genericity +/// caller's concrete type (`swarm-controller`'s own +/// `otel_http_client::AuthenticatedHttpClient` today, conceivably something +/// else from a future caller) — the same genericity /// this crate already has over `N`/`R`, applied to the one other caller- /// supplied thing it touches. /// @@ -84,8 +84,8 @@ const DEFAULT_INTERVAL: Duration = Duration::from_mins(1); /// before this parameter existed. `Some(client)` routes every export /// through that [`HttpClient`] instead, for a caller whose destination /// checks a bearer token per request rather than a static header -/// (`swarm-controller`'s case — see `swarm_queue_client::otel_http_client`'s -/// module doc for why a static header doesn't fit an expiring token). +/// (`swarm-controller`'s case — see its own `otel_http_client` module doc +/// for why a static header doesn't fit an expiring token). pub fn spawn_exporter( jobq: Arc>>, service_name: &str, diff --git a/swarm-controller/Cargo.toml b/swarm-controller/Cargo.toml index 8ea1ec6e..32d2ce10 100644 --- a/swarm-controller/Cargo.toml +++ b/swarm-controller/Cargo.toml @@ -84,16 +84,20 @@ swarm-authelia-bridge-sock.workspace = true # creation config are shared with the hive that writes it, so this end does # not get to declare them privately. # -# `otel-auth` for `vcs_metrics`'s `AuthenticatedHttpClient` — see that -# feature's own comment in `swarm-queue-client`'s Cargo.toml. -swarm-queue-client = { workspace = true, features = ["kv", "otel-auth"] } -# The `HttpClient` trait itself, so `main.rs` can name +swarm-queue-client = { workspace = true, features = ["kv"] } +# `otel_http_client.rs`'s `AuthenticatedHttpClient` — an +# `opentelemetry_http::HttpClient` impl authenticated with this crate's own +# `swarm-queue-client` identity. Lives in this crate rather than +# `swarm-queue-client` (mara's own call during review) since this daemon is +# its only caller; see that module's doc for the full rationale. `opentelemetry-http` +# is also needed directly (not just transitively) so `main.rs` can name # `Box` when handing # `vcs_metrics::authenticated_http_client()`'s output to -# `hive_jobq_metrics::spawn_exporter` — a transitive dependency (via -# `swarm-queue-client`'s `otel-auth` feature above) isn't enough to `use` -# it directly, Cargo requires a direct entry for that. +# `hive_jobq_metrics::spawn_exporter`. opentelemetry-http.workspace = true +async-trait.workspace = true +bytes.workspace = true +http.workspace = true tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 57bf51cc..ea09dfc9 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -43,6 +43,7 @@ use utoipa_axum::{router::OpenApiRouter, routes}; mod auth; mod config_pr; mod forge; +mod otel_http_client; mod status; mod vcs_metrics; mod webhook; diff --git a/swarm-queue-client/src/otel_http_client.rs b/swarm-controller/src/otel_http_client.rs similarity index 76% rename from swarm-queue-client/src/otel_http_client.rs rename to swarm-controller/src/otel_http_client.rs index ce526637..3429cba8 100644 --- a/swarm-queue-client/src/otel_http_client.rs +++ b/swarm-controller/src/otel_http_client.rs @@ -1,17 +1,24 @@ //! An [`opentelemetry_http::HttpClient`] that authenticates every request -//! with a fresh, audience-scoped bearer token minted from this crate's own -//! [`QueueConfig`] identity — "one identity per principal" applied to a -//! third destination (the queue connection and `swarm-authelia-bridge`'s -//! client, [`crate::mint_token_for`], are the other two). +//! with a fresh, audience-scoped bearer token minted from this daemon's own +//! [`swarm_queue_client::QueueConfig`] identity — "one identity per +//! principal" applied to a third destination (the queue connection and +//! `auth.rs`'s bridge client, `swarm_queue_client::mint_token_for`, are the +//! other two). +//! +//! Lives here, not in `swarm-queue-client` (mara's own call during review: +//! it does not belong in the queue-connect crate) — this daemon is the only +//! caller, and the module only *reuses* that crate's +//! `QueueConfig`/`mint_token_for_blocking`, it doesn't extend the +//! queue-connect contract those exist for. //! //! Exists for exactly one shape of caller: an OTLP metric exporter whose //! `PeriodicReader` drives export from a background thread with **no tokio //! reactor** (`opentelemetry-otlp`'s `reqwest-blocking-client` feature is //! chosen everywhere in this workspace for that reason — see -//! `swarm-controller::vcs_metrics`'s module doc). That constraint is why -//! this mints via [`crate::mint_token_for_blocking`] rather than the async -//! [`crate::mint_token_for`]: the latter needs a runtime to `.await` on, -//! which the calling thread does not have. +//! `vcs_metrics`'s module doc). That constraint is why this mints via +//! [`swarm_queue_client::mint_token_for_blocking`] rather than the async +//! `swarm_queue_client::mint_token_for`: the latter needs a runtime to +//! `.await` on, which the calling thread does not have. //! //! Per-request, not per-tick or cached: `opentelemetry-otlp` 0.32 exposes //! `WithHttpConfig::with_http_client`, a seam for a caller-supplied @@ -25,8 +32,7 @@ use async_trait::async_trait; use bytes::Bytes; use http::{Request, Response}; use opentelemetry_http::{HttpClient, HttpError}; - -use crate::{QueueConfig, mint_token_for_blocking}; +use swarm_queue_client::{QueueConfig, mint_token_for_blocking}; /// See the module doc. Wraps a plain `reqwest::blocking::Client` — deliberately /// not `opentelemetry_http`'s own blanket impl for that type (behind its @@ -37,9 +43,9 @@ pub struct AuthenticatedHttpClient { inner: reqwest::blocking::Client, cfg: QueueConfig, /// The audience requested at the token endpoint — see - /// [`crate::mint_token_for_blocking`]'s own doc (via - /// [`crate::mint_token_for`]'s) for why this has to be asked for - /// explicitly rather than left to whatever the identity's default is. + /// [`swarm_queue_client::mint_token_for_blocking`]'s own doc (via + /// `swarm_queue_client::mint_token_for`'s) for why this has to be asked + /// for explicitly rather than left to whatever the identity's default is. audience: String, } diff --git a/swarm-controller/src/vcs_metrics.rs b/swarm-controller/src/vcs_metrics.rs index 09f169ef..10d51723 100644 --- a/swarm-controller/src/vcs_metrics.rs +++ b/swarm-controller/src/vcs_metrics.rs @@ -118,8 +118,8 @@ fn build_provider(interval: Duration) -> Result { // `.with_http_client(..)`: unlike the hive tier's own exporters, this // one crosses a real trust boundary (the swarm-tier `otlp/swarm` // receiver checks a bearer token's audience, see - // `swarm_queue_client::otel_http_client`'s module doc) — a plain - // endpoint-only client would be refused at the receiver. + // `crate::otel_http_client`'s module doc) — a plain endpoint-only + // client would be refused at the receiver. let exporter = MetricExporter::builder() .with_http() .with_protocol(Protocol::HttpJson) @@ -135,8 +135,8 @@ fn build_provider(interval: Duration) -> Result { .build()) } -/// Build the [`swarm_queue_client::otel_http_client::AuthenticatedHttpClient`] -/// this exporter pushes through. +/// Build the [`crate::otel_http_client::AuthenticatedHttpClient`] this +/// exporter pushes through. /// /// The queue identity (`SWARM_CONTROLLER_OIDC_*`) is read fresh here rather /// than threaded in from a caller — by the time this runs, [`endpoint`] has @@ -160,8 +160,8 @@ fn build_provider(interval: Duration) -> Result { /// this unit for the same reason the forge client needs no special /// handling either) — a second, narrower trust bundle here would just be /// the same fact stated twice. -pub(crate) fn authenticated_http_client() --> Result { +pub(crate) fn authenticated_http_client() -> Result +{ let cfg = swarm_queue_client::QueueConfig::from_env("SWARM_CONTROLLER") .context("reading the queue identity this daemon authenticates its OTLP push with")? .ok_or_else(|| { @@ -175,13 +175,11 @@ pub(crate) fn authenticated_http_client() "SWARM_CONTROLLER_OTEL_AUDIENCE is unset, but OTEL_EXPORTER_OTLP_ENDPOINT is — the nix \ module sets both together", )?; - Ok( - swarm_queue_client::otel_http_client::AuthenticatedHttpClient::new( - reqwest::blocking::Client::new(), - cfg, - audience, - ), - ) + Ok(crate::otel_http_client::AuthenticatedHttpClient::new( + reqwest::blocking::Client::new(), + cfg, + audience, + )) } /// `service.name = swarm-controller` plus whatever the operator set in diff --git a/swarm-queue-client/Cargo.toml b/swarm-queue-client/Cargo.toml index e7640599..3798b01d 100644 --- a/swarm-queue-client/Cargo.toml +++ b/swarm-queue-client/Cargo.toml @@ -30,18 +30,6 @@ kv = ["async-nats/kv"] # `cargo check -p swarm-nats-auth` — no `kv` anywhere in that build — # surfaced it as `cannot find jetstream in async_nats`). notices = ["async-nats/jetstream"] -# OFF by default, same reasoning as `kv`/`notices` above: only a caller -# wiring an OTLP exporter's `HttpClient` seam to this crate's identity -# (today, `swarm-controller`'s `vcs_metrics`/`hive_jobq_metrics`) needs -# `opentelemetry_http`/`async-trait`/`bytes`/`http` pulled in — a plain -# queue-connect-only consumer (the auth-callout responder, a hive -# publishing its status) has no reason to carry them. -otel-auth = [ - "dep:opentelemetry-http", - "dep:async-trait", - "dep:bytes", - "dep:http", -] [dependencies] # Bare (no `kv`/`jetstream`) unless a consumer opts into the `kv` feature @@ -57,12 +45,6 @@ async-nats.workspace = true # feature's comment above), and `cargo check -p swarm-queue-client` alone # must not depend on what else is in the build. reqwest = { workspace = true, features = ["blocking"] } -# All four `optional = true`, gated behind the `otel-auth` feature above — -# see that feature's own comment for why. -opentelemetry-http = { workspace = true, optional = true } -async-trait = { workspace = true, optional = true } -bytes = { workspace = true, optional = true } -http = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true # A library, so its errors are a matchable enum rather than an opaque diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index 1a7acbbe..1165142e 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -164,14 +164,6 @@ pub fn chain(error: &dyn std::error::Error) -> String { /// which is the disagreement this module exists to prevent. pub mod status; -/// An [`opentelemetry_http::HttpClient`] impl that authenticates every -/// request with a fresh token from this crate's identity — see the -/// module's own doc for the full rationale. Behind the `otel-auth` -/// feature, same reasoning as `status`/`kv` above: only a caller wiring an -/// OTLP exporter to this identity needs the extra dependencies it pulls in. -#[cfg(feature = "otel-auth")] -pub mod otel_http_client; - /// The subject the swarm controller publishes on when the hive-wide knowledge /// repository has changed. One writer, many readers — every hive subscribes. ///