Skip to main content
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. Logs are bridged from the log crate — no log statement changes needed. Traces are added by configuring a tracer provider alongside the log provider.
If you don’t have a Collector and want to export directly from your application to Bronto, see Direct export to Bronto at the bottom of this page.

Prerequisites

Install dependencies

Cargo.toml
# 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"] }
CratePurpose
opentelemetry_sdkSdkLoggerProvider, processors
opentelemetry-otlpOTLP/HTTP exporter
opentelemetry-appender-logBridges the log crate into OTel
logStandard Rust logging facade

Configure the log bridge

Set up a SdkLoggerProvider with an OTLP exporter and register the OpenTelemetryLogBridge as the global log logger.
otel_logging.rs
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 attributeBronto conceptDescription
service.nameDatasetGroups logs from one service
service.namespaceCollectionGroups related services or a team’s services
These are set via Resource::new in the setup above.

Complete example

main.rs
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 log collection

After running your application, open the Search page in Bronto. Filter by the dataset name you set in service.name — your log records 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.
  • configure_otel_logging() is called before the first log:: macro.
  • provider.shutdown() is called before process exit to flush buffered records.

Traces

Middleware instrumentation is available for Actix Web, Axum, Tonic (gRPC), and other popular frameworks via community crates. See the OpenTelemetry Rust registry for available packages.

Install tracing dependencies

Add the tracing feature flags to your existing dependencies:
Cargo.toml
# 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

otel_tracing.rs
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

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

main.rs
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");
}

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:
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");
RegionLogs endpointTraces endpoint
EUhttps://ingestion.eu.bronto.io/v1/logshttps://ingestion.eu.bronto.io/v1/traces
UShttps://ingestion.us.bronto.io/v1/logshttps://ingestion.us.bronto.io/v1/traces
See API Keys for how to create a key with ingestion permissions. No other changes to the rest of the setup are required.