Skip to content

Easier way to handle multiple Braintrust projects in the same Rails process #118

@alfakini

Description

Hello team! Sharing here an idea, based on the problem we face in our current setup.

Got inspired to send this issue after #116, as I think we can comeup with a better API that helps us handle tracing and evals for multiple projects and OTel isolation.

First, let me explain our setup

We use the Braintrust Ruby SDK in production with multiple Braintrust projects within a single Rails process, and we keep Braintrust's OTel separate from the rest of the app's OTel. Trace context is stored in the metadata for each Session (traceparent, tracestate).

Per-process isolation: we use set_global: false and a dedicated TracerProvider per project. That way each agent's traces go to the right Braintrust project and we don't pollute the app's global OTel.

tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new

braintrust = Braintrust.init(
  api_key: config[:api_key],
  default_project: config[:project_name],
  set_global: false, 
  auto_instrument: false,
  tracer_provider: tracer_provider
)

Then we only instrument the OpenAI clients that belong to that feature by passing the tracer_provider to instrumment!:

Braintrust.instrument!(:ruby_openai, target: openai_client, tracer_provider: tracer_provider)

Multi-turn conversations run in different Sidekiq jobs (one per message), but we want one trace for the whole conversation. So we use TraceContext propagators and persist the context on a DB record (Conversation Session). First message creates the root span and saves traceparent/tracestate, later jobs read that back and continue the same trace.

First message create root span and persist:

carrier = {}
propagator = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator
propagator.inject(carrier, context: OpenTelemetry::Trace.context_with_span(span))

traceable.update!(
  traceparent: carrier["traceparent"],
  tracestate: carrier["tracestate"]
)

Later messages, restore and run inside that context:

carrier = {
  "traceparent" => traceable.traceparent,
  "tracestate" => traceable.tracestate
}.compact
parent_context = propagator.extract(carrier)

OpenTelemetry::Context.with_current(parent_context) do
  # All spans created here attach to the original trace
end

One gotcha: when we create a new root span we wrap it in OpenTelemetry::Context::ROOT so any ambient OTel from Rails middleware doesn't accidentally become the parent. Keeps the Braintrust trace clean.

OpenTelemetry::Context.with_current(OpenTelemetry::Context::ROOT) do
  tracer.in_span(name, attributes: attributes) do |span|
    # Inject the trace context (traceparent, tracestate)
    yield(span)
  end
end

What I would love to get

It would be great to have a first-class “multi-project with OTel isolation” API.

Idea: register projects by a stable key, and have the gem own the isolated TracerProvider per key.

Setup (e.g. once in an initializer): register each project with a key. The gem would create an isolated TracerProvider per key (no global, no cross-talk).

Braintrust.register(project: :braintrust_project_one)
Braintrust.register(project: braintrust_project_two)

Instrumenting a client: pass the project key instead of a tracer_provider.

Braintrust.instrument!(:ruby_openai, target: openai_client, project: :braintrust_project_one)

For custom spans and trace-context propagation we need the tracer (and optionally the tracer_provider) for a given project. Something like:

tracer = Braintrust.tracer(:braintrust_project_one)
tracer.in_span("my_operation", attributes: { ... }) { |span| ... }

# If we need the TracerProvider (e.g. for inject/extract with TraceContext):
tracer_provider = Braintrust.tracer_provider(:braintrust_project_one)

That would make multi-project + OTel isolation the default and easy to get right. Not sure if we need a "register" abstraction.

Would also be cool to have some "batteries included" with helpers to common spans. E.g.: root_span, task_span, tool_span, etc.

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions