Rust SDK
Complete guide to using ScoutQuest in Rust applications with high performance and memory safety.
Installation
Add ScoutQuest to your Cargo.toml
:
[dependencies]
scoutquest-rust = "1.0"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Feature Flags
[dependencies]
scoutquest-rust = { version = "1.0", features = ["json", "health-checks", "metrics"] }
# Available features:
# - json: JSON serialization support (enabled by default)
# - health-checks: Built-in health check server
# - metrics: Prometheus metrics integration
# - tracing: Distributed tracing support
Quick Start
Get started with ScoutQuest in just a few lines of Rust code:
use scoutquest_rust::{ServiceDiscoveryClient, ServiceInstance};
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box> {
// Create client
let client = ServiceDiscoveryClient::new("http://localhost:8080")?;
// Register your service
let mut metadata = HashMap::new();
metadata.insert("version".to_string(), "1.0.0".to_string());
metadata.insert("environment".to_string(), "production".to_string());
let instance = client.register_service(
"my-service",
"localhost",
3000,
Some(metadata)
).await?;
println!("Service registered with ID: {}", instance.id);
// Discover other services
let user_service = client.discover_service("user-service").await?;
println!("User service found at: {}:{}", user_service.host, user_service.port);
// Make HTTP calls through ScoutQuest
let response = client.get_service("user-service", "/api/users").await?;
println!("Users response: {}", response);
Ok(())
}
Client Configuration
Basic Configuration
use scoutquest_rust::{ServiceDiscoveryClient, ClientConfig};
use std::time::Duration;
let config = ClientConfig::builder()
.timeout(Duration::from_secs(30))
.retry_count(3)
.retry_delay(Duration::from_millis(1000))
.user_agent("MyApp/1.0.0")
.build();
let client = ServiceDiscoveryClient::with_config("http://localhost:8080", config)?;
Advanced Configuration
use scoutquest_rust::{ServiceDiscoveryClient, ClientConfig, RetryPolicy};
use std::time::Duration;
let config = ClientConfig::builder()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.retry_policy(RetryPolicy::ExponentialBackoff {
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(30),
multiplier: 2.0,
max_retries: 5,
})
.connection_pool_size(10)
.enable_http2(true)
.enable_compression(true)
.user_agent("MyRustApp/2.1.0")
.default_headers([
("X-Service-Name".to_string(), "my-rust-service".to_string()),
("X-Version".to_string(), env!("CARGO_PKG_VERSION").to_string()),
])
.build();
let client = ServiceDiscoveryClient::with_config("http://localhost:8080", config)?;
TLS Configuration
use scoutquest_rust::{ServiceDiscoveryClient, ClientConfig, TlsConfig};
let tls_config = TlsConfig::builder()
.ca_certificate_file("/path/to/ca.pem")
.client_certificate_file("/path/to/client.pem")
.client_private_key_file("/path/to/client.key")
.verify_hostname(true)
.build();
let config = ClientConfig::builder()
.tls_config(tls_config)
.build();
let client = ServiceDiscoveryClient::with_config("https://localhost:8443", config)?;
Service Registration
Basic Registration
use scoutquest_rust::ServiceDiscoveryClient;
use std::collections::HashMap;
let client = ServiceDiscoveryClient::new("http://localhost:8080")?;
// Simple registration
let instance = client.register_service("api-service", "localhost", 3000, None).await?;
// With metadata
let mut metadata = HashMap::new();
metadata.insert("version".to_string(), "1.2.3".to_string());
metadata.insert("environment".to_string(), "production".to_string());
metadata.insert("region".to_string(), "us-east-1".to_string());
let instance = client.register_service(
"api-service",
"localhost",
3000,
Some(metadata)
).await?;
Advanced Registration with Service Builder
use scoutquest_rust::{ServiceDiscoveryClient, ServiceRegistration, HealthCheck};
use std::time::Duration;
let registration = ServiceRegistration::builder()
.name("api-service")
.host("localhost")
.port(3000)
.tags(vec!["api".to_string(), "v1".to_string(), "production".to_string()])
.metadata([
("version".to_string(), "1.2.3".to_string()),
("description".to_string(), "High-performance API service".to_string()),
("capabilities".to_string(), "users,auth,notifications".to_string()),
])
.health_check(
HealthCheck::builder()
.path("/health")
.interval(Duration::from_secs(30))
.timeout(Duration::from_secs(5))
.retry_count(3)
.success_threshold(2)
.build()
)
.build();
let instance = client.register_service_with_config(registration).await?;
Service Deregistration
// Deregister specific instance
client.deregister_service(&instance.id).await?;
// Deregister all instances of a service
client.deregister_all_instances("api-service").await?;
// Graceful shutdown with automatic deregistration
use tokio::signal;
tokio::select! {
_ = signal::ctrl_c() => {
println!("Shutting down gracefully...");
client.deregister_service(&instance.id).await?;
}
}
Service Discovery
Basic Discovery
// Discover a service (returns single healthy instance)
let instance = client.discover_service("user-service").await?;
println!("Service at: {}:{}", instance.host, instance.port);
// List all services
let services = client.list_services().await?;
for service in services {
println!("Service: {} - {} instances", service.name, service.instance_count);
}
// List instances of a specific service
let instances = client.list_service_instances("user-service").await?;
for instance in instances {
println!("Instance: {} at {}:{} [{}]",
instance.id, instance.host, instance.port, instance.status);
}
Advanced Discovery with Filtering
use scoutquest_rust::{ServiceDiscoveryOptions, ServiceFilter};
// Discover with filtering options
let options = ServiceDiscoveryOptions::builder()
.tags(vec!["v1".to_string(), "production".to_string()])
.exclude_tags(vec!["deprecated".to_string()])
.metadata_filter([
("region".to_string(), "us-east-1".to_string()),
("version".to_string(), "^1.2.0".to_string()), // Semver matching
])
.require_healthy(true)
.build();
let instance = client.discover_service_with_options("user-service", options).await?;
// Custom discovery with filtering logic
let instances = client.list_service_instances("user-service").await?;
let filtered: Vec<_> = instances
.into_iter()
.filter(|inst| {
inst.metadata.get("load")
.and_then(|load| load.parse::().ok())
.map(|load| load < 0.8)
.unwrap_or(false)
})
.collect();
if let Some(instance) = filtered.first() {
println!("Selected low-load instance: {}", instance.id);
}
Service Discovery with Caching
use scoutquest_rust::{ServiceDiscoveryClient, CacheConfig};
use std::time::Duration;
let cache_config = CacheConfig::builder()
.ttl(Duration::from_secs(60))
.max_size(1000)
.enable_background_refresh(true)
.build();
let client = ServiceDiscoveryClient::with_cache("http://localhost:8080", cache_config)?;
// This will be cached for 60 seconds
let instance = client.discover_service("user-service").await?;
HTTP Calls Through Service Discovery
Basic HTTP Methods
// GET request
let response = client.get_service("user-service", "/api/users").await?;
println!("Users: {}", response);
// GET with path parameters
let user_id = "123";
let response = client.get_service(
"user-service",
&format!("/api/users/{}", user_id)
).await?;
// POST request with JSON body
use serde_json::json;
let payload = json!({
"name": "John Doe",
"email": "john@example.com"
});
let response = client.post_service(
"user-service",
"/api/users",
payload.to_string()
).await?;
// PUT request
let updated_user = json!({
"name": "John Smith",
"email": "john.smith@example.com"
});
let response = client.put_service(
"user-service",
&format!("/api/users/{}", user_id),
updated_user.to_string()
).await?;
// DELETE request
client.delete_service("user-service", &format!("/api/users/{}", user_id)).await?;
Advanced HTTP Configuration
use scoutquest_rust::{HttpRequest, HttpMethod};
use std::time::Duration;
use std::collections::HashMap;
// Custom HTTP request with full control
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), format!("Bearer {}", token));
headers.insert("Content-Type".to_string(), "application/json".to_string());
let request = HttpRequest::builder()
.method(HttpMethod::Post)
.service_name("user-service")
.path("/api/users")
.headers(headers)
.body(user_data)
.timeout(Duration::from_secs(30))
.retry_count(3)
.build();
let response = client.execute_request(request).await?;
// Request with service-specific options
use scoutquest_rust::ServiceRequestOptions;
let options = ServiceRequestOptions::builder()
.tags(vec!["v1".to_string()])
.prefer_local(true)
.timeout(Duration::from_secs(15))
.build();
let response = client.get_service_with_options(
"user-service",
"/api/users",
options
).await?;
Streaming Responses
use futures_util::StreamExt;
use scoutquest_rust::StreamingRequest;
let request = StreamingRequest::builder()
.service_name("data-service")
.path("/api/stream/events")
.build();
let mut stream = client.stream_service(request).await?;
while let Some(chunk) = stream.next().await {
match chunk {
Ok(data) => {
// Process streaming data
println!("Received chunk: {} bytes", data.len());
}
Err(e) => {
eprintln!("Stream error: {}", e);
break;
}
}
}
Error Handling
Error Types
use scoutquest_rust::{
ServiceDiscoveryClient,
ScoutQuestError,
ServiceNotFoundError,
ServiceUnavailableError,
NetworkError,
ConfigurationError
};
match client.discover_service("user-service").await {
Ok(instance) => {
println!("Found service: {}", instance.id);
}
Err(ScoutQuestError::ServiceNotFound(service_name)) => {
eprintln!("Service '{}' not found", service_name);
// Handle service not registered
}
Err(ScoutQuestError::ServiceUnavailable(service_name)) => {
eprintln!("Service '{}' is unavailable", service_name);
// Handle all instances unhealthy
}
Err(ScoutQuestError::Network(net_err)) => {
eprintln!("Network error: {}", net_err);
// Handle connectivity issues
}
Err(ScoutQuestError::Configuration(config_err)) => {
eprintln!("Configuration error: {}", config_err);
// Handle client misconfiguration
}
Err(e) => {
eprintln!("Unexpected error: {}", e);
}
}
Custom Error Handler
use scoutquest_rust::{ServiceDiscoveryClient, ClientConfig, ErrorHandler, ErrorContext};
#[derive(Clone)]
struct MyErrorHandler;
impl ErrorHandler for MyErrorHandler {
fn handle_error(&self, error: &ScoutQuestError, context: &ErrorContext) -> bool {
// Log error with context
tracing::error!(
error = %error,
operation = %context.operation,
service = %context.service_name,
endpoint = %context.endpoint,
retry_count = context.retry_count,
"ScoutQuest operation failed"
);
// Send to monitoring system
metrics::counter!("scoutquest_errors_total", 1,
"error_type" => error.error_type(),
"service" => context.service_name.clone()
);
// Custom retry logic
match error {
ScoutQuestError::Network(_) if context.retry_count < 3 => true, // Retry
ScoutQuestError::ServiceUnavailable(_) if context.retry_count < 2 => true,
_ => false, // Don't retry
}
}
}
let config = ClientConfig::builder()
.error_handler(MyErrorHandler)
.build();
let client = ServiceDiscoveryClient::with_config("http://localhost:8080", config)?;
Circuit Breaker Pattern
use scoutquest_rust::{ServiceDiscoveryClient, CircuitBreakerConfig};
use std::time::Duration;
let circuit_breaker = CircuitBreakerConfig::builder()
.failure_threshold(5)
.success_threshold(3)
.timeout(Duration::from_secs(60))
.build();
let config = ClientConfig::builder()
.circuit_breaker("user-service", circuit_breaker)
.build();
let client = ServiceDiscoveryClient::with_config("http://localhost:8080", config)?;
// Circuit breaker will automatically open after 5 failures
// and close after 3 consecutive successes
match client.get_service("user-service", "/api/users").await {
Ok(response) => println!("Success: {}", response),
Err(ScoutQuestError::CircuitBreakerOpen) => {
println!("Circuit breaker is open, using fallback");
// Use cached data or alternative service
}
Err(e) => eprintln!("Error: {}", e),
}
Health Check Integration
Built-in Health Check Server
use scoutquest_rust::{HealthCheckServer, HealthStatus, HealthCheck};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
struct AppHealthChecker {
database_healthy: Arc>,
cache_healthy: Arc>,
}
#[async_trait::async_trait]
impl HealthCheck for AppHealthChecker {
async fn check_health(&self) -> HealthStatus {
let db_ok = *self.database_healthy.read().await;
let cache_ok = *self.cache_healthy.read().await;
if db_ok && cache_ok {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy(format!(
"DB: {}, Cache: {}",
if db_ok { "OK" } else { "FAIL" },
if cache_ok { "OK" } else { "FAIL" }
))
}
}
}
let health_checker = AppHealthChecker {
database_healthy: Arc::new(RwLock::new(true)),
cache_healthy: Arc::new(RwLock::new(true)),
};
let health_server = HealthCheckServer::builder()
.bind_address("0.0.0.0:3001")
.health_check(health_checker)
.readiness_path("/ready")
.liveness_path("/health")
.build();
// Start health check server
tokio::spawn(async move {
if let Err(e) = health_server.run().await {
eprintln!("Health server error: {}", e);
}
});
Custom Health Checks
use scoutquest_rust::{HealthCheck, HealthStatus};
#[derive(Clone)]
struct DatabaseHealthCheck {
connection_pool: sqlx::Pool,
}
#[async_trait::async_trait]
impl HealthCheck for DatabaseHealthCheck {
async fn check_health(&self) -> HealthStatus {
match sqlx::query("SELECT 1").fetch_one(&self.connection_pool).await {
Ok(_) => HealthStatus::Healthy,
Err(e) => HealthStatus::Unhealthy(format!("Database error: {}", e)),
}
}
}
#[derive(Clone)]
struct RedisHealthCheck {
redis_client: redis::Client,
}
#[async_trait::async_trait]
impl HealthCheck for RedisHealthCheck {
async fn check_health(&self) -> HealthStatus {
let mut conn = match self.redis_client.get_async_connection().await {
Ok(conn) => conn,
Err(e) => return HealthStatus::Unhealthy(format!("Redis connection failed: {}", e)),
};
match redis::cmd("PING").query_async::<_, String>(&mut conn).await {
Ok(_) => HealthStatus::Healthy,
Err(e) => HealthStatus::Unhealthy(format!("Redis ping failed: {}", e)),
}
}
}
Observability & Monitoring
Metrics Integration
use scoutquest_rust::{ServiceDiscoveryClient, ClientConfig, MetricsConfig};
let metrics_config = MetricsConfig::builder()
.enable_prometheus(true)
.enable_request_metrics(true)
.enable_discovery_metrics(true)
.metrics_endpoint("/metrics")
.build();
let config = ClientConfig::builder()
.metrics_config(metrics_config)
.build();
let client = ServiceDiscoveryClient::with_config("http://localhost:8080", config)?;
// Metrics are automatically collected for:
// - Service discovery operations
// - HTTP request latency and success rates
// - Circuit breaker state changes
// - Health check results
Distributed Tracing
use scoutquest_rust::{ServiceDiscoveryClient, ClientConfig, TracingConfig};
use tracing::{info_span, Instrument};
let tracing_config = TracingConfig::builder()
.service_name("my-rust-service")
.enable_jaeger(true)
.jaeger_endpoint("http://localhost:14268/api/traces")
.build();
let config = ClientConfig::builder()
.tracing_config(tracing_config)
.build();
let client = ServiceDiscoveryClient::with_config("http://localhost:8080", config)?;
// Trace service calls
async fn fetch_user_data(client: &ServiceDiscoveryClient, user_id: &str) -> Result {
info_span!("fetch_user_data", user_id = %user_id)
.in_scope(|| async move {
let response = client.get_service(
"user-service",
&format!("/api/users/{}", user_id)
).await?;
tracing::info!("Successfully fetched user data");
Ok(response)
})
.await
}
Custom Middleware
use scoutquest_rust::{Middleware, MiddlewareContext, HttpRequest, HttpResponse};
#[derive(Clone)]
struct LoggingMiddleware;
#[async_trait::async_trait]
impl Middleware for LoggingMiddleware {
async fn before_request(&self, request: &mut HttpRequest, _ctx: &MiddlewareContext) {
tracing::info!(
service = %request.service_name(),
method = %request.method(),
path = %request.path(),
"Making service request"
);
}
async fn after_response(&self, request: &HttpRequest, response: &HttpResponse, ctx: &MiddlewareContext) {
tracing::info!(
service = %request.service_name(),
method = %request.method(),
path = %request.path(),
status = response.status(),
duration_ms = ctx.duration().as_millis(),
"Service request completed"
);
}
}
let config = ClientConfig::builder()
.middleware(LoggingMiddleware)
.build();
let client = ServiceDiscoveryClient::with_config("http://localhost:8080", config)?;
Framework Integration
Axum Integration
use axum::{
extract::{Extension, Path},
http::StatusCode,
response::Json,
routing::get,
Router,
};
use scoutquest_rust::ServiceDiscoveryClient;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
scoutquest: Arc,
}
async fn get_user_profile(
Extension(state): Extension,
Path(user_id): Path,
) -> Result, StatusCode> {
match state.scoutquest.get_service("profile-service", &format!("/users/{}", user_id)).await {
Ok(response) => {
let profile: serde_json::Value = serde_json::from_str(&response)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(profile))
}
Err(_) => Err(StatusCode::SERVICE_UNAVAILABLE),
}
}
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = ServiceDiscoveryClient::new("http://localhost:8080")?;
// Register this service
let _instance = client.register_service("api-gateway", "localhost", 3000, None).await?;
let state = AppState {
scoutquest: Arc::new(client),
};
let app = Router::new()
.route("/users/:id/profile", get(get_user_profile))
.layer(Extension(state));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
Tonic (gRPC) Integration
use scoutquest_rust::ServiceDiscoveryClient;
use tonic::{Request, Response, Status};
pub struct MyGrpcService {
scoutquest: ServiceDiscoveryClient,
}
impl MyGrpcService {
pub fn new(scoutquest_url: &str) -> Result> {
Ok(Self {
scoutquest: ServiceDiscoveryClient::new(scoutquest_url)?,
})
}
}
#[tonic::async_trait]
impl my_service_server::MyService for MyGrpcService {
async fn get_user_data(
&self,
request: Request,
) -> Result, Status> {
let user_id = request.into_inner().user_id;
// Discover and call HTTP service
match self.scoutquest.get_service(
"user-service",
&format!("/api/users/{}", user_id)
).await {
Ok(response) => {
let user_data: UserData = serde_json::from_str(&response)
.map_err(|e| Status::internal(format!("JSON parse error: {}", e)))?;
Ok(Response::new(GetUserResponse {
user: Some(user_data.into()),
}))
}
Err(e) => Err(Status::unavailable(format!("Service call failed: {}", e))),
}
}
}
Best Practices
Performance Optimization
- Reuse
ServiceDiscoveryClient
instances across requests - Enable connection pooling for high-throughput scenarios
- Use caching for frequently discovered services
- Configure appropriate timeouts and retry policies
- Enable HTTP/2 for better multiplexing
Error Handling
- Always handle
ServiceNotFoundError
andServiceUnavailableError
- Implement circuit breaker patterns for external dependencies
- Use structured logging with tracing for observability
- Implement graceful degradation when services are unavailable
Production Deployment
- Enable TLS for production ScoutQuest servers
- Use health checks to ensure service readiness
- Implement proper service deregistration on shutdown
- Monitor metrics and set up alerting
- Use feature flags for gradual service rollouts
Examples
Check out these complete examples to see the Rust SDK in action: