Skip to content

Commit 2d69f73

Browse files
committed
Add observability, bench, presets & replay support
Introduce observability and benchmarking workflows and CLI ergonomics: add new `bench` and `observability` commands, surface observability assets, and wire a benchmark runner. Expand the `doctor` command to scan workspace signals (health, shutdown, tracing, rate-limit, body limits, etc.), emit structured checks/warnings/failures, and support a --strict mode. Add project presets and template wiring: introduce ProjectPreset (prod-api, ai-api, realtime-api), recommended feature bundles, CLI --preset support for `cargo rustapi new`, and related tests. Enable the `replay` feature by default for the cargo CLI and export replay, streaming multipart, and RateLimitStrategy types in the public API files. Library tweaks: expose RateLimitStrategy and extend rate-limit internals to support multiple strategies; clarify multipart usage (drop/consume previous field before next). Update docs/README examples and CLI README entries, add tests for new commands, and update .gitignore to ignore markdown files.
1 parent 6d57471 commit 2d69f73

22 files changed

Lines changed: 1563 additions & 336 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ docs/PRODUCTION_CHECKLIST.md
1818
/.github/instructions
1919
/.github/prompts
2020
/.github/skills
21+
*.md
22+
tasks.md

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,32 @@ All error responses include a unique `error_id` (`err_{uuid}`) for log correlati
6868
Record and replay HTTP request/response pairs for production debugging:
6969

7070
```rust
71+
use rustapi_rs::extras::replay::{ReplayConfig, ReplayLayer};
72+
use rustapi_rs::prelude::*;
73+
7174
RustApi::new()
72-
.layer(ReplayLayer::new(store, config))
73-
.run("0.0.0.0:8080").await;
75+
.layer(
76+
ReplayLayer::new(
77+
ReplayConfig::new()
78+
.enabled(true)
79+
.admin_token("local-replay-token"),
80+
),
81+
)
82+
.run("0.0.0.0:8080")
83+
.await?;
7484
```
7585

7686
```sh
77-
cargo rustapi replay list
78-
cargo rustapi replay run <id> --target http://localhost:8080
79-
cargo rustapi replay diff <id> --target http://staging
87+
cargo rustapi replay list -t local-replay-token
88+
cargo rustapi replay run <id> -t local-replay-token --target http://localhost:8080
89+
cargo rustapi replay diff <id> -t local-replay-token --target http://staging
8090
```
8191

8292
- Middleware-based recording; no application code changes
8393
- Sensitive header redaction; disabled by default
8494
- In-memory (dev) or filesystem (production) storage with TTL
8595
- `ReplayClient` for programmatic test automation
96+
- Full incident workflow: [`docs/cookbook/src/recipes/replay.md`](docs/cookbook/src/recipes/replay.md)
8697

8798
### Dual-Stack HTTP/1.1 + HTTP/3
8899

api/public/rustapi-rs.all-features.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ pub use rustapi_rs::MethodRouter
4242
pub use rustapi_rs::Multipart
4343
pub use rustapi_rs::MultipartConfig
4444
pub use rustapi_rs::MultipartField
45+
pub use rustapi_rs::StreamingMultipart
46+
pub use rustapi_rs::StreamingMultipartField
4547
pub use rustapi_rs::NoContent
4648
pub use rustapi_rs::OAuth2Client
4749
pub use rustapi_rs::OAuth2Config
@@ -56,6 +58,7 @@ pub use rustapi_rs::Path
5658
pub use rustapi_rs::Provider
5759
pub use rustapi_rs::Query
5860
pub use rustapi_rs::RateLimitLayer
61+
pub use rustapi_rs::RateLimitStrategy
5962
pub use rustapi_rs::Redirect
6063
pub use rustapi_rs::RedisSessionStore
6164
pub use rustapi_rs::Request
@@ -171,6 +174,8 @@ pub use rustapi_rs::core::MethodRouter
171174
pub use rustapi_rs::core::Multipart
172175
pub use rustapi_rs::core::MultipartConfig
173176
pub use rustapi_rs::core::MultipartField
177+
pub use rustapi_rs::core::StreamingMultipart
178+
pub use rustapi_rs::core::StreamingMultipartField
174179
pub use rustapi_rs::core::NoContent
175180
pub use rustapi_rs::core::Path
176181
pub use rustapi_rs::core::Query
@@ -268,8 +273,27 @@ pub use rustapi_rs::extras::oauth2::TokenResponse
268273
pub use rustapi_rs::extras::oauth2::oauth2
269274
pub mod rustapi_rs::extras::rate_limit
270275
pub use rustapi_rs::extras::rate_limit::RateLimitLayer
276+
pub use rustapi_rs::extras::rate_limit::RateLimitStrategy
271277
pub use rustapi_rs::extras::rate_limit::rate_limit
272278
pub mod rustapi_rs::extras::replay
279+
pub use rustapi_rs::extras::replay::FsReplayStore
280+
pub use rustapi_rs::extras::replay::FsReplayStoreConfig
281+
pub use rustapi_rs::extras::replay::InMemoryReplayStore
282+
pub use rustapi_rs::extras::replay::RecordedRequest
283+
pub use rustapi_rs::extras::replay::RecordedResponse
284+
pub use rustapi_rs::extras::replay::ReplayAdminAuth
285+
pub use rustapi_rs::extras::replay::ReplayClient
286+
pub use rustapi_rs::extras::replay::ReplayClientError
287+
pub use rustapi_rs::extras::replay::ReplayConfig
288+
pub use rustapi_rs::extras::replay::ReplayEntry
289+
pub use rustapi_rs::extras::replay::ReplayId
290+
pub use rustapi_rs::extras::replay::ReplayLayer
291+
pub use rustapi_rs::extras::replay::ReplayMeta
292+
pub use rustapi_rs::extras::replay::ReplayQuery
293+
pub use rustapi_rs::extras::replay::ReplayStore
294+
pub use rustapi_rs::extras::replay::ReplayStoreError
295+
pub use rustapi_rs::extras::replay::ReplayStoreResult
296+
pub use rustapi_rs::extras::replay::RetentionJob
273297
pub use rustapi_rs::extras::replay::replay
274298
pub mod rustapi_rs::extras::retry
275299
pub use rustapi_rs::extras::retry::retry
@@ -354,6 +378,8 @@ pub use rustapi_rs::prelude::Message
354378
pub use rustapi_rs::prelude::Multipart
355379
pub use rustapi_rs::prelude::MultipartConfig
356380
pub use rustapi_rs::prelude::MultipartField
381+
pub use rustapi_rs::prelude::StreamingMultipart
382+
pub use rustapi_rs::prelude::StreamingMultipartField
357383
pub use rustapi_rs::prelude::Negotiate
358384
pub use rustapi_rs::prelude::NoContent
359385
pub use rustapi_rs::prelude::OAuth2Client
@@ -364,6 +390,7 @@ pub use rustapi_rs::prelude::PkceVerifier
364390
pub use rustapi_rs::prelude::Provider
365391
pub use rustapi_rs::prelude::Query
366392
pub use rustapi_rs::prelude::RateLimitLayer
393+
pub use rustapi_rs::prelude::RateLimitStrategy
367394
pub use rustapi_rs::prelude::Redirect
368395
pub use rustapi_rs::prelude::RedisSessionStore
369396
pub use rustapi_rs::prelude::Request

api/public/rustapi-rs.default.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub use rustapi_rs::MethodRouter
2525
pub use rustapi_rs::Multipart
2626
pub use rustapi_rs::MultipartConfig
2727
pub use rustapi_rs::MultipartField
28+
pub use rustapi_rs::StreamingMultipart
29+
pub use rustapi_rs::StreamingMultipartField
2830
pub use rustapi_rs::NoContent
2931
pub use rustapi_rs::Path
3032
pub use rustapi_rs::Query
@@ -97,6 +99,8 @@ pub use rustapi_rs::core::MethodRouter
9799
pub use rustapi_rs::core::Multipart
98100
pub use rustapi_rs::core::MultipartConfig
99101
pub use rustapi_rs::core::MultipartField
102+
pub use rustapi_rs::core::StreamingMultipart
103+
pub use rustapi_rs::core::StreamingMultipartField
100104
pub use rustapi_rs::core::NoContent
101105
pub use rustapi_rs::core::Path
102106
pub use rustapi_rs::core::Query
@@ -165,6 +169,8 @@ pub use rustapi_rs::prelude::KeepAlive
165169
pub use rustapi_rs::prelude::Multipart
166170
pub use rustapi_rs::prelude::MultipartConfig
167171
pub use rustapi_rs::prelude::MultipartField
172+
pub use rustapi_rs::prelude::StreamingMultipart
173+
pub use rustapi_rs::prelude::StreamingMultipartField
168174
pub use rustapi_rs::prelude::NoContent
169175
pub use rustapi_rs::prelude::Path
170176
pub use rustapi_rs::prelude::Query

crates/cargo-rustapi/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ assert_cmd = "2.0"
5353
predicates = "3.1"
5454

5555
[features]
56-
default = ["remote-spec"]
56+
default = ["remote-spec", "replay"]
5757
remote-spec = ["dep:reqwest"]
5858
replay = ["dep:reqwest"]

crates/cargo-rustapi/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@ cargo install cargo-rustapi
1818
| `cargo rustapi new <name>` | Create a new project with the perfect directory structure |
1919
| `cargo rustapi run` | Run the development server |
2020
| `cargo rustapi run --reload` | Run with hot-reload (auto-rebuild on file changes) |
21+
| `cargo rustapi bench` | Run the repository benchmark workflow via `scripts/bench.ps1` |
22+
| `cargo rustapi doctor [--strict]` | Validate toolchain availability and check project signals against the production checklist |
23+
| `cargo rustapi observability [--check]` | Surface observability docs, benchmark assets, and recommended baseline features |
24+
| `cargo rustapi new <name> --preset <preset>` | Start from opinionated `prod-api`, `ai-api`, or `realtime-api` feature bundles |
2125
| `cargo rustapi generate resource <name>` | Scaffold a new API resource (Model + Handlers + Tests) |
2226
| `cargo rustapi client --spec <path> --language <lang>` | Generate a client library (Rust, TS, Python) from OpenAPI spec |
2327
| `cargo rustapi deploy <platform>` | Generate deployment configs for Docker, Fly.io, Railway, or Shuttle |
2428
| `cargo rustapi migrate <action>` | Database migration commands (create, run, revert, status, reset) |
29+
| `cargo rustapi replay <subcommand>` | Work with time-travel replay entries from a running RustAPI service |
2530

2631
## 🚀 Quick Start
2732

@@ -46,3 +51,8 @@ The templates used by the CLI are opinionated but flexible. They enforce:
4651
- `api`: REST API structure with separated `handlers` and `models`
4752
- `web`: Web application with HTML templates (`rustapi-view`)
4853
- `full`: Complete example with Database, Auth, and Docker support
54+
55+
**Available Presets:**
56+
- `prod-api`: production-facing API defaults (`extras-config`, `extras-cors`, `extras-rate-limit`, `extras-security-headers`, `extras-structured-logging`, `extras-timeout`)
57+
- `ai-api`: AI-oriented API defaults with `protocol-toon`
58+
- `realtime-api`: realtime-oriented API defaults with `protocol-ws`

crates/cargo-rustapi/src/cli.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
//! CLI argument parsing
22
33
use crate::commands::{
4-
self, AddArgs, ClientArgs, DeployArgs, DoctorArgs, GenerateArgs, MigrateArgs, NewArgs, RunArgs,
5-
WatchArgs,
4+
self, AddArgs, BenchArgs, ClientArgs, DeployArgs, DoctorArgs, GenerateArgs, MigrateArgs,
5+
NewArgs, ObservabilityArgs, RunArgs, WatchArgs,
66
};
7+
#[cfg(feature = "replay")]
8+
use crate::commands::ReplayArgs;
79
use clap::{Parser, Subcommand};
810

911
/// The official CLI tool for the RustAPI framework. Scaffold new projects, run development servers, and manage database migrations.
@@ -33,9 +35,15 @@ enum Commands {
3335
/// Add a feature or dependency
3436
Add(AddArgs),
3537

38+
/// Run the benchmark workflow
39+
Bench(BenchArgs),
40+
3641
/// Check environment health
3742
Doctor(DoctorArgs),
3843

44+
/// Surface observability docs and baseline workflow assets
45+
Observability(ObservabilityArgs),
46+
3947
/// Generate code from templates
4048
#[command(subcommand)]
4149
Generate(GenerateArgs),
@@ -59,9 +67,8 @@ enum Commands {
5967
Deploy(DeployArgs),
6068

6169
/// Replay debugging commands (time-travel debugging)
62-
#[cfg(feature = "replay")]
6370
#[command(subcommand)]
64-
Replay(commands::ReplayArgs),
71+
Replay(ReplayArgs),
6572
}
6673

6774
impl Cli {
@@ -72,13 +79,14 @@ impl Cli {
7279
Commands::Run(args) => commands::run_dev(args).await,
7380
Commands::Watch(args) => commands::watch(args).await,
7481
Commands::Add(args) => commands::add(args).await,
82+
Commands::Bench(args) => commands::bench(args).await,
7583
Commands::Doctor(args) => commands::doctor(args).await,
84+
Commands::Observability(args) => commands::observability(args).await,
7685
Commands::Generate(args) => commands::generate(args).await,
7786
Commands::Migrate(args) => commands::migrate(args).await,
7887
Commands::Docs { port } => commands::open_docs(port).await,
7988
Commands::Client(args) => commands::client(args).await,
8089
Commands::Deploy(args) => commands::deploy(args).await,
81-
#[cfg(feature = "replay")]
8290
Commands::Replay(args) => commands::replay(args).await,
8391
}
8492
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! Benchmark workflow command.
2+
3+
use anyhow::{bail, Context, Result};
4+
use clap::Args;
5+
use console::style;
6+
use std::path::{Path, PathBuf};
7+
use tokio::process::Command;
8+
9+
/// Run the repository benchmark workflow.
10+
#[derive(Args, Debug, Clone)]
11+
pub struct BenchArgs {
12+
/// Project or workspace path to inspect.
13+
#[arg(long, default_value = ".", value_name = "PATH")]
14+
pub path: PathBuf,
15+
/// Override performance snapshot warmup iterations.
16+
#[arg(long)]
17+
pub warmup: Option<u32>,
18+
/// Override performance snapshot measured iterations.
19+
#[arg(long)]
20+
pub iterations: Option<u32>,
21+
}
22+
23+
pub async fn bench(args: BenchArgs) -> Result<()> {
24+
let inspect_path = resolve_path(&args.path)?;
25+
let workspace_root = find_workspace_root(&inspect_path)
26+
.with_context(|| format!("No Cargo.toml found above {}", inspect_path.display()))?;
27+
let script_path = workspace_root.join("scripts").join("bench.ps1");
28+
29+
if !script_path.exists() {
30+
bail!(
31+
"Benchmark script was not found at {}",
32+
script_path.display()
33+
);
34+
}
35+
36+
let shell = if cfg!(windows) { "powershell" } else { "pwsh" };
37+
38+
println!(
39+
"{} {}",
40+
style("Running benchmark workflow from").bold(),
41+
style(script_path.display()).cyan()
42+
);
43+
44+
let mut command = Command::new(shell);
45+
if cfg!(windows) {
46+
command.args(["-ExecutionPolicy", "Bypass", "-File"]);
47+
} else {
48+
command.arg("-File");
49+
}
50+
command.arg(&script_path).current_dir(&workspace_root);
51+
52+
if let Some(warmup) = args.warmup {
53+
command.env("RUSTAPI_PERF_WARMUP", warmup.to_string());
54+
}
55+
if let Some(iterations) = args.iterations {
56+
command.env("RUSTAPI_PERF_ITERS", iterations.to_string());
57+
}
58+
59+
let status = command.status().await.context("Failed to launch benchmark workflow")?;
60+
if !status.success() {
61+
bail!("Benchmark workflow exited with status {}", status);
62+
}
63+
64+
println!("{}", style("Benchmark workflow finished.").green());
65+
Ok(())
66+
}
67+
68+
fn resolve_path(path: &Path) -> Result<PathBuf> {
69+
if path.is_absolute() {
70+
Ok(path.to_path_buf())
71+
} else {
72+
Ok(std::env::current_dir()
73+
.context("failed to determine current directory")?
74+
.join(path))
75+
}
76+
}
77+
78+
fn find_workspace_root(start: &Path) -> Option<PathBuf> {
79+
let mut current = if start.is_dir() {
80+
start.to_path_buf()
81+
} else {
82+
start.parent()?.to_path_buf()
83+
};
84+
85+
loop {
86+
if current.join("Cargo.toml").exists() {
87+
return Some(current);
88+
}
89+
90+
if !current.pop() {
91+
return None;
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)