-
Notifications
You must be signed in to change notification settings - Fork 6
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
endOne 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
endWhat 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.