Skip to content

Rust SDK: Usage

Create an Aires instance using the builder pattern:

use aires_sdk::Aires;

#[tokio::main]
async fn main() {
    let aires = Aires::builder()
        .service("my-service")
        .endpoint("http://localhost:4317")
        .environment("production")
        .build()
        .expect("failed to build config");

    let aires = Aires::from_config(aires).expect("failed to create client");

    // Use aires...

    aires.flush().await;
}

The Aires instance owns a background Tokio task that batches and ships events. It must live for the duration of your application. When dropped, it flushes remaining events synchronously.

Six severity levels are available, each returning an EventBuilder:

aires.trace("entering inner loop");
aires.debug("parsed 42 config entries");
aires.info("server listening on :3000");
aires.warn("connection pool utilization at 85%");
aires.error("handler returned 500");
aires.fatal("database unreachable, shutting down");

Each method returns an EventBuilder — you must call .emit() to send the event:

// This does nothing (builder not emitted):
aires.info("forgotten event");

// This sends the event:
aires.info("server started").emit();

The EventBuilder provides a fluent API for attaching context to events. Every method returns Self, so you can chain calls:

aires.info("order processed")
    .trace_id("trace-abc-123")
    .span_id("span-001")
    .session_id("sess-789")
    .user_id("user-42")
    .agent_id("billing-agent")
    .category("payment")
    .kind("request")
    .display_text("\x1b[32m✓\x1b[0m Order processed successfully")
    .tag("stripe")
    .tag("production")
    .attr("order_id", "ord-456")
    .attr("amount", "14999")
    .attr("currency", "usd")
    .data("order_details", &order)  // anything implementing Serialize
    .related("customer", "cust-789", "Acme Corp")
    .source(file!(), line!() as i32, "process_order")
    .emit();
MethodSignatureDescription
trace_id(impl Into<String>) -> SelfSet distributed trace ID
span_id(impl Into<String>) -> SelfSet span ID
parent_span_id(impl Into<String>) -> SelfSet parent span ID
subtrace_id(impl Into<String>) -> SelfSet subtrace ID
session_id(impl Into<String>) -> SelfSet session ID
user_id(impl Into<String>) -> SelfSet user ID
agent_id(impl Into<String>) -> SelfSet AI agent ID
category(impl Into<String>) -> SelfSet event category (e.g. "http", "db")
kind(impl Into<String>) -> SelfSet event kind (e.g. "request", "metric")
display_text(impl Into<String>) -> SelfSet formatted display text
tag(impl Into<String>) -> SelfAdd a tag (call multiple times for multiple tags)
attr(impl Into<String>, impl Into<String>) -> SelfAdd a string attribute
data(impl Into<String>, impl Serialize) -> SelfAdd a structured data field (JSON-serialized)
related(impl Into<String>, impl Into<String>, impl Into<String>) -> SelfAdd a related object (type, id, label)
source(&str, i32, &str) -> SelfSet source file, line, function
http(impl Into<String>, impl Into<String>, i32, i64) -> SelfSet HTTP info (method, path, status, duration_ms)
error_info(impl Into<String>, impl Into<String>, impl Into<String>, bool) -> SelfSet error info (type, message, stack, handled)
duration_ns(u64) -> SelfSet span duration in nanoseconds
emit(self)Send the event to the batch worker

Create a span to represent a unit of work within a trace:

use uuid::Uuid;

let trace_id = Uuid::now_v7().to_string();
let span_id = Uuid::now_v7().to_string();

aires.span("process-task")
    .trace_id(&trace_id)
    .span_id(&span_id)
    .category("task")
    .attr("task_id", "task-123")
    .emit();

// Child span
let child_span_id = Uuid::now_v7().to_string();
aires.span("query-database")
    .trace_id(&trace_id)
    .span_id(&child_span_id)
    .parent_span_id(&span_id)
    .category("db")
    .attr("db.operation", "SELECT")
    .attr("db.table", "tasks")
    .emit();

Record metric values:

// Gauge
aires.metric("db.connections.active", 42.0)
    .attr("pool", "primary")
    .emit();

// Counter (increment by 1)
aires.metric("http.requests.total", 1.0)
    .attr("method", "POST")
    .attr("path", "/api/tasks")
    .attr("status", "201")
    .emit();

// Histogram observation (latency)
aires.metric("http.request.duration_ms", 47.2)
    .attr("method", "POST")
    .attr("path", "/api/tasks")
    .tag("api")
    .emit();

Use the .http() builder method for HTTP request/response events:

aires.info("POST /api/tasks")
    .trace_id(&trace_id)
    .span_id(&span_id)
    .category("http")
    .http("POST", "/api/tasks", 201, 47)
    .attr("user_agent", "curl/8.0")
    .emit();

Attach error information with .error_info():

match db.execute(&query).await {
    Ok(_) => {},
    Err(e) => {
        aires.error("database query failed")
            .trace_id(&trace_id)
            .category("db")
            .error_info(
                "PostgresError",             // error type
                &e.to_string(),              // message
                &format!("{:?}", e),         // stack/debug repr
                true,                        // handled
            )
            .attr("query", &query)
            .emit();
    }
}

The aires_log! macro automatically captures source location:

use aires_sdk::aires_log;

// Basic usage
aires_log!(aires, info, "server started");

// With attributes
aires_log!(aires, error, "request failed",
    method = "POST",
    path = "/api/tasks",
    status = "500"
);

This expands to:

aires.info("request failed")
    .source(file!(), line!() as i32, "")
    .attr("method", "POST")
    .attr("path", "/api/tasks")
    .attr("status", "500")
    .emit();

Always flush before shutdown to ensure all buffered events are shipped:

// Async flush (preferred)
aires.flush().await;

// The Drop impl calls flush_sync() automatically,
// but explicit flushing gives you error handling.

For graceful shutdown:

use tokio::signal;

#[tokio::main]
async fn main() {
    let aires = /* ... */;

    // ... application logic ...

    // Wait for SIGTERM
    signal::ctrl_c().await.expect("failed to listen for ctrl-c");

    // Flush all remaining events
    aires.flush().await;
}

Aires is Send + Sync and can be shared across threads. The recommended pattern is to wrap it in an Arc or use a static OnceLock:

use std::sync::OnceLock;
use aires_sdk::Aires;

static AIRES: OnceLock<Aires> = OnceLock::new();

fn init() {
    let config = Aires::builder()
        .service("my-service")
        .endpoint("http://localhost:4317")
        .build()
        .unwrap();

    AIRES.set(Aires::from_config(config).unwrap()).unwrap();
}

fn get() -> &'static Aires {
    AIRES.get().expect("aires not initialized")
}