e-volv
Docs menu

Rust

The e-volv-logs crate (imported as evolve_logs) is a tracing layer for Rust 1.75 or later. Default features (tokio, reqwest) give a Tokio-backed batcher and a reqwest transport; default-features = false gives a plain std::thread timer path with no async runtime. It speaks the same wire contract as the Node.js, Python, Go and Java SDKs.

Install and initialise

toml
[dependencies]
e-volv-logs = "0.1" # default features: tokio + reqwest; features "axum", "reqwest-middleware"
# e-volv-logs = { version = "0.1", default-features = false } # sync path, no async runtime
rust
let layer = evolve_logs::layer(evolve_logs::Options {
    key: "evk_…".into(), // project ingest key — a server key, from the environment
    url: "https://api.e-volv.io/api/public/v1/logs".into(),
    service: "orders-api".into(),
    environment: "production".into(),
    release: "1.4.2".into(),
    ..Default::default()
});
let client = layer.client();
let _guard = layer.guard(); // flush + stop the batching worker on drop

tracing_subscriber::registry().with(layer).init();

client.info("order created", serde_json::json!({"orderId": "o_1"}).as_object().unwrap().clone());

If key or url is empty, the client is a no-op and warns once on stderr — it never panics into user code. Install the layer with a tracing_subscriber::filter::Targets filter that matches your application’s targets: the layer converts every enabled tracing event into a wire event, so dependency chatter (hyper, h2, reqwest) should be silenced the same way you would for any other subscriber.

Traces and spans

Trace context rides on tracing spans. The layer promotes each span to a span-end event with span.name and durationMs, carrying the span’s trace, span and parent ids:

rust
let span = evolve_logs::span("db.query");
let _entered = span.enter();
// …work…
// On drop the span end is recorded; parent/child nesting through enter()
// gives children their parent's span id.

tracing macros require static span names, so evolve_logs::span(name) emits an evolve.span span and the layer promotes its evolve_span_name field to span.name — dynamic names work. Attach error and attribute data to an open span:

rust
evolve_logs::set_span_attrs(&span, serde_json::json!({"table": "orders"})
    .as_object().unwrap().clone());
if let Err(err) = charge().await {
    evolve_logs::record_error(&span, &err); // span ends as failed
}

axum

rust
let app: axum::Router = axum::Router::new()
    .route("/orders", axum::routing::get(handler))
    .layer(axum::middleware::from_fn(
        evolve_logs::axum::traceparent_middleware, // feature "axum"
    ));

One root http.request span per request (http.method, http.target); an inbound traceparent header is continued, a missing or malformed one starts a fresh root trace.

reqwest client

rust
let client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new())
    .with(evolve_logs::reqwest_middleware::TraceparentMiddleware) // feature "reqwest-middleware"
    .build();

Injects a traceparent header into outbound requests made inside a trace, never overwriting an existing header.

Queue hops: Pub/Sub, SQS and Cloud Tasks

rust
// producer: publish the traceparent string with the message
let header = evolve_logs::traceparent(); // "00-<traceId>-<spanId>-01"

// consumer: continue the producer's trace for the duration of a closure
evolve_logs::with_traceparent_fn(&header, || {
    client.info("job received", serde_json::Map::new());
});

// async consumer: with_traceparent instruments a future
evolve_logs::with_traceparent(&header, async {
    client.info("job received", serde_json::Map::new());
})
.await;

Every queue speaks string maps, so one traceparent() string covers them all. An absent or malformed header starts a fresh trace.

Errors

tracing::error!(error = ?e) (severity ≥ 17) becomes exception.type (the concrete type name), exception.message and exception.stack. Inside a tracked span it folds into the span end instead of double-reporting; outside one it becomes an exception log. client.exception(&err, attrs) reports an explicit occurrence from non- tracing code.

Delivery

The shared numbers: flush at 200 events, every 2 seconds or at 512 KB, always gzipped. A 429 honours Retry-After, otherwise exponential backoff from 500 ms (doubling, capped at 10 s), up to three attempts; a 413 halves the batch and counts the excess half. Past twice the batch size the oldest events drop, visible on client.dropped(). Call client.flush() before your Tokio runtime shuts down — with the Tokio worker, batching stops on guard drop but in-flight deliveries are fire-and-forget.

Already emit OpenTelemetry? The Rust preset in packages/logs-otel-presets remains a valid route — point the OTel Rust SDK at the ingest with one file. See OpenTelemetry presets.

Package reference: packages/logs-rust. Back to SDK overview.