> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bronto.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send Rust logs, metrics, and traces to Bronto

> Instrument Rust applications with the OpenTelemetry Rust SDK to send logs, metrics, and traces to Bronto over OTLP/HTTP, via an OTel Collector or directly.

One setup function sends **logs, metrics, and traces** from your Rust application to Bronto. Logs are bridged from the `log` crate, metrics flow through a meter provider, and traces through a tracer provider — all three sharing one `Resource`.

The application exports to an [OpenTelemetry Collector](/agent-setup/open-telemetry), which forwards to Bronto. If you don't run a Collector, [export directly to Bronto](#direct-export-to-bronto) instead — only the endpoint and an API key header change.

<Note>
  The OpenTelemetry Rust SDK is still evolving and its builder APIs have changed between releases. Pin your versions and check the [OpenTelemetry Rust documentation](https://opentelemetry.io/docs/languages/rust/) for the release you deploy.
</Note>

## Prerequisites

* Rust 1.70 or later
* An OTel Collector reachable from your application, with `logs`, `metrics`, and `traces` pipelines forwarding to Bronto — see [Connect OpenTelemetry Collector to Bronto](/agent-setup/open-telemetry)

## Install dependencies

```toml Cargo.toml theme={"dark"}
# check https://crates.io/crates/opentelemetry for latest versions
opentelemetry = { version = "*", features = ["logs", "metrics", "trace"] }
opentelemetry_sdk = { version = "*", features = ["logs", "metrics", "trace", "rt-tokio"] }
opentelemetry-otlp = { version = "*", features = ["logs", "metrics", "trace", "http-proto", "reqwest-client"] }
opentelemetry-appender-log = "*"
opentelemetry-semantic-conventions = "*"
log = "0.4"
tokio = { version = "1", features = ["full"] }
```

## Initialise the SDK

```rust otel.rs theme={"dark"}
use opentelemetry::{global, KeyValue};
use opentelemetry_appender_log::OpenTelemetryLogBridge;
use opentelemetry_otlp::{LogExporter, MetricExporter, SpanExporter, WithExportConfig};
use opentelemetry_sdk::{
    logs::SdkLoggerProvider,
    metrics::SdkMeterProvider,
    resource::Resource,
    trace::SdkTracerProvider,
};
use opentelemetry_semantic_conventions::resource::{
    DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_NAMESPACE,
};

const OTLP_ENDPOINT: &str = "http://localhost:4318";

pub struct Otel {
    logger_provider: SdkLoggerProvider,
    meter_provider: SdkMeterProvider,
    tracer_provider: SdkTracerProvider,
}

impl Otel {
    /// Flush and shut down all three providers.
    pub fn shutdown(&self) {
        let _ = self.logger_provider.shutdown();
        let _ = self.meter_provider.shutdown();
        let _ = self.tracer_provider.shutdown();
    }
}

pub fn configure_otel() -> Otel {
    let resource = Resource::new(vec![
        KeyValue::new(SERVICE_NAME, "my-service"),
        KeyValue::new(SERVICE_NAMESPACE, "my-team"),
        KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, "production"),
    ]);

    // Logs — bridge the `log` crate into OTel.
    let logger_provider = SdkLoggerProvider::builder()
        .with_resource(resource.clone())
        .with_batch_exporter(
            LogExporter::builder()
                .with_http()
                .with_endpoint(format!("{OTLP_ENDPOINT}/v1/logs"))
                .build()
                .expect("failed to build OTLP log exporter"),
        )
        .build();
    log::set_boxed_logger(Box::new(OpenTelemetryLogBridge::new(&logger_provider)))
        .expect("failed to set logger");
    log::set_max_level(log::LevelFilter::Info);

    // Metrics
    let meter_provider = SdkMeterProvider::builder()
        .with_resource(resource.clone())
        .with_periodic_exporter(
            MetricExporter::builder()
                .with_http()
                .with_endpoint(format!("{OTLP_ENDPOINT}/v1/metrics"))
                .build()
                .expect("failed to build OTLP metric exporter"),
        )
        .build();
    global::set_meter_provider(meter_provider.clone());

    // Traces
    let tracer_provider = SdkTracerProvider::builder()
        .with_resource(resource)
        .with_batch_exporter(
            SpanExporter::builder()
                .with_http()
                .with_endpoint(format!("{OTLP_ENDPOINT}/v1/traces"))
                .build()
                .expect("failed to build OTLP span exporter"),
        )
        .build();
    global::set_tracer_provider(tracer_provider.clone());

    Otel { logger_provider, meter_provider, tracer_provider }
}
```

Two resource attributes determine how Bronto organises your data:

| OTel attribute      | Bronto concept | Description                                  |
| ------------------- | -------------- | -------------------------------------------- |
| `service.name`      | Dataset        | Groups telemetry from one service            |
| `service.namespace` | Collection     | Groups related services or a team's services |

<Note>
  `http://localhost:4318` is the standard OTLP/HTTP address for a Collector on the same host. Use the address reachable from your application if the Collector runs in another container, pod, or host. No authentication is needed between the application and the Collector — the Collector holds the Bronto API key.
</Note>

## Instrument your application

Existing `log::info!`, `log::warn!`, and `log::error!` calls need no changes:

```rust main.rs theme={"dark"}
mod otel;

use log::info;
use opentelemetry::{global, trace::{TraceContextExt, Tracer}, KeyValue};

#[tokio::main]
async fn main() {
    let otel = otel::configure_otel();

    let tracer = global::tracer("my-service");
    let payments = global::meter("my-service").u64_counter("app.payments").build();

    tracer.in_span("process-payment", |cx| {
        cx.span().set_attribute(KeyValue::new("payment.amount", 99.99));
        payments.add(1, &[]);
        info!("Processing payment"); // trace_id and span_id attached automatically
    });

    // Flush buffered telemetry before exit.
    otel.shutdown();
}
```

<Tip>
  Middleware instrumentation for Actix Web, Axum, and Tonic is available through community crates — see the [OpenTelemetry Rust registry](https://opentelemetry.io/ecosystem/registry/?language=rust\&component=instrumentation).
</Tip>

## Verify

Run your application, then check each signal in Bronto, filtering by the `service.name` you set:

* **Logs** — the [Search](https://app.bronto.io/search) page, in the dataset named after your service
* **Metrics** — the Metric Explorer
* **Traces** — the Explore Traces page

If nothing arrives:

* Confirm the Collector is running and reachable at the configured endpoint, and that its pipelines include an `otlp` receiver and the Bronto exporters.
* Confirm `configure_otel()` runs before the first `log::` macro or span.
* The batch exporters run on background tasks — keep the `shutdown()` call so short-lived programs flush before exit.

For signal-specific reference material, see [Send Metrics to Bronto](/metrics/send-metrics) and [Send Traces to Bronto](/tracing/send-traces).

## Direct export to Bronto

Without a Collector, the application exports straight to Bronto over OTLP/HTTP. Point `OTLP_ENDPOINT` at your Bronto region and add your API key to each exporter:

```rust theme={"dark"}
const OTLP_ENDPOINT: &str = "https://ingestion.eu.bronto.io"; // or ingestion.us.bronto.io

LogExporter::builder()
    .with_http()
    .with_endpoint(format!("{OTLP_ENDPOINT}/v1/logs"))
    .with_headers(HashMap::from([(
        "x-bronto-api-key".to_string(),
        "<YOUR_API_KEY>".to_string(),
    )]))
    .build()
```

Apply the same `.with_headers(...)` call to the metric and span exporters.

| Region | Base endpoint                    |
| ------ | -------------------------------- |
| EU     | `https://ingestion.eu.bronto.io` |
| US     | `https://ingestion.us.bronto.io` |

See [API Keys](/Account-Management/API-Keys) for how to create a key with ingestion permissions.

<Tip>
  Prefer the Collector for multi-service environments — it batches, filters, and enriches telemetry, and keeps the API key out of every application. Direct export suits single services and simple architectures.
</Tip>

## GenAI semantic conventions

If your application calls an LLM, OpenTelemetry's [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md) define `gen_ai.*` span attributes for model, token usage, and prompt and response content.

### Manual spans

There is no first-party GenAI instrumentation for Rust, so set the attributes yourself around each model call:

```rust theme={"dark"}
let tracer = global::tracer("my-service");

tracer.in_span("chat gpt-4o-mini", |cx| {
    let span = cx.span();
    span.set_attribute(KeyValue::new("gen_ai.provider.name", "openai"));
    span.set_attribute(KeyValue::new("gen_ai.request.model", "gpt-4o-mini"));
    span.set_attribute(KeyValue::new("gen_ai.usage.input_tokens", 33));
    span.set_attribute(KeyValue::new("gen_ai.usage.output_tokens", 74));
    // Bronto flattens array attributes — this is searchable as finish_reasons.0
    span.set_attribute(KeyValue::new("gen_ai.response.finish_reasons", vec!["stop"]));
});
```

Message content (`gen_ai.input.messages` / `gen_ai.output.messages`) is opt-in by convention and off by default in the languages that have auto-instrumentation. Since you are setting attributes by hand, apply the same discipline — gate prompt and response content behind your own config flag rather than always sending it.

See the [GenAI span conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md) for the full attribute list.

For the recommended attribute set and Bronto search queries, see [LLM Observability](/ai-features/llm-observability).
