ScoutQuest / Documentation / Rust SDK

Rust SDK

Complete guide to using ScoutQuest in Rust applications with high performance and memory safety.

Version: 1.0.0 View on Crates.io

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 and ServiceUnavailableError
  • 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: