Skip to content

Commit c467b79

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 a190fbc commit c467b79

File tree

9 files changed

+1467
-50
lines changed

9 files changed

+1467
-50
lines changed

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ urlencoding = "2.1"
5959
xmltree = "0.11"
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)