autumn_web/shadow/mod.rs
1//! Live-traffic shadow mirroring and primary-vs-shadow response diffing
2//! (issue #1653).
3//!
4//! Canary, blue/green, and rolling deploys all answer the same question — *are
5//! the aggregate metrics still healthy?* — and all of them route **real**
6//! traffic to the new build to answer it. None of them catches the regression
7//! that returns `200 OK` with a dropped field, a reordered list, or an
8//! off-by-one total, because nothing ever compares two responses to the same
9//! request. This module does.
10//!
11//! ```text
12//! ┌──────────────────┐
13//! client ──request──▶ │ live build │ ──response──▶ client
14//! └────────┬─────────┘ │
15//! │ (copy) │ (tee)
16//! ┌────────▼─────────┐ ┌────▼─────┐
17//! │ candidate build │──▶│ differ │──▶ /actuator/shadow
18//! └──────────────────┘ └──────────┘ + metrics
19//! ```
20//!
21//! The candidate's response reaches the differ and nothing else. It is never a
22//! `Response`, never touches the primary's state, and never delays the client.
23//!
24//! # Scope of this slice
25//!
26//! Idempotent (`GET`/`HEAD`) traffic only, against an operator-provided shadow
27//! target. Mirroring mutating methods requires virtualizing the candidate's
28//! effects — DB writes, outbound HTTP, `#[job]` enqueues — which is the
29//! deliberate follow-up slice. The method allowlist is therefore a constant,
30//! not a config key.
31//!
32//! # Wiring
33//!
34//! Configured through [`ShadowConfig`](crate::shadow::ShadowConfig) (`[shadow]` in
35//! `autumn.toml`) and
36//! assembled into the ingress stack by the framework router; nothing here needs
37//! to be called by hand. See `docs/guide/staged-deploys.md`.
38
39// Private modules behind a curated re-export list, matching
40// `crate::middleware`: the module split is an implementation detail, and
41// everything below is deliberate public surface. `transport` stays public
42// because a third party may want to implement [`ShadowTransport`] against a
43// non-HTTP candidate.
44pub(crate) mod config;
45pub(crate) mod diff;
46pub(crate) mod layer;
47pub(crate) mod registry;
48pub(crate) mod sample;
49pub mod transport;
50
51pub use config::ShadowConfig;
52pub use diff::{
53 Comparison, Divergence, DivergenceKind, NormalizedBody, ResponseFacts, TRUNCATION_MARKER,
54 compare, normalize_body, redact_path_and_query, status_class,
55};
56pub use layer::{
57 COMPARISONS_METRIC, DIVERGENCES_METRIC, MirrorSettings, ShadowMirrorLayer, ShadowMirrorService,
58};
59pub use registry::{
60 DivergenceRecord, LabelledCount, OVERFLOW_ROUTE_LABEL, Recorded, RequestContext,
61 ShadowRegistry, ShadowSnapshot, ShadowStats,
62};
63pub use sample::{
64 MIRRORABLE_METHODS, MirrorDecision, MirrorSelector, SHADOW_HEADER, SHADOW_HEADER_VALUE,
65 SkipReason, roll_from,
66};
67#[cfg(feature = "http-client")]
68pub use transport::HttpShadowTransport;
69pub use transport::{ShadowError, ShadowFuture, ShadowRequest, ShadowTransport};
70
71/// What `{actuator-prefix}/shadow` needs to report on a mirror run: the
72/// registry plus the two facts that live in config rather than in it.
73///
74/// Installed into [`crate::AppState`]'s runtime extension map when the mirror
75/// layer is assembled, so a replica with mirroring switched off simply has no
76/// handle and the endpoint reports a disabled mirror.
77#[derive(Clone, Debug)]
78pub struct ShadowHandle {
79 /// Shared counters and the bounded divergence ring.
80 pub registry: ShadowRegistry,
81 /// Whether mirroring is switched on for this replica.
82 pub enabled: bool,
83 /// The configured candidate target.
84 pub target: Option<String>,
85}
86
87impl ShadowHandle {
88 /// Build the payload the actuator endpoint publishes.
89 #[must_use]
90 pub fn snapshot(&self) -> ShadowSnapshot {
91 self.registry.snapshot(self.enabled, self.target.as_deref())
92 }
93
94 /// The payload for a replica that never assembled a mirror.
95 #[must_use]
96 pub fn disabled_snapshot() -> ShadowSnapshot {
97 ShadowSnapshot::disabled()
98 }
99
100 /// A handle for a replica that configured `[shadow]` but could not assemble
101 /// a mirror — today, a build without the `http-client` feature, which has
102 /// no way to dial the candidate.
103 ///
104 /// Installed so the actuator can distinguish "configured but inert here"
105 /// from "never configured": both report `enabled: false`, but only this one
106 /// reports the target the operator asked for.
107 #[must_use]
108 pub fn inactive(target: impl Into<String>) -> Self {
109 Self {
110 registry: ShadowRegistry::new(1),
111 enabled: false,
112 target: Some(target.into()),
113 }
114 }
115}