Skip to content

Conversation

@delner
Copy link
Collaborator

@delner delner commented Jan 5, 2026

Summary

This PR introduces a standardized integration framework for adding instrumentation to the Braintrust Ruby SDK. It provides a consistent architecture and tooling for building, testing, and registering new integrations (e.g., OpenAI, Anthropic, RubyLLM).

Goals

  • Standardize integration structure - Define a clear contract that all provider integrations follow
  • Simplify adding new integrations - Provide a generator and documentation so contributors can scaffold new integrations quickly
  • Separate instrumentation from tracing - Because integrating with 3rd party libraries is generally useful beyond just tracing, we want to isolate the integration logic to make it harness-able for other features.
  • Enable auto-instrumentation - Create an interface to make it easy to apply instrumentation automatically.

Non-goals

  • This PR does not convert existing instrumentation to this integration API; that happens in follow up PRs. This is ground-work only.

Architecture

The framework separates concerns into four components:

  • Integration - Declares metadata about a provider: gem name, version constraints, and whether the library is loaded. Answers "can we instrument this?" and delegates patching to its Patcher.

  • Patcher - Encapsulates the actual monkey-patching logic. Prepends instrumentation modules onto target classes (e.g., OpenAI::Client). Thread-safe and idempotent.

  • Registry - Singleton that discovers and manages all available integrations. Used by Braintrust.instrument! to find and activate the right patcher for a given provider.

  • Context - Attaches per-instance configuration to instrumented objects. When instrumenting a specific client with custom options (e.g., tracer_provider), Context stores those options on the instance for later retrieval.

lib/braintrust/contrib/
├── integration.rb    # Base module for integration contract
├── patcher.rb        # Base class for patching logic
├── registry.rb       # Singleton registry for integrations
└── context.rb        # Per-instance configuration storage

templates/contrib/
├── integration.rb.erb
├── integration_test.rb.erb
├── patcher.rb.erb
└── patcher_test.rb.erb

Class-level instrumentation (Braintrust.instrument!(:openai)):

┌─────────────────────────────────────────────────────────────────────────┐
│ Braintrust.instrument!(:openai)                                         │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Registry.lookup(:openai)                                                │
│   • Finds registered integration                                        │
│   • Checks available? and compatible?                                   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Integration.instrument! → Patcher.patch!                                │
│   • Prepends instrumentation module onto target class                   │
│   • e.g., OpenAI::Client.prepend(Instrumentation)                       │
│   • All instances now instrumented                                      │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ At runtime: client.chat.completions.create(...)                         │
│   • Instrumentation intercepts call                                     │
│   • Uses default tracer_provider                                        │
│   • Creates span, records input/output/tokens, calls original method    │
└─────────────────────────────────────────────────────────────────────────┘

Instance-level instrumentation (Braintrust.instrument!(:openai, target: client, tracer_provider: custom)):

┌─────────────────────────────────────────────────────────────────────────┐
│ Braintrust.instrument!(:openai, target: client, tracer_provider: tp)    │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Registry.lookup(:openai)                                                │
│   • Finds registered integration                                        │
│   • Checks available? and compatible?                                   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Integration.instrument!                                                 │
│   • Context.set!(client, tracer_provider: tp)                           │
│   • Stores config on instance via @braintrust_context                   │
│   • Patcher.patch! (same class-level prepend, idempotent)               │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ At runtime: client.chat.completions.create(...)                         │
│   • Instrumentation intercepts call                                     │
│   • Checks Context.from(self) for instance-specific config              │
│   • Uses instance's tracer_provider (or falls back to default)          │
│   • Creates span, records input/output/tokens, calls original method    │
└─────────────────────────────────────────────────────────────────────────┘

Creating a New Integration

Use the generator to scaffold a new integration:

rake contrib:generate NAME=my_llm AUTO_REGISTER=true

This creates:

  • lib/braintrust/contrib/my_llm/integration.rb - Integration metadata
  • lib/braintrust/contrib/my_llm/patcher.rb - Patching logic (fill in target class and instrumentation)
  • test/braintrust/contrib/my_llm/integration_test.rb
  • test/braintrust/contrib/my_llm/patcher_test.rb

Additional generator options:

Option Description
GEM_NAMES Comma-separated gem names (default: derived from NAME)
REQUIRE_PATHS Comma-separated require paths for auto-instrument detection
MIN_VERSION Minimum compatible version
MAX_VERSION Maximum compatible version
AUTO_REGISTER Register integration in contrib.rb (default: false)

@delner delner requested review from clutchski and realark January 5, 2026 19:08
@delner delner self-assigned this Jan 5, 2026
@delner delner added the enhancement New feature or request label Jan 5, 2026
Copy link
Contributor

@realark realark left a comment

Choose a reason for hiding this comment

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

LGTM!

@delner delner force-pushed the auto_instrument/integration_api branch from 050ecd3 to 2794b09 Compare January 6, 2026 15:47
@delner delner merged commit c86e7ca into feature/auto_instrument Jan 6, 2026
@delner delner deleted the auto_instrument/integration_api branch January 6, 2026 15:49
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.

3 participants