{"id":50454116,"url":"https://github.com/mizcausevic-dev/request-shadow-rs","last_synced_at":"2026-06-01T01:05:39.901Z","repository":{"id":358081296,"uuid":"1239331854","full_name":"mizcausevic-dev/request-shadow-rs","owner":"mizcausevic-dev","description":"Async request mirroring with sampling, divergence detection, and structured response diffs. The SRE primitive for safe migrations. Part of the Platform Reliability Stack.","archived":false,"fork":false,"pushed_at":"2026-05-15T15:53:49.000Z","size":16,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-15T18:20:16.008Z","etag":null,"topics":["async","diff","migration","mirror","reliability","rust","shadow","sre","tokio"],"latest_commit_sha":null,"homepage":"https://kineticgain.com/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mizcausevic-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-15T01:52:07.000Z","updated_at":"2026-05-15T15:53:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mizcausevic-dev/request-shadow-rs","commit_stats":null,"previous_names":["mizcausevic-dev/request-shadow-rs"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/mizcausevic-dev/request-shadow-rs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mizcausevic-dev%2Frequest-shadow-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mizcausevic-dev%2Frequest-shadow-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mizcausevic-dev%2Frequest-shadow-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mizcausevic-dev%2Frequest-shadow-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mizcausevic-dev","download_url":"https://codeload.github.com/mizcausevic-dev/request-shadow-rs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mizcausevic-dev%2Frequest-shadow-rs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33755379,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","diff","migration","mirror","reliability","rust","shadow","sre","tokio"],"created_at":"2026-06-01T01:05:39.831Z","updated_at":"2026-06-01T01:05:39.892Z","avatar_url":"https://github.com/mizcausevic-dev.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# request-shadow\n\n[![CI](https://github.com/mizcausevic-dev/request-shadow-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/mizcausevic-dev/request-shadow-rs/actions/workflows/ci.yml)\n[![Rust](https://img.shields.io/badge/rust-1.86%2B-orange)](https://www.rust-lang.org/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\n**Async request mirroring with sampling, divergence detection, and structured response diffs.** The SRE primitive for migrations: send the same request to the production service AND a candidate, compare the responses, return the production one to the client while you collect divergence telemetry.\n\n```rust\nuse std::sync::Arc;\nuse request_shadow::{Backend, ResponseRecord, ShadowConfig, Shadower};\n# use async_trait::async_trait;\n# #[derive(Clone)] struct Mock(ResponseRecord);\n# #[async_trait]\n# impl Backend for Mock {\n#     async fn call(\u0026self, _input: \u0026[u8]) -\u003e Result\u003cResponseRecord, request_shadow::ShadowError\u003e { Ok(self.0.clone()) }\n# }\n# async fn demo() -\u003e Result\u003c(), request_shadow::ShadowError\u003e {\nlet primary  = Arc::new(Mock(ResponseRecord::ok(b\"prod\".to_vec())));\nlet shadow   = Arc::new(Mock(ResponseRecord::ok(b\"prod\".to_vec())));\nlet shadower = Shadower::new(primary, shadow, ShadowConfig::full_sample());\n\nlet outcome = shadower.call(b\"hello\").await?;\nassert!(outcome.primary.ok);\nassert!(outcome.divergence.is_none()); // bytes match\n# Ok(()) }\n```\n\n---\n\n## Why a small crate\n\nEvery service mesh has a knob for traffic shadowing — Linkerd, Istio, AWS App Mesh. They're great when you own the mesh. They're useless when the migration is **in-process**: binary library swap, codec change, JSON-vs-protobuf swap, ORM cutover.\n\nThis crate gives you the same shape as a 30-line Tokio task:\n\n1. **Backend trait** — abstracts the call. Implement once per transport.\n2. **Shadower** — fires both legs concurrently, returns the primary record + an optional divergence.\n3. **Divergence** — typed diff: status / headers / body each get their own bool + summary.\n4. **Sampling** — sticky on the input bytes (SHA-256 mod 100). The same input always gets the same yes/no for a given rate.\n5. **Timeout for the shadow leg only** — never blocks the primary call.\n\n---\n\n## Pieces\n\n| Type | Purpose |\n| --- | --- |\n| `Backend` | `async fn call(\u0026self, input: \u0026[u8]) -\u003e Result\u003cResponseRecord, _\u003e`. Implement over `reqwest::Client`, your gRPC client, or anything else. |\n| `ResponseRecord` | Backend output: `ok`, `status`, sorted `headers`, opaque `body`. |\n| `ShadowConfig` | Sampling rate, shadow timeout, list of fields to ignore in the diff. |\n| `Shadower` | The composer. Cheap to clone (both backends are `Arc`). |\n| `ShadowOutcome` | What `Shadower::call` returns: `primary`, optional `shadow`, optional `divergence`, plus reason flags. |\n| `Divergence` | `status: Option\u003c(u16, u16)\u003e`, `headers: Option\u003cHeaderDiff\u003e`, `body: Option\u003cBodyDiff\u003e`. Each piece is `None` when that aspect matches or was ignored. |\n| `DivergenceLog` | Bounded ring buffer of recent divergences for operator inspection. |\n\n---\n\n## Sampling\n\nSet `sample_rate(N)` to mirror N% of requests. Bucketing is sticky over the input bytes:\n\n```rust\nuse request_shadow::ShadowConfig;\nlet cfg = ShadowConfig::full_sample().sample_rate(10);\nassert_eq!(cfg.should_shadow(b\"req-key-1\"), cfg.should_shadow(b\"req-key-1\"));\n```\n\nSame key, same answer. Deterministic. No RNG dep.\n\n---\n\n## Composes with\n\n- **[reliability-toolkit-rs](https://github.com/mizcausevic-dev/reliability-toolkit-rs)** — wrap the shadow `Backend` in a [`CircuitBreaker`](https://github.com/mizcausevic-dev/reliability-toolkit-rs#circuitbreaker--closed--open--halfopen) so a flaky candidate never bleeds into the primary path.\n- **[slo-budget-tracker](https://github.com/mizcausevic-dev/slo-budget-tracker)** — record every divergence against an SLO so you can answer \"is the candidate good enough to promote?\"\n- **[feature-flag-rs](https://github.com/mizcausevic-dev/feature-flag-rs)** — flip the sampling rate from a remote config push without redeploying.\n\n---\n\n## Run the example\n\n```bash\ncargo run --example inproc\n```\n\nBuilds a primary \"v1\" backend and a \"v2\" candidate, fires three requests, prints the primary body + the structured divergence each time.\n\n---\n\n## Bench\n\n```bash\ncargo bench\n```\n\nThe bundled bench times `Divergence::compare` on a 4KB equal body so you can spot regressions in the diff path.\n\n---\n\n## Tests\n\n```bash\ncargo test --all-targets\ncargo test --doc\ncargo clippy --all-targets -- -Dwarnings\ncargo fmt --all -- --check\n```\n\nCI matrix: `stable`, `beta`, `1.86.0` (MSRV). Eleven async tests cover identical responses, body/status/header divergence, ignore-fields, sampling at 0%, sticky sampling, timeout handling, shadow-backend failures, and the divergence log.\n\n---\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmizcausevic-dev%2Frequest-shadow-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmizcausevic-dev%2Frequest-shadow-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmizcausevic-dev%2Frequest-shadow-rs/lists"}