Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`Arc<Mutex<Breaker>>` + `Timer` shell; single per-host breaker; `now()`-only (no sleep,
no new dependency). 4-class outcome partition so `4xx`/`Auth`/unclassified errors neither
trip nor mask an outage. (ADR-0031 §5, ADR-0034.)
- `oath-adapter-net-http-api` `Tracing` resilience layer (Slice 1 PR 5) — the outermost
`Tracing<S, T>` service + `TracingLayer<T>` factory (`net-api::Layer`): one `info` span
per logical request (method, route, status, `ErrorKind`, latency, attempts), attached to
the inner future via `tracing::Instrument` so downstream events — including `Retry`'s new
per-attempt events — nest under it. Latency via `net-api::Timer` deltas; secret-safe by
construction (reads only method, `uri().path()` with the query dropped, status,
`ErrorKind`, and the clock — never headers or bodies); body-transparent. `Retry` now emits
`debug` per-attempt/backoff events and records the final attempt count onto the ambient
span (a no-op without a `Tracing` span). Routed to the ADR-0014 Telemetry plane. Adds the
`tracing` facade (runtime dep) + `tracing-subscriber` (dev-dep). (ADR-0031 §6, ADR-0014,
ADR-0034.)
- net-http construction-surface design refinements (ADR-0034 append-only
Amendments 2026-07-04, spec updated) — an absent `RateLimit<K>` directive now
**fails closed** (not "defaults to `Global`"), closing the last silent
Expand Down
68 changes: 68 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ serde = { version = "1", features = ["derive"] }
thiserror = "2"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features = false, features = [
"registry",
] }
proptest = "1"
serde_json = "1"

Expand Down
2 changes: 2 additions & 0 deletions crates/adapter/net/http/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ futures-util = { workspace = true }
http-body = { workspace = true }
http-body-util = { workspace = true }
pin-project-lite = { workspace = true }
tracing = { workspace = true }

[dev-dependencies]
tokio = { workspace = true }
oath-adapter-net-mock = { workspace = true }
tracing-subscriber = { workspace = true }

[lints]
workspace = true
4 changes: 4 additions & 0 deletions crates/adapter/net/http/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
//! factory, and the `CircuitBreakerConfig` thresholds
//! - [`timeout`] — the `Timeout` layer, its `TimeoutLayer` factory, and the
//! `RequestTimeout` per-request override
//! - [`trace`] — the `Tracing` layer and its `TracingLayer` factory (outermost;
//! one span per request, secret-safe, routed to the ADR-0014 Telemetry plane)
//!
//! The resilience layers, `stack`/`build` assembly, and backends land in later
//! slices. No async runtime, `hyper`, `reqwest`, or `serde` here.
Expand All @@ -33,6 +35,7 @@ pub mod rate_limit;
pub mod retry;
pub mod service;
pub mod timeout;
pub mod trace;

pub use auth::{Auth, AuthSource, NoAuth, SetHeaders};
pub use body::{BufferMode, Guarded, ResponseBody};
Expand All @@ -47,3 +50,4 @@ pub use rate_limit::{RateLimit, RateLimitLayer, RateScope, Scope};
pub use retry::{Retry, RetryConfig, RetryLayer, Retryable};
pub use service::Service;
pub use timeout::{RequestTimeout, Timeout, TimeoutLayer};
pub use trace::{Tracing, TracingLayer};
38 changes: 33 additions & 5 deletions crates/adapter/net/http/api/src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,24 +235,52 @@ where
let eligible = req.extensions().get::<Retryable>().is_some();
let max = self.cfg.max_attempts.get();
let mut attempt: u32 = 1;
// Whole-request clone per attempt: `http::Extensions` requires
// `Clone` on insert, so `Request<Bytes>` is `Clone` (Bytes is a
// cheap refcount bump; the directives ride along). `Auth`/`RateLimit`
// re-run inside this call, so credentials/budget refresh for free.
loop {
// Whole-request clone per attempt: `http::Extensions` requires
// `Clone` on insert, so `Request<Bytes>` is `Clone` (Bytes is a
// cheap refcount bump; the directives ride along). `Auth`/`RateLimit`
// re-run inside this call, so credentials/budget refresh for free.
// Whole-request clone per attempt (see the existing note above this loop).
let outcome = self.inner.call(req.clone()).await;
// Per-attempt telemetry — nests under Tracing's span when present,
// a no-op otherwise (ADR-0031 §6). `debug`: drill-down, pay-per-use.
match &outcome {
Ok(resp) => tracing::event!(
tracing::Level::DEBUG,
attempt = u64::from(attempt),
status = u64::from(resp.status().as_u16()),
"http.attempt"
),
Err(e) => tracing::event!(
tracing::Level::DEBUG,
attempt = u64::from(attempt),
error_kind = crate::trace::kind_label(e.kind()),
"http.attempt"
),
}
let retry = eligible
&& attempt < max
&& match &outcome {
Err(e) => is_transient(e.kind()),
Ok(resp) => resp.status().is_server_error(), // 5xx only; 429 is 4xx
};
if !retry {
// Record the final attempt count onto the current span — the
// "http.request" span when composed under Tracing; a no-op
// otherwise (no active span / the field is absent).
tracing::Span::current().record("attempts", u64::from(attempt));
return outcome; // success, non-retryable outcome, or attempts exhausted
}
drop(outcome); // release the prior response's Guarded permit before waiting
let ceil = backoff_ceiling(self.cfg.base, self.cfg.cap, attempt);
self.timer.sleep(self.rng.duration_in(ceil)).await;
let delay = self.rng.duration_in(ceil);
tracing::event!(
tracing::Level::DEBUG,
attempt = u64::from(attempt),
backoff_us = u64::try_from(delay.as_micros()).unwrap_or(u64::MAX),
"http.retry.backoff"
);
self.timer.sleep(delay).await;
attempt += 1;
}
}
Expand Down
Loading