| Crates.io | revoke-trace |
| lib.rs | revoke-trace |
| version | 0.3.0 |
| created_at | 2025-07-13 06:02:16.431409+00 |
| updated_at | 2025-07-13 06:02:16.431409+00 |
| description | Distributed tracing with OpenTelemetry for Revoke framework |
| homepage | |
| repository | https://github.com/revoke/revoke |
| max_upload_size | |
| id | 1750021 |
| size | 162,037 |
Distributed tracing module for the Revoke microservices framework, providing OpenTelemetry-based observability.
Add to your Cargo.toml:
[dependencies]
revoke-trace = { version = "0.1.0" }
use revoke_trace::{init_tracer, Tracer};
use opentelemetry::trace::{Tracer as _, Span};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracer
let tracer = init_tracer("my-service").await?;
// Create a span
let mut span = tracer.start("process-request");
span.set_attribute("user.id", "user-123");
// Do work
process_request().await?;
// End span
span.end();
Ok(())
}
use revoke_trace::{TracerConfig, OtlpExporter};
let config = TracerConfig::builder()
.service_name("my-service")
.service_version("1.0.0")
.otlp_endpoint("http://localhost:4317")
.sample_rate(0.1) // Sample 10% of traces
.build();
let tracer = config.init().await?;
use revoke_trace::{TracerConfig, BatchConfig, Resource};
use opentelemetry::KeyValue;
let config = TracerConfig::builder()
.service_name("my-service")
.with_resource(Resource::new(vec![
KeyValue::new("service.namespace", "production"),
KeyValue::new("service.instance.id", "instance-1"),
KeyValue::new("deployment.environment", "prod"),
]))
.with_batch_config(BatchConfig {
max_queue_size: 2048,
max_export_batch_size: 512,
max_export_timeout: Duration::from_secs(30),
scheduled_delay: Duration::from_secs(5),
})
.with_sampler(Sampler::TraceIdRatio(0.1))
.build();
use revoke_trace::global;
use opentelemetry::trace::{Tracer, SpanKind, Status};
let tracer = global::tracer("my-component");
// Create span with options
let span = tracer
.span_builder("database-query")
.with_kind(SpanKind::Client)
.with_attributes(vec![
KeyValue::new("db.system", "postgresql"),
KeyValue::new("db.statement", "SELECT * FROM users"),
])
.start(&tracer);
// Set span as active
let _guard = span.enter();
// Add events
span.add_event(
"query.executed",
vec![KeyValue::new("rows.count", 42)],
);
// Set status
span.set_status(Status::Ok);
use revoke_trace::{FutureExt, Context};
// Automatically propagate context
async fn process_request(ctx: Context) {
// Spawn task with context
tokio::spawn(
async move {
do_background_work().await;
}
.with_context(ctx.clone())
);
// Call another service with context
call_service()
.with_context(ctx)
.await?;
}
use revoke_trace::SpanExt;
let span = tracer.start("operation");
match do_operation().await {
Ok(result) => {
span.set_status(Status::Ok);
result
}
Err(e) => {
span.record_error(&e);
span.set_status(Status::error(e.to_string()));
return Err(e);
}
}
use revoke_trace::axum::{TraceLayer, trace_extractor};
use axum::{Router, routing::get};
let app = Router::new()
.route("/", get(handler))
.layer(TraceLayer::new("http-server"));
async fn handler(trace: trace_extractor::Trace) -> &'static str {
let span = trace.span();
span.set_attribute("custom.attribute", "value");
"Hello, World!"
}
use revoke_trace::tonic::{TraceInterceptor, TraceService};
use tonic::transport::Server;
let service = MyService::default();
let traced_service = TraceService::new(service);
Server::builder()
.add_service(
MyServiceServer::with_interceptor(
traced_service,
TraceInterceptor::new()
)
)
.serve(addr)
.await?;
use revoke_trace::database::{trace_query, ConnectionTracer};
// Trace individual queries
let result = trace_query("SELECT * FROM users", || {
sqlx::query("SELECT * FROM users")
.fetch_all(&pool)
.await
}).await?;
// Trace connection pool
let pool = sqlx::PgPoolOptions::new()
.after_connect(|conn| {
ConnectionTracer::new().instrument(conn)
})
.connect(&database_url)
.await?;
use revoke_trace::sampling::{Sampler, ParentBased, TraceIdRatio};
// Always sample
let sampler = Sampler::AlwaysOn;
// Never sample
let sampler = Sampler::AlwaysOff;
// Sample 10% of traces
let sampler = Sampler::TraceIdRatio(0.1);
// Parent-based sampling
let sampler = Sampler::ParentBased(Box::new(
TraceIdRatio::new(0.1)
));
// Custom sampler
struct CustomSampler;
impl ShouldSample for CustomSampler {
fn should_sample(
&self,
parent_context: Option<&Context>,
trace_id: TraceId,
name: &str,
span_kind: &SpanKind,
attributes: &[KeyValue],
) -> SamplingResult {
// Custom logic
if name.contains("critical") {
SamplingResult::RecordAndSample
} else {
SamplingResult::Drop
}
}
}
use revoke_trace::enrichment::{SpanEnricher, StandardEnricher};
let enricher = StandardEnricher::new()
.with_process_info() // PID, executable name
.with_host_info() // Hostname, IP
.with_runtime_info() // Rust version, OS
.with_environment_info(); // Environment variables
tracer.set_span_enricher(enricher);
use revoke_trace::enrichment::SpanEnricher;
struct TenantEnricher {
tenant_id: String,
}
impl SpanEnricher for TenantEnricher {
fn enrich(&self, span: &mut Span) {
span.set_attribute("tenant.id", self.tenant_id.clone());
span.set_attribute("region", detect_region());
}
}
use revoke_trace::metrics::{TracingMetrics, MetricsExporter};
// Enable trace metrics
let metrics = TracingMetrics::new()
.with_span_duration_histogram()
.with_span_count_counter()
.with_error_rate_gauge();
// Export to Prometheus
let exporter = MetricsExporter::prometheus()
.with_endpoint("http://localhost:9090")
.build();
metrics.set_exporter(exporter);
use revoke_trace::batch::{BatchSpanProcessor, BatchConfig};
let batch_config = BatchConfig {
max_queue_size: 2048,
max_export_batch_size: 512,
scheduled_delay: Duration::from_secs(5),
max_export_timeout: Duration::from_secs(30),
};
let processor = BatchSpanProcessor::new(
exporter,
batch_config,
);
use revoke_trace::limits::SpanLimits;
let limits = SpanLimits::builder()
.with_max_attributes_per_span(128)
.with_max_events_per_span(128)
.with_max_links_per_span(128)
.with_max_attribute_length(1024)
.build();
tracer.set_span_limits(limits);
use revoke_trace::propagation::{Propagator, TraceContextPropagator};
use http::HeaderMap;
let propagator = TraceContextPropagator::new();
// Inject context into headers
let mut headers = HeaderMap::new();
propagator.inject(&context, &mut headers);
// Extract context from headers
let context = propagator.extract(&headers);
use revoke_trace::propagation::global;
// Set global propagator
global::set_text_map_propagator(TraceContextPropagator::new());
// In HTTP client
let client = reqwest::Client::new();
let mut request = client.get("http://other-service/api");
// Inject current context
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&mut request);
});
let response = request.send().await?;
use revoke_trace::debug::{ConsoleExporter, DebugTracer};
// Console exporter for development
let tracer = DebugTracer::new()
.with_console_exporter()
.with_pretty_print()
.build();
// In-memory exporter for testing
let (tracer, receiver) = DebugTracer::new()
.with_memory_exporter()
.build();
// Check captured spans
let spans = receiver.try_recv()?;
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].name, "test-span");
use revoke_trace::testing::{TestTracer, SpanAssertion};
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_tracing() {
let (tracer, spans) = TestTracer::new();
// Run code under test
my_function().await;
// Assert spans
spans.assert()
.span_exists("process-request")
.has_attribute("user.id", "123")
.has_status(Status::Ok);
}
}
service.operation)See the examples directory:
basic_tracing.rs - Simple tracing setuphttp_server.rs - HTTP server with tracinggrpc_service.rs - gRPC service with tracingcontext_propagation.rs - Cross-service context propagationcustom_exporter.rs - Custom trace exporter implementation