> ## 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 and traces to Bronto via OpenTelemetry

> Instrument Rust applications with the OpenTelemetry Rust SDK and tracing crate to send logs and traces to Bronto over OTLP via a local Collector.

This page covers instrumenting a Rust application with the OpenTelemetry SDK to send **logs and traces** to Bronto over OTLP/HTTP via a local OTel Collector. Both signals are co-equal: logs are bridged from the `log` crate — no log statement changes needed — and traces are emitted via a tracer provider.

<Tip>
  If you don't have a Collector and want to export directly from your application to Bronto, see [Direct export to Bronto](#direct-export-to-bronto) at the bottom of this page.
</Tip>

## Prerequisites

* Rust 1.70 or later
* Cargo
* A running OTel Collector configured to forward logs and traces to Bronto — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry)
* [OpenTelemetry Rust SDK documentation](https://opentelemetry.io/docs/languages/rust/)

## Install dependencies

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

| Crate                        | Purpose                           |
| ---------------------------- | --------------------------------- |
| `opentelemetry_sdk`          | `SdkLoggerProvider`, processors   |
| `opentelemetry-otlp`         | OTLP/HTTP exporter                |
| `opentelemetry-appender-log` | Bridges the `log` crate into OTel |
| `log`                        | Standard Rust logging facade      |

## Configure the log bridge

Set up a `SdkLoggerProvider` with an OTLP exporter and register the `OpenTelemetryLogBridge` as the global `log` logger.

```rust otel_logging.rs theme={"dark"}
use opentelemetry::KeyValue;
use opentelemetry_appender_log::OpenTelemetryLogBridge;
use opentelemetry_otlp::{LogExporter, WithExportConfig};
use opentelemetry_sdk::{
    logs::SdkLoggerProvider,
    resource::Resource,
};
use opentelemetry_semantic_conventions::resource::{
    DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_NAMESPACE,
};

pub fn configure_otel_logging() -> SdkLoggerProvider {
    let exporter = LogExporter::builder()
        .with_http()
        .with_endpoint("http://localhost:4318/v1/logs")
        .build()
        .expect("failed to build OTLP log exporter");

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

    let provider = SdkLoggerProvider::builder()
        .with_resource(resource)
        .with_batch_exporter(exporter)
        .build();

    let log_bridge = OpenTelemetryLogBridge::new(&provider);
    log::set_boxed_logger(Box::new(log_bridge)).expect("failed to set logger");
    log::set_max_level(log::LevelFilter::Info);

    provider
}
```

## Configure the OTLP exporter

The `with_endpoint` call in the snippet above connects to the OTel Collector on `localhost:4318`. If your Collector runs on a different host or port, update the URL accordingly.

## Set resource attributes

Resource attributes are attached to every log record exported from this process. Two attributes drive how Bronto organises incoming logs:

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

These are set via `Resource::new` in the setup above.

## Complete example

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

use log::{error, info, warn};

#[tokio::main]
async fn main() {
    let provider = otel_logging::configure_otel_logging();

    info!("Application started");
    warn!("Low disk space, free_gb={}", 2.1);
    error!("Database connection failed: timeout");

    // Flush buffered log records before exit
    provider.shutdown().expect("failed to shut down logger provider");
}
```

Existing `log::info!()`, `log::warn!()`, and `log::error!()` calls require no changes.

## Verify delivery

After running your application, check both signals in Bronto:

* **Logs**: open the [Search](https://app.bronto.io/search) page and filter by the dataset name you set in `service.name` — your log records should appear within a few seconds.
* **Traces**: open the Explore Traces page and filter by the same `service.name` — your spans should appear within a few seconds.

If no logs appear, check:

* The OTel Collector is running and reachable at the configured endpoint.
* The Collector's pipeline includes a `logs` pipeline with an `otlp` receiver and the Bronto exporter — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry).
* `configure_otel_logging()` is called before the first `log::` macro.
* `provider.shutdown()` is called before process exit to flush buffered records.

## Traces

<Tip>
  Middleware instrumentation is available for Actix Web, Axum, Tonic (gRPC), and other popular frameworks via community crates. See the [OpenTelemetry Rust registry](https://opentelemetry.io/ecosystem/registry/?language=rust\&component=instrumentation) for available packages.
</Tip>

### Install tracing dependencies

Add the tracing feature flags to your existing dependencies:

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

### Configure the tracer provider

```rust otel_tracing.rs theme={"dark"}
use opentelemetry::global;
use opentelemetry_otlp::{SpanExporter, WithExportConfig};
use opentelemetry_sdk::{
    resource::Resource,
    trace::SdkTracerProvider,
};

pub fn configure_otel_tracing(resource: Resource) -> SdkTracerProvider {
    let exporter = SpanExporter::builder()
        .with_http()
        .with_endpoint("http://localhost:4318/v1/traces")
        .build()
        .expect("failed to build OTLP span exporter");

    let provider = SdkTracerProvider::builder()
        .with_resource(resource)
        .with_batch_exporter(exporter)
        .build();

    global::set_tracer_provider(provider.clone());
    provider
}
```

Pass the same `Resource` used for logging so both signals share `service.name` and `service.namespace`.

### Creating spans

```rust theme={"dark"}
use opentelemetry::{global, trace::{Tracer, TraceContextExt}};

let tracer = global::tracer("my-service");

tracer.in_span("process-payment", |cx| {
    let span = cx.span();
    span.set_attribute(KeyValue::new("payment.amount", 99.99));
    log::info!("Processing payment");  // trace_id and span_id injected automatically
});
```

### Complete example

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

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

#[tokio::main]
async fn main() {
    let log_provider = otel_logging::configure_otel_logging();
    let trace_provider = otel_tracing::configure_otel_tracing(
        // pass the same resource — create it once and share it
        opentelemetry_sdk::resource::Resource::new(vec![
            opentelemetry::KeyValue::new("service.name", "my-service"),
            opentelemetry::KeyValue::new("service.namespace", "my-team"),
        ])
    );

    let tracer = global::tracer("my-service");
    tracer.in_span("handle-request", |_cx| {
        info!("Handling request");  // trace_id + span_id attached automatically
    });

    log_provider.shutdown().expect("failed to shut down log provider");
    trace_provider.shutdown().expect("failed to shut down trace provider");
}
```

## GenAI semantic conventions

If your application calls an LLM, OpenTelemetry defines [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md). There is no first-party GenAI auto-instrumentation package for Rust yet — set the attributes yourself around each LLM call:

<Note>
  This page was verified on **July 17, 2026**. GenAI libraries and semantic conventions are evolving rapidly, so package configuration and emitted attributes can change between releases. Keep your instrumentation current and verify the fields emitted by the version you deploy.
</Note>

```rust theme={"dark"}
use opentelemetry::{global, trace::Tracer, KeyValue};

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));
});
```

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

See [LLM Observability](/ai-features/llm-observability) for the full recommended attribute set and how to search and aggregate GenAI fields in Bronto.

## Direct export to Bronto

If you are not using an OTel Collector, export directly to Bronto by replacing the exporter configurations with the Bronto OTLP endpoints and your API key:

```rust theme={"dark"}
use std::collections::HashMap;

let mut headers = HashMap::new();
headers.insert("x-bronto-api-key".to_string(), "<YOUR_API_KEY>".to_string());

let log_exporter = LogExporter::builder()
    .with_http()
    .with_endpoint("https://ingestion.eu.bronto.io/v1/logs") // or ingestion.us.bronto.io
    .with_headers(headers.clone())
    .build()
    .expect("failed to build log exporter");

let span_exporter = SpanExporter::builder()
    .with_http()
    .with_endpoint("https://ingestion.eu.bronto.io/v1/traces") // or ingestion.us.bronto.io
    .with_headers(headers)
    .build()
    .expect("failed to build span exporter");
```

| Region | Logs endpoint                            | Traces endpoint                            |
| ------ | ---------------------------------------- | ------------------------------------------ |
| EU     | `https://ingestion.eu.bronto.io/v1/logs` | `https://ingestion.eu.bronto.io/v1/traces` |
| US     | `https://ingestion.us.bronto.io/v1/logs` | `https://ingestion.us.bronto.io/v1/traces` |

See [API Keys](/Account-Management/API-Keys) for how to create a key with ingestion permissions. No other changes to the rest of the setup are required.
