Skip to content

Add auto-instrumentation via BraintrustTracingLayer#22

Open
Stephen Belanger (Qard) wants to merge 1 commit intomainfrom
auto-instrumentation
Open

Add auto-instrumentation via BraintrustTracingLayer#22
Stephen Belanger (Qard) wants to merge 1 commit intomainfrom
auto-instrumentation

Conversation

@Qard
Copy link
Copy Markdown
Contributor

Summary

This PR implements true auto-instrumentation for the Braintrust Rust SDK using the tracing crate and OpenTelemetry GenAI semantic conventions.

With this change, users can install a tracing subscriber once and have all AI SDK calls automatically logged to Braintrust - zero manual code needed.

What's New

BraintrustTracingLayer

A new tracing_subscriber::Layer that:

  • Captures OpenTelemetry GenAI semantic convention events
  • Automatically converts them to Braintrust SpanLog entries
  • Sends logs to Braintrust in the background
  • Supports streaming responses
  • Works with any instrumented AI SDK

Usage

use braintrust_sdk_rust::{BraintrustClient, BraintrustTracingLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let braintrust = BraintrustClient::builder()
        .api_url("https://api.braintrust.dev")
        .build().await?;

    let span = braintrust.span_builder().await?
        .project_name("my-project")
        .build();

    // Install once - this is all you need!
    tracing_subscriber::registry()
        .with(BraintrustTracingLayer::new(span.clone()))
        .init();

    // All instrumented AI SDK calls are now automatically logged!
    Ok(())
}

Implementation Details

Core Components

  1. src/tracing_layer/mod.rs: Main layer implementation

    • Implements Layer<S> trait
    • Lifecycle hooks: on_new_span, on_record, on_event, on_close
    • Filters events by gen_ai.* target prefix
  2. src/tracing_layer/data.rs: GenAISpanData storage struct

    • Accumulates request/response data
    • Stores prompts, completions, usage metrics
    • Supports streaming chunks
  3. src/tracing_layer/visitor.rs: FieldVisitor for field extraction

    • Implements tracing::field::Visit
    • Extracts typed fields into serde_json::Value

Supported SDKs

This works with any SDK that emits OpenTelemetry GenAI events:

  • async-openai (with instrumentation feature)
  • rust-genai / genai (with instrumentation feature) - covers 11+ providers

Testing

  • ✅ 5 comprehensive integration tests
  • ✅ Tests layer installation, event capture, filtering, streaming
  • ✅ All existing tests still pass
  • ✅ Clippy and fmt checks pass
  • ✅ Working integration example included

Dependencies

Added tracing-subscriber with registry feature:

tracing-subscriber = { version = "0.3", features = ["registry"] }

Documentation

  • Updated README with auto-instrumentation section
  • Added examples/auto_instrumentation.rs
  • Updated design doc at docs/auto-instrumentation.md

Benefits

  • Zero manual logging code - install layer once, automatic logging forever
  • Consistent format across all AI providers
  • Standard conventions - follows OpenTelemetry GenAI specs
  • Low overhead - async logging doesn't block application
  • Streaming support - automatically captures streaming responses

Breaking Changes

None. This is a purely additive feature.

Related Work

This PR is part of a broader effort to bring auto-instrumentation to Rust AI applications. Future work includes:

  • Contributing instrumentation upstream to async-openai and rust-genai
  • Instrumenting additional SDKs (rig-core, candle-core, etc.)
  • Adding AWS Bedrock interceptor support

Checklist

  • Tests added and passing
  • Documentation updated
  • Example code provided
  • Clippy checks passing
  • Formatting checks passing
  • No breaking changes

🤖 Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

@Qard Stephen Belanger (Qard) added the enhancement New feature or request label Feb 27, 2026
@Qard Stephen Belanger (Qard) changed the base branch from main to log-queue-and-streaming-writes February 27, 2026 19:49
@Qard Stephen Belanger (Qard) force-pushed the auto-instrumentation branch 2 times, most recently from 19f5959 to c625768 Compare February 27, 2026 21:33
Base automatically changed from log-queue-and-streaming-writes to main March 3, 2026 01:46
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the tracing layer runs on every tracing event, like every call to tracing::info!. Can we avoid that given we only care about genai spans? It would help with overhead a lot in high throughput apps.

Comment thread Cargo.toml Outdated
Comment thread src/tracing_layer/mod.rs Outdated
Comment thread src/tracing_layer/mod.rs Outdated
Comment thread src/tracing_layer/mod.rs Outdated
Comment thread src/tracing_layer/mod.rs Outdated
Comment thread src/tracing_layer/mod.rs Outdated
Comment thread TESTING_OTEL.md Outdated
Comment thread examples/auto_instrumentation.rs
@AbhiPrasad
Copy link
Copy Markdown
Member

I think we still need to take care of the fact that the tracing layer runs on every tracing event.

@Qard Stephen Belanger (Qard) force-pushed the auto-instrumentation branch 2 times, most recently from a1c1b8a to bb25277 Compare March 4, 2026 18:17
@Qard
Copy link
Copy Markdown
Contributor Author

Ankur Goyal (@ankrgyl) Manu Goyal (@manugoyal) Any idea what AI SDKs should be prioritized for Rust auto-instrumentation? The idea here is to build on the excellent tracing framework and open PRs to add tracing crate support to any AI SDKs which do not already have proper support for emitting spans using OpenTelemetry Gen-AI Semantic Conventions.

@Qard Stephen Belanger (Qard) force-pushed the auto-instrumentation branch 2 times, most recently from a41a188 to e75127d Compare March 6, 2026 22:38
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread TESTING_OTEL.md
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can clean this up a bit, we probably only need to document Approach 1 and 2. Might also make sense to move this into a docs folder.

Comment thread src/tracing_layer/mod.rs
);
return;
};
handle.spawn(async move {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we bound the number of tasks we spawn?

Comment thread src/tracing_layer/mod.rs
Comment on lines +340 to +343
let mut metadata = serde_json::Map::new();
if let Some(provider) = data.provider_name {
metadata.insert("provider".to_string(), serde_json::json!(provider));
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite a lot of boilerplate. Can we avoid doing this if we are just going to pass it into log_builder.metadata anyway? We can just make the builder take these different attributes, not caring if they are Option<>

Comment thread src/tracing_layer/data.rs
Comment on lines +80 to +84
"gen_ai.provider.name" => {
if let Value::String(s) = value {
self.provider_name = Some(s);
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't block, but maybe makes sense to use a macro for these calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants