Skip to content

Commit 0830910

Browse files
committed
feat: add request hooks infrastructure
Adds RequestHooks trait to enable intercepting and modifying S3 API requests at key points in the request lifecycle. This enables implementing cross-cutting concerns like load balancing, telemetry, and debug logging without modifying the core request handling logic.
1 parent 3c6586a commit 0830910

File tree

10 files changed

+1504
-60
lines changed

10 files changed

+1504
-60
lines changed

CLAUDE.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ All source files that haven't been generated MUST include the following copyrigh
3434

3535
### Comments
3636
- **NO redundant comments** - Code should be self-documenting
37-
- Avoid obvious comments like `// Set x to 5` for `x := 5`
37+
- Avoid obvious comments like `// Set x to 5` for `let x = 5;`
3838
- Only add comments when they explain WHY, not WHAT
3939
- Document complex algorithms or non-obvious business logic
4040

4141
## Critical Code Patterns
4242

4343
### Builder Pattern
44-
All S3 API requests MUST use the builder pattern, with the following documentation but then for the appropriate API
44+
All S3 API requests MUST use the builder pattern, with documentation similar to the following example (adapted for each specific API)
4545

4646
```rust
4747
/// Argument builder for the [`AppendObject`](https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-buckets-objects-append.html) S3 API operation.
@@ -73,6 +73,33 @@ impl Client {
7373
}
7474
```
7575

76+
### Rust-Specific Best Practices
77+
78+
1. **Ownership and Borrowing**
79+
- Prefer `&str` over `&String` in function parameters
80+
- Use `AsRef<str>` or `Into<String>` for flexible string parameters
81+
- Return owned types from functions unless lifetime annotations are clear
82+
83+
2. **Type Safety**
84+
- Use `#[must_use]` attribute for functions returning important values
85+
- Prefer strong typing over primitive obsession
86+
- Use newtypes for domain-specific values
87+
88+
3. **Unsafe Code**
89+
- Avoid `unsafe` code unless absolutely necessary
90+
- Document all safety invariants when `unsafe` is required
91+
- Isolate `unsafe` blocks and keep them minimal
92+
93+
4. **Performance**
94+
- Use `Cow<'_, str>` to avoid unnecessary allocations
95+
- Prefer iterators over collecting into intermediate vectors
96+
- Use `Box<dyn Trait>` sparingly; prefer generics when possible
97+
98+
5. **Async Patterns**
99+
- Use `tokio::select!` for concurrent operations
100+
- Avoid blocking operations in async contexts
101+
- Use `async-trait` for async trait methods
102+
76103
## Code Quality Principles
77104

78105
### Why Code Quality Standards Are Mandatory
@@ -126,8 +153,9 @@ Complex distributed systems code must remain **human-readable**:
126153
### Variables
127154
- Use meaningful variable names that reflect business concepts
128155
- Variable names should reflect usage frequency: frequent variables can be shorter
129-
- Constants should follow Rust patterns
130-
- Global variables should be clearly identified and documented for their system-wide purpose
156+
- Constants should use SCREAMING_SNAKE_CASE (e.g., `MAX_RETRIES`, `DEFAULT_TIMEOUT`)
157+
- Static variables should be clearly identified with proper safety documentation
158+
- Prefer `const` over `static` when possible for compile-time constants
131159

132160
### Developer Documentation
133161

@@ -179,7 +207,7 @@ Claude will periodically analyze the codebase and suggest:
179207
Before any code changes:
180208
1. ✅ Run `cargo fmt --all` to check and fix code formatting
181209
2. ✅ Run `cargo test` to ensure all tests pass
182-
3. ✅ Run `cargo clippy` to check for common mistakes
210+
3. ✅ Run `cargo clippy --all-targets --all-features --workspace -- -D warnings` to check for common mistakes and ensure no warnings
183211
4. ✅ Ensure new code has appropriate test coverage
184212
5. ✅ Verify no redundant comments are added
185213

@@ -222,6 +250,6 @@ fn operation() -> Result<Response, Error> {
222250
- **Fix formatting**: `cargo fmt --all`
223251
- **Run tests**: `cargo test`
224252
- **Run specific test**: `cargo test test_name`
225-
- **Check code**: `cargo clippy`
253+
- **Check code**: `cargo clippy --all-targets --all-features --workspace -- -D warnings`
226254
- **Build project**: `cargo build --release`
227-
- **Generate docs**: `cargo doc --open`
255+
- **Generate docs**: `cargo doc --open`

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ serde = { version = "1.0", features = ["derive"] }
5656
serde_json = "1.0"
5757
sha2 = { version = "0.10", optional = true }
5858
urlencoding = "2.1"
59-
xmltree = "0.11"
59+
xmltree = "0.12"
6060
http = "1.3"
6161
thiserror = "2.0"
62-
typed-builder = "0.22"
62+
typed-builder = "0.23"
6363

6464
[dev-dependencies]
6565
minio-common = { path = "./common" }
@@ -87,6 +87,9 @@ name = "object_prompt"
8787
[[example]]
8888
name = "append_object"
8989

90+
[[example]]
91+
name = "load_balancing_with_hooks"
92+
9093
[[bench]]
9194
name = "s3-api"
9295
path = "benches/s3/api_benchmarks.rs"

common/src/test_context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ impl TestContext {
144144
/// - `CleanupGuard` - A guard that automatically deletes the bucket when dropped.
145145
///
146146
/// # Example
147-
/// ```ignore
147+
/// ```no_run
148148
/// let (bucket_name, guard) = client.create_bucket_helper().await;
149149
/// println!("Created temporary bucket: {}", bucket_name);
150150
/// // The bucket will be removed when `guard` is dropped.

examples/debug_logging_hook.rs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// MinIO Rust Library for Amazon S3 Compatible Cloud Storage
2+
// Copyright 2024 MinIO, Inc.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
//! Example demonstrating how to use RequestHooks for debug logging.
17+
//!
18+
//! This example shows:
19+
//! - Creating a custom debug logging hook
20+
//! - Attaching the hook to the MinIO client
21+
//! - Automatic logging of all S3 API requests with headers and response status
22+
//! - Using both `before_signing_mut` and `after_execute` hooks
23+
//!
24+
//! Run with default values (test-bucket / test-object.txt, verbose mode enabled):
25+
//! ```
26+
//! cargo run --example debug_logging_hook
27+
//! ```
28+
//!
29+
//! Run with custom bucket and object:
30+
//! ```
31+
//! cargo run --example debug_logging_hook -- mybucket myobject
32+
//! ```
33+
//!
34+
//! Disable verbose output:
35+
//! ```
36+
//! cargo run --example debug_logging_hook -- --no-verbose
37+
//! ```
38+
39+
use clap::{ArgAction, Parser};
40+
use futures_util::StreamExt;
41+
use minio::s3::builders::ObjectContent;
42+
use minio::s3::client::hooks::{Extensions, RequestHooks};
43+
use minio::s3::client::{Method, Response};
44+
use minio::s3::creds::StaticProvider;
45+
use minio::s3::error::Error;
46+
use minio::s3::http::Url;
47+
use minio::s3::multimap_ext::Multimap;
48+
use minio::s3::response::BucketExistsResponse;
49+
use minio::s3::segmented_bytes::SegmentedBytes;
50+
use minio::s3::types::{S3Api, ToStream};
51+
use minio::s3::{MinioClient, MinioClientBuilder};
52+
use std::sync::Arc;
53+
54+
/// Debug logging hook that prints detailed information about each S3 request.
55+
#[derive(Debug)]
56+
struct DebugLoggingHook {
57+
/// Enable verbose output including all headers
58+
verbose: bool,
59+
}
60+
61+
impl DebugLoggingHook {
62+
fn new(verbose: bool) -> Self {
63+
Self { verbose }
64+
}
65+
}
66+
67+
#[async_trait::async_trait]
68+
impl RequestHooks for DebugLoggingHook {
69+
fn name(&self) -> &'static str {
70+
"debug-logger"
71+
}
72+
73+
async fn before_signing_mut(
74+
&self,
75+
method: &Method,
76+
url: &mut Url,
77+
_region: &str,
78+
_headers: &mut Multimap,
79+
_query_params: &Multimap,
80+
bucket_name: Option<&str>,
81+
object_name: Option<&str>,
82+
_body: Option<&SegmentedBytes>,
83+
_extensions: &mut Extensions,
84+
) -> Result<(), Error> {
85+
if self.verbose {
86+
let bucket_obj = match (bucket_name, object_name) {
87+
(Some(b), Some(o)) => format!("{b}/{o}"),
88+
(Some(b), None) => b.to_string(),
89+
_ => url.to_string(),
90+
};
91+
println!("→ Preparing {method} request for {bucket_obj}");
92+
}
93+
Ok(())
94+
}
95+
96+
async fn after_execute(
97+
&self,
98+
method: &Method,
99+
url: &Url,
100+
_region: &str,
101+
headers: &Multimap,
102+
_query_params: &Multimap,
103+
bucket_name: Option<&str>,
104+
object_name: Option<&str>,
105+
resp: &Result<Response, reqwest::Error>,
106+
_extensions: &mut Extensions,
107+
) {
108+
// Format the basic request info
109+
let bucket_obj = match (bucket_name, object_name) {
110+
(Some(b), Some(o)) => format!("{b}/{o}"),
111+
(Some(b), None) => b.to_string(),
112+
_ => url.to_string(),
113+
};
114+
115+
// Format response status
116+
let status = match resp {
117+
Ok(response) => format!("✓ {}", response.status()),
118+
Err(err) => format!("✗ Error: {err}"),
119+
};
120+
121+
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
122+
println!("S3 Request: {method} {bucket_obj}");
123+
println!("URL: {url}");
124+
println!("Status: {status}");
125+
126+
if self.verbose {
127+
// Print headers alphabetically
128+
let mut header_strings: Vec<String> = headers
129+
.iter_all()
130+
.map(|(k, v)| format!("{}: {}", k, v.join(",")))
131+
.collect();
132+
header_strings.sort();
133+
134+
println!("\nRequest Headers:");
135+
for header in header_strings {
136+
println!(" {header}");
137+
}
138+
}
139+
140+
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
141+
}
142+
}
143+
144+
/// Example demonstrating debug logging with hooks
145+
#[derive(Parser)]
146+
struct Cli {
147+
/// Bucket to use for the example
148+
#[arg(default_value = "test-bucket")]
149+
bucket: String,
150+
/// Object to upload
151+
#[arg(default_value = "test-object.txt")]
152+
object: String,
153+
/// Disable verbose output (verbose is enabled by default, use --no-verbose to disable)
154+
#[arg(long = "no-verbose", action = ArgAction::SetFalse, default_value_t = true)]
155+
verbose: bool,
156+
}
157+
158+
#[tokio::main]
159+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
160+
env_logger::init();
161+
let args = Cli::parse();
162+
163+
println!("\n🔧 MinIO Debug Logging Hook Example\n");
164+
println!("This example demonstrates how hooks can be used for debugging S3 requests.");
165+
println!(
166+
"We'll perform a few operations on bucket '{}' with debug logging enabled.\n",
167+
args.bucket
168+
);
169+
170+
// Create the debug logging hook
171+
let debug_hook = Arc::new(DebugLoggingHook::new(args.verbose));
172+
173+
// Create MinIO client with the debug logging hook attached
174+
let static_provider = StaticProvider::new(
175+
"Q3AM3UQ867SPQQA43P2F",
176+
"zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
177+
None,
178+
);
179+
180+
let client: MinioClient = MinioClientBuilder::new("https://play.min.io".parse()?)
181+
.provider(Some(static_provider))
182+
.hook(debug_hook) // Attach the debug logging hook
183+
.build()?;
184+
185+
println!("✓ Created MinIO client with debug logging hook\n");
186+
187+
// Operation 1: Check if bucket exists
188+
println!("📋 Checking if bucket exists...");
189+
let resp: BucketExistsResponse = client.bucket_exists(&args.bucket).build().send().await?;
190+
191+
// Operation 2: Create bucket if it doesn't exist
192+
if !resp.exists() {
193+
println!("\n📋 Creating bucket...");
194+
client.create_bucket(&args.bucket).build().send().await?;
195+
} else {
196+
println!("\n✓ Bucket already exists");
197+
}
198+
199+
// Operation 3: Upload a small object
200+
println!("\n📋 Uploading object...");
201+
let content = b"Hello from MinIO Rust SDK with debug logging!";
202+
let object_content: ObjectContent = content.to_vec().into();
203+
client
204+
.put_object_content(&args.bucket, &args.object, object_content)
205+
.build()
206+
.send()
207+
.await?;
208+
209+
// Operation 4: List objects in the bucket
210+
println!("\n📋 Listing objects in bucket...");
211+
let mut list_stream = client
212+
.list_objects(&args.bucket)
213+
.recursive(false)
214+
.build()
215+
.to_stream()
216+
.await;
217+
218+
let mut total_objects = 0;
219+
while let Some(result) = list_stream.next().await {
220+
match result {
221+
Ok(resp) => {
222+
total_objects += resp.contents.len();
223+
}
224+
Err(e) => {
225+
eprintln!("Error listing objects: {e}");
226+
}
227+
}
228+
}
229+
println!("\n✓ Found {total_objects} objects in bucket");
230+
231+
println!("\n🎉 All operations completed successfully with debug logging enabled!\n");
232+
println!("💡 Tip: Run with --no-verbose to disable detailed output\n");
233+
234+
Ok(())
235+
}

0 commit comments

Comments
 (0)