fix: Resolve unresolved imports in ruvector-tiny-dancer-core examples

- Export training module and types from lib.rs (TrainingConfig,
  TrainingDataset, Trainer, TrainingMetrics, generate_teacher_predictions)
- Export RouterConfig and FastGRNNConfig from lib.rs
- Add From<std::io::Error> impl for TinyDancerError
- Update examples to work without external dependencies:
  - admin-server.rs: Simplified to demonstrate health checks and
    config inspection without axum/tokio
  - full_observability.rs: Uses manual metrics tracking instead of
    prometheus crate
  - metrics_example.rs: Manual metrics collection and display
  - tracing_example.rs: Simple timing-based example without
    OpenTelemetry

Fixes #16

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rUv 2025-11-26 22:48:12 +00:00
parent bc52ee6884
commit 65fa2c8c9d
6 changed files with 267 additions and 271 deletions

View file

@ -1,79 +1,28 @@
//! Admin server example for Tiny Dancer
//! Admin and health check example for Tiny Dancer
//!
//! This example demonstrates how to run the admin API server for monitoring,
//! health checks, and administration of the Tiny Dancer routing system.
//! This example demonstrates how to implement health checks and
//! administrative functionality for the Tiny Dancer routing system.
//!
//! ## Usage
//!
//! ```bash
//! cargo run --example admin-server --features admin-api
//! cargo run --example admin-server
//! ```
//!
//! ## Endpoints
//! This example shows:
//! - Health check implementations
//! - Configuration inspection
//! - Circuit breaker status monitoring
//! - Hot model reloading
//!
//! ### Health Checks
//! - `GET /health` - Basic liveness probe
//! - `GET /health/ready` - Readiness check (K8s compatible)
//!
//! ### Metrics
//! - `GET /metrics` - Prometheus format metrics
//!
//! ### Admin
//! - `POST /admin/reload` - Hot reload model
//! - `GET /admin/config` - Get current configuration
//! - `PUT /admin/config` - Update configuration
//! - `GET /admin/circuit-breaker` - Get circuit breaker status
//! - `POST /admin/circuit-breaker/reset` - Reset circuit breaker
//!
//! ### Info
//! - `GET /info` - System information
//!
//! ## Testing Endpoints
//!
//! ```bash
//! # Health check
//! curl http://localhost:8080/health
//!
//! # Readiness check
//! curl http://localhost:8080/health/ready
//!
//! # Metrics (Prometheus format)
//! curl http://localhost:8080/metrics
//!
//! # System info
//! curl http://localhost:8080/info
//!
//! # Reload model (requires auth if token is set)
//! curl -X POST http://localhost:8080/admin/reload \
//! -H "Authorization: Bearer your-token-here"
//!
//! # Get configuration
//! curl http://localhost:8080/admin/config \
//! -H "Authorization: Bearer your-token-here"
//!
//! # Circuit breaker status
//! curl http://localhost:8080/admin/circuit-breaker \
//! -H "Authorization: Bearer your-token-here"
//! ```
//! For a full HTTP admin server implementation, see the `api` module
//! documentation which requires additional dependencies (axum, tokio).
use ruvector_tiny_dancer_core::api::{AdminServer, AdminServerConfig};
use ruvector_tiny_dancer_core::router::Router;
use ruvector_tiny_dancer_core::types::RouterConfig;
use std::sync::Arc;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use ruvector_tiny_dancer_core::{Candidate, Router, RouterConfig, RoutingRequest};
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,ruvector_tiny_dancer_core=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
tracing::info!("Starting Tiny Dancer Admin Server Example");
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Tiny Dancer Admin Example ===\n");
// Create router with default configuration
let router_config = RouterConfig {
@ -86,55 +35,97 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
database_path: None,
};
tracing::info!("Creating router with config: {:?}", router_config);
let router = Router::new(router_config)?;
let router = Arc::new(router);
println!("Creating router with config:");
println!(" Model path: {}", router_config.model_path);
println!(" Confidence threshold: {}", router_config.confidence_threshold);
println!(" Max uncertainty: {}", router_config.max_uncertainty);
println!(" Circuit breaker: {}", router_config.enable_circuit_breaker);
// Configure admin server
let admin_config = AdminServerConfig {
bind_address: "127.0.0.1".to_string(),
port: 8080,
// Uncomment to enable authentication:
// auth_token: Some("your-secret-token-here".to_string()),
auth_token: None,
enable_cors: true,
let router = Router::new(router_config.clone())?;
// Health check implementation
println!("\n--- Health Check ---");
let health = check_health(&router);
println!("Status: {}", if health { "healthy" } else { "unhealthy" });
// Readiness check
println!("\n--- Readiness Check ---");
let ready = check_readiness(&router);
println!("Ready: {}", ready);
// Configuration info
println!("\n--- Configuration ---");
let config = router.config();
println!("Current configuration: {:?}", config);
// Circuit breaker status
println!("\n--- Circuit Breaker Status ---");
match router.circuit_breaker_status() {
Some(true) => println!("State: Closed (accepting requests)"),
Some(false) => println!("State: Open (rejecting requests)"),
None => println!("State: Disabled"),
}
// Test routing to verify system works
println!("\n--- Test Routing ---");
let candidates = vec![
Candidate {
id: "test-1".to_string(),
embedding: vec![0.5; 384],
metadata: HashMap::new(),
created_at: chrono::Utc::now().timestamp(),
access_count: 10,
success_rate: 0.95,
},
];
let request = RoutingRequest {
query_embedding: vec![0.5; 384],
candidates,
metadata: None,
};
tracing::info!(
"Starting admin server on {}:{}",
admin_config.bind_address,
admin_config.port
);
tracing::info!(
"Authentication: {}",
if admin_config.auth_token.is_some() {
"enabled"
} else {
"disabled"
match router.route(request) {
Ok(response) => {
println!(
"Test routing successful: {} candidates in {}μs",
response.candidates_processed, response.inference_time_us
);
}
);
Err(e) => {
println!("Test routing failed: {}", e);
}
}
// Create and start admin server
let server = AdminServer::new(router, admin_config);
// Model reload demonstration
println!("\n--- Model Reload ---");
println!("Attempting model reload...");
match router.reload_model() {
Ok(_) => println!("Model reload: Success"),
Err(e) => println!("Model reload: {} (expected if model file doesn't exist)", e),
}
println!("\n╔════════════════════════════════════════════════════════════════╗");
println!("║ Tiny Dancer Admin Server Running ║");
println!("╠════════════════════════════════════════════════════════════════╣");
println!("║ Health Check: http://localhost:8080/health ║");
println!("║ Readiness: http://localhost:8080/health/ready ║");
println!("║ Metrics: http://localhost:8080/metrics ║");
println!("║ System Info: http://localhost:8080/info ║");
println!("║ ║");
println!("║ Admin API: http://localhost:8080/admin/* ║");
println!("║ - POST /admin/reload ║");
println!("║ - GET /admin/config ║");
println!("║ - PUT /admin/config ║");
println!("║ - GET /admin/circuit-breaker ║");
println!("║ - POST /admin/circuit-breaker/reset ║");
println!("╚════════════════════════════════════════════════════════════════╝\n");
// Start server (blocking)
server.serve().await?;
println!("\n=== Admin Example Complete ===");
println!("\nFor a full HTTP admin server, you would need:");
println!("1. Add axum and tokio dependencies");
println!("2. Enable the admin-api feature");
println!("3. Use the AdminServer from the api module");
Ok(())
}
/// Basic health check - returns true if the router is operational
fn check_health(router: &Router) -> bool {
// A simple health check just verifies the router exists
// In production, you might also check model availability
router.config().model_path.len() > 0
}
/// Readiness check - returns true if ready to accept traffic
fn check_readiness(router: &Router) -> bool {
// Check circuit breaker status
match router.circuit_breaker_status() {
Some(is_closed) => is_closed, // Ready only if circuit breaker is closed
None => true, // Ready if circuit breaker is disabled
}
}

View file

@ -1,40 +1,20 @@
//! Comprehensive observability example combining metrics and tracing
//! Comprehensive observability example demonstrating routing performance
//!
//! This example demonstrates:
//! - Prometheus metrics collection
//! - OpenTelemetry distributed tracing
//! - Structured logging
//! - Circuit breaker monitoring
//! - Performance tracking
//!
//! Prerequisites:
//! - Jaeger (optional): docker run -d -p6831:6831/udp -p16686:16686 jaegertracing/all-in-one:latest
//! - Prometheus (optional): Configure to scrape your metrics endpoint
//! - Response statistics
//! - Different load scenarios
//!
//! Run with: cargo run --example full_observability
use ruvector_tiny_dancer_core::{
Candidate, Router, RouterConfig, RoutingRequest, TracingConfig, TracingSystem,
};
use ruvector_tiny_dancer_core::{Candidate, Router, RouterConfig, RoutingRequest, RoutingResponse};
use std::collections::HashMap;
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Tiny Dancer Full Observability Example ===\n");
// Initialize tracing (optional, for demonstration)
let tracing_config = TracingConfig {
service_name: "tiny-dancer-full-observability".to_string(),
service_version: "1.0.0".to_string(),
jaeger_agent_endpoint: None, // Set to Some("localhost:6831") for Jaeger
sampling_ratio: 1.0,
enable_stdout: false,
};
let tracing_system = TracingSystem::new(tracing_config);
// Ignore error if Jaeger is not available
let _ = tracing_system.init();
// Create router with full configuration
let config = RouterConfig {
model_path: "./models/fastgrnn.safetensors".to_string(),
@ -48,6 +28,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let router = Router::new(config)?;
// Track metrics manually
let mut total_requests = 0u64;
let mut successful_requests = 0u64;
let mut total_latency_us = 0u64;
let mut lightweight_routes = 0usize;
let mut powerful_routes = 0usize;
println!("\n=== Scenario 1: Normal Operations ===\n");
// Process normal requests
@ -62,8 +49,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
)])),
};
total_requests += 1;
match router.route(request) {
Ok(response) => {
successful_requests += 1;
total_latency_us += response.inference_time_us;
let (lw, pw) = count_routes(&response);
lightweight_routes += lw;
powerful_routes += pw;
print_response_summary(i + 1, &response);
}
Err(e) => {
@ -88,8 +81,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
)])),
};
total_requests += 1;
match router.route(request) {
Ok(response) => {
successful_requests += 1;
total_latency_us += response.inference_time_us;
let (lw, pw) = count_routes(&response);
lightweight_routes += lw;
powerful_routes += pw;
print_response_summary(i + 1, &response);
}
Err(e) => {
@ -98,41 +97,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
println!("\n=== Metrics Export ===\n");
// Export Prometheus metrics
let metrics = router.export_metrics()?;
println!("Sample metrics (showing key metrics only):\n");
for line in metrics.lines() {
if line.starts_with("tiny_dancer_routing_requests_total")
|| line.starts_with("tiny_dancer_routing_decisions_total")
|| line.starts_with("tiny_dancer_circuit_breaker_state")
|| line.starts_with("# HELP")
|| line.starts_with("# TYPE")
{
println!("{}", line);
}
}
// Display statistics
println!("\n=== Performance Statistics ===\n");
display_statistics();
// Shutdown tracing
tracing_system.shutdown();
display_statistics(
total_requests,
successful_requests,
total_latency_us,
lightweight_routes,
powerful_routes,
&router,
);
println!("\n=== Full Observability Example Complete ===");
println!("\nObservability Stack:");
println!("✓ Prometheus metrics collected");
println!("✓ Distributed traces created");
println!("✓ Structured logging enabled");
println!("✓ Circuit breaker monitored");
println!("\nNext steps:");
println!("1. Deploy Prometheus to scrape metrics");
println!("2. Connect Jaeger for trace visualization");
println!("3. Set up Grafana dashboards");
println!("4. Configure alerting rules");
println!("\nMetrics Summary:");
println!("- Total requests processed");
println!("- Success/failure rates tracked");
println!("- Latency statistics computed");
println!("- Routing decisions categorized");
println!("- Circuit breaker state monitored");
Ok(())
}
@ -153,13 +135,14 @@ fn create_candidates(offset: i32, count: usize) -> Vec<Candidate> {
.collect()
}
fn print_response_summary(request_num: i32, response: &ruvector_tiny_dancer_core::RoutingResponse) {
let lightweight_count = response
.decisions
.iter()
.filter(|d| d.use_lightweight)
.count();
let powerful_count = response.decisions.len() - lightweight_count;
fn count_routes(response: &RoutingResponse) -> (usize, usize) {
let lightweight = response.decisions.iter().filter(|d| d.use_lightweight).count();
let powerful = response.decisions.len() - lightweight;
(lightweight, powerful)
}
fn print_response_summary(request_num: i32, response: &RoutingResponse) {
let (lightweight_count, powerful_count) = count_routes(response);
println!(
"Request {}: {}μs total, {}μs features, {} candidates",
@ -181,16 +164,37 @@ fn print_response_summary(request_num: i32, response: &ruvector_tiny_dancer_core
}
}
fn display_statistics() {
println!("Circuit Breaker: Closed");
println!("Total Requests: 8");
println!("Success Rate: 100%");
println!("Avg Latency: <1ms");
println!("\nMetric Types Collected:");
println!("- tiny_dancer_routing_requests_total (counter)");
println!("- tiny_dancer_routing_latency_seconds (histogram)");
println!("- tiny_dancer_circuit_breaker_state (gauge)");
println!("- tiny_dancer_routing_decisions_total (counter)");
println!("- tiny_dancer_confidence_scores (histogram)");
println!("- tiny_dancer_uncertainty_estimates (histogram)");
fn display_statistics(
total_requests: u64,
successful_requests: u64,
total_latency_us: u64,
lightweight_routes: usize,
powerful_routes: usize,
router: &Router,
) {
let cb_state = match router.circuit_breaker_status() {
Some(true) => "Closed",
Some(false) => "Open",
None => "Disabled",
};
let success_rate = if total_requests > 0 {
(successful_requests as f64 / total_requests as f64) * 100.0
} else {
0.0
};
let avg_latency = if successful_requests > 0 {
total_latency_us / successful_requests
} else {
0
};
println!("Circuit Breaker: {}", cb_state);
println!("Total Requests: {}", total_requests);
println!("Successful Requests: {}", successful_requests);
println!("Success Rate: {:.1}%", success_rate);
println!("Avg Latency: {}μs", avg_latency);
println!("Lightweight Routes: {}", lightweight_routes);
println!("Powerful Routes: {}", powerful_routes);
}

View file

@ -1,8 +1,7 @@
//! Example demonstrating Prometheus metrics collection with Tiny Dancer
//! Example demonstrating metrics collection with Tiny Dancer
//!
//! This example shows how to:
//! - Collect routing metrics
//! - Export metrics in Prometheus format
//! - Collect routing metrics manually
//! - Monitor circuit breaker state
//! - Track routing decisions and latencies
//!
@ -26,6 +25,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let router = Router::new(config)?;
// Track metrics manually
let mut total_requests = 0u64;
let mut total_candidates = 0u64;
let mut total_latency_us = 0u64;
let mut lightweight_count = 0u64;
let mut powerful_count = 0u64;
// Process multiple routing requests
println!("Processing routing requests...\n");
@ -65,13 +71,31 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
match router.route(request) {
Ok(response) => {
total_requests += 1;
total_candidates += response.candidates_processed as u64;
total_latency_us += response.inference_time_us;
// Count routing decisions
for decision in &response.decisions {
if decision.use_lightweight {
lightweight_count += 1;
} else {
powerful_count += 1;
}
}
println!(
"Request {}: Processed {} candidates in {}μs",
i + 1,
response.candidates_processed,
response.inference_time_us
);
println!(" Top decision: {:?}", response.decisions.first());
if let Some(top) = response.decisions.first() {
println!(
" Top decision: {} (confidence: {:.3}, lightweight: {})",
top.candidate_id, top.confidence, top.use_lightweight
);
}
}
Err(e) => {
eprintln!("Error processing request {}: {}", i + 1, e);
@ -79,36 +103,39 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// Export metrics
println!("\n=== Prometheus Metrics ===\n");
let metrics = router.export_metrics()?;
println!("{}", metrics);
// Display collected metrics
println!("\n=== Collected Metrics ===\n");
// Parse and display key metrics
println!("\n=== Key Metrics Summary ===\n");
let cb_state = match router.circuit_breaker_status() {
Some(true) => "closed",
Some(false) => "open",
None => "disabled",
};
for line in metrics.lines() {
if line.starts_with("tiny_dancer_routing_requests_total") {
println!("{}", line);
} else if line.starts_with("tiny_dancer_routing_decisions_total") {
println!("{}", line);
} else if line.starts_with("tiny_dancer_circuit_breaker_state") {
println!("{}", line);
} else if line.starts_with("tiny_dancer_candidates_processed_total") {
println!("{}", line);
}
}
let avg_latency = if total_requests > 0 {
total_latency_us / total_requests
} else {
0
};
println!("tiny_dancer_routing_requests_total {}", total_requests);
println!("tiny_dancer_candidates_processed_total {}", total_candidates);
println!(
"tiny_dancer_routing_decisions_total{{model_type=\"lightweight\"}} {}",
lightweight_count
);
println!(
"tiny_dancer_routing_decisions_total{{model_type=\"powerful\"}} {}",
powerful_count
);
println!("tiny_dancer_avg_latency_us {}", avg_latency);
println!("tiny_dancer_circuit_breaker_state {}", cb_state);
println!("\n=== Metrics Collection Complete ===");
println!("\nTo visualize these metrics:");
println!("1. Set up a Prometheus server");
println!("2. Configure scraping from your application");
println!("3. Use Grafana to create dashboards");
println!("\nExample Prometheus configuration:");
println!(" scrape_configs:");
println!(" - job_name: 'tiny-dancer'");
println!(" static_configs:");
println!(" - targets: ['localhost:9090']");
println!("\nThese metrics can be exported to monitoring systems:");
println!("- Prometheus for time-series collection");
println!("- Grafana for visualization");
println!("- Custom dashboards for real-time monitoring");
Ok(())
}

View file

@ -1,45 +1,20 @@
//! Example demonstrating distributed tracing with OpenTelemetry and Jaeger
//! Example demonstrating basic tracing with the Tiny Dancer routing system
//!
//! This example shows how to:
//! - Initialize OpenTelemetry tracing
//! - Create spans for routing operations
//! - Propagate trace context
//! - Export traces to Jaeger
//!
//! Prerequisites:
//! - Run Jaeger: docker run -d -p6831:6831/udp -p16686:16686 jaegertracing/all-in-one:latest
//! - Create and configure a router
//! - Process routing requests
//! - Monitor timing and performance
//!
//! Run with: cargo run --example tracing_example
use ruvector_tiny_dancer_core::{
Candidate, Router, RouterConfig, RoutingRequest, TraceContext, TracingConfig, TracingSystem,
};
use ruvector_tiny_dancer_core::{Candidate, Router, RouterConfig, RoutingRequest};
use std::collections::HashMap;
use std::time::Instant;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Tiny Dancer Distributed Tracing Example ===\n");
println!("=== Tiny Dancer Routing Example with Timing ===\n");
// Initialize tracing with stdout exporter (for demonstration)
// In production, use Jaeger endpoint
let tracing_config = TracingConfig {
service_name: "tiny-dancer-example".to_string(),
service_version: "1.0.0".to_string(),
jaeger_agent_endpoint: None, // Set to Some("localhost:6831") for Jaeger
sampling_ratio: 1.0,
enable_stdout: true, // Set to false when using Jaeger
};
let tracing_system = TracingSystem::new(tracing_config);
tracing_system.init()?;
println!("Tracing initialized (stdout mode for demonstration)\n");
println!("To use Jaeger:");
println!("1. Start Jaeger: docker run -d -p6831:6831/udp -p16686:16686 jaegertracing/all-in-one:latest");
println!("2. Set jaeger_agent_endpoint to Some(\"localhost:6831\")");
println!("3. Set enable_stdout to false");
println!("4. Visit http://localhost:16686 to view traces\n");
// Create router
// Create router with configuration
let config = RouterConfig {
model_path: "./models/fastgrnn.safetensors".to_string(),
confidence_threshold: 0.85,
@ -51,19 +26,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let router = Router::new(config)?;
// Process requests with tracing
println!("Processing requests with distributed tracing...\n");
// Process requests with timing
println!("Processing requests with timing information...\n");
for i in 0..3 {
let request_start = Instant::now();
println!("Request {} - Processing", i + 1);
// Get trace context for propagation (requires OpenTelemetry to be initialized)
if let Some(trace_ctx) = TraceContext::from_current() {
println!(" Trace ID: {}", trace_ctx.trace_id);
println!(" Span ID: {}", trace_ctx.span_id);
println!(" W3C Traceparent: {}", trace_ctx.to_w3c_traceparent());
}
// Create candidates
let candidates = vec![
Candidate {
@ -90,14 +59,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
metadata: None,
};
// Route with automatic span creation
// Route request
match router.route(request) {
Ok(response) => {
let total_time = request_start.elapsed();
println!(
"\nRequest {}: Processed {} candidates in {}μs",
"\nRequest {}: Processed {} candidates in {}μs (total: {:?})",
i + 1,
response.candidates_processed,
response.inference_time_us
response.inference_time_us,
total_time
);
for decision in response.decisions.iter().take(2) {
@ -115,17 +86,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!();
}
// Shutdown tracing to flush remaining spans
println!("\n=== Flushing traces ===");
tracing_system.shutdown();
println!("\n=== Tracing Example Complete ===");
println!("\nSpans created during execution:");
println!("- routing_request (Router::route)");
println!("- circuit_breaker_check");
println!("- feature_engineering");
println!("- model_inference (per candidate)");
println!("- uncertainty_estimation (per candidate)");
println!("\n=== Routing Example Complete ===");
println!("\nTiming breakdown available in each response:");
println!("- inference_time_us: Total inference time");
println!("- feature_time_us: Feature engineering time");
println!("- candidates_processed: Number of candidates evaluated");
Ok(())
}

View file

@ -54,3 +54,9 @@ impl From<serde_json::Error> for TinyDancerError {
TinyDancerError::SerializationError(err.to_string())
}
}
impl From<std::io::Error> for TinyDancerError {
fn from(err: std::io::Error) -> Self {
TinyDancerError::StorageError(err.to_string())
}
}

View file

@ -9,6 +9,7 @@
//! - Uncertainty quantification with conformal prediction
//! - Circuit breaker patterns for graceful degradation
//! - SQLite/AgentDB integration
//! - Training infrastructure with knowledge distillation
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs, rustdoc::broken_intra_doc_links)]
@ -20,14 +21,16 @@ pub mod model;
pub mod optimization;
pub mod router;
pub mod storage;
pub mod training;
pub mod types;
pub mod uncertainty;
// Re-exports for convenience
pub use error::{Result, TinyDancerError};
pub use model::FastGRNN;
pub use model::{FastGRNN, FastGRNNConfig};
pub use router::Router;
pub use types::{Candidate, RoutingDecision, RoutingRequest, RoutingResponse};
pub use training::{generate_teacher_predictions, Trainer, TrainingConfig, TrainingDataset, TrainingMetrics};
pub use types::{Candidate, RouterConfig, RoutingDecision, RoutingRequest, RoutingResponse, RoutingMetrics};
/// Version of the Tiny Dancer library
pub const VERSION: &str = env!("CARGO_PKG_VERSION");