Bitfab Ruby SDK

Ruby client library for Bitfab - trace and monitor your Ruby application's function execution with nested span support.

Installation

Add to your Gemfile:

gem 'bitfab'

Or install directly:

gem install bitfab

Requirements

  • Ruby >= 3.4
  • Runtime dependencies: base64, opentelemetry-sdk (>= 1.2, < 2, verified against 1.2 through 1.13, so an application already pinning its own OpenTelemetry version does not have to move)
  • Optional: opentelemetry-exporter-otlp, only to deliver through an OpenTelemetry Collector. Use a version compatible with your opentelemetry-sdk; if the gem cannot load, the SDK warns once and keeps delivering directly to Bitfab

Quick Start

require 'bitfab'

# Configure once at application startup
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY')
)

# Add tracing to your classes
class OrderService
  include Bitfab::Traceable
  bitfab_function "order-processing"

  bitfab_span :process_order, type: "function"
  def process_order(order_id)
    # Your code here
    { status: "completed" }
  end
end

# Use your code normally - spans are sent automatically
service = OrderService.new
service.process_order("order-123")

# Flush traces before exit (automatic via at_exit hook)
Bitfab.flush_traces

Configuration

Basic Configuration

Bitfab.configure(
  api_key: "your-api-key"
)

Custom Service URL

Bitfab.configure(
  api_key: "your-api-key",
  service_url: "https://custom.example.com"
)

Disabling Span Sending

The enabled option controls whether spans are sent to Bitfab. When disabled, your code executes normally but no spans are created or sent.

# Disable tracing (useful for development/test environments)
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY', 'dummy-key'),
  enabled: false
)

Common patterns:

# Rails: Enable only in production
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY', 'dummy-key'),
  enabled: Rails.env.production?
)

# Environment variable control
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY', 'dummy-key'),
  enabled: ENV.fetch('BITFAB_ENABLED', 'false') == 'true'
)

# Multi-environment control
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY', 'dummy-key'),
  enabled: ['production', 'staging'].include?(ENV['RACK_ENV'])
)

When enabled: false:

  • ✅ Code executes normally with no performance impact
  • ✅ Return values and errors work as expected
  • ✅ Nested spans are properly skipped
  • ❌ No HTTP requests are made
  • ❌ No span data is collected or sent

Usage

Class-Level Trace Function Key

All spans in the class share the same trace function key:

class PaymentService
  include Bitfab::Traceable
  bitfab_function "payment-processing"

  bitfab_span :charge_card, type: "function"
  def charge_card(amount)
    # Traced automatically
  end

  bitfab_span :refund, type: "function"
  def refund(transaction_id)
    # Also uses "payment-processing" key
  end
end

Per-Span Trace Function Key

Each span can declare its own key:

class NotificationService
  include Bitfab::Traceable

  bitfab_span :send_email, trace_function_key: "email-notifications", type: "function"
  def send_email(to, subject)
    # Uses "email-notifications" key
  end

  bitfab_span :send_sms, trace_function_key: "sms-notifications", type: "function"
  def send_sms(to, message)
    # Uses "sms-notifications" key
  end
end

Span Types

Bitfab supports the following span types:

  • "llm" - LLM API calls
  • "agent" - Agent decision loops
  • "function" - Business logic functions
  • "guardrail" - Validation and safety checks
  • "handoff" - Human-in-the-loop interactions
  • "custom" - Custom span types (default)
bitfab_span :validate_input, type: "guardrail"
bitfab_span :call_openai, type: "llm"
bitfab_span :agent_loop, type: "agent"

Custom Span Names

By default, the method name is used as the span name. Override it:

bitfab_span :process_order, name: "ProcessOrderV2", type: "function"
def process_order(order_id)
  # Span will be named "ProcessOrderV2"
end

Nested Spans

Spans automatically track parent-child relationships:

class OrderPipeline
  include Bitfab::Traceable
  bitfab_function "order-pipeline"

  bitfab_span :process, type: "function"
  def process(order_id)
    validate(order_id)
    # More processing
  end

  bitfab_span :validate, type: "guardrail"
  def validate(order_id)
    check_fraud(order_id)
    # More validation
  end

  bitfab_span :check_fraud, type: "guardrail"
  def check_fraud(order_id)
    # Fraud checking logic
  end
end

# Creates 3 nested spans:
# process (parent)
#   └─ validate (child)
#       └─ check_fraud (grandchild)

Input/Output Capture

Positional and keyword arguments are automatically captured:

bitfab_span :process_order, type: "function"
def process_order(order_id, priority: :normal)
  # Input captured: { "order_id" => "123", "priority" => "normal" }
  { status: "completed" }
  # Output captured: { "status" => "completed" }
end

Metadata

Add custom metadata to spans:

# Definition-time metadata
bitfab_span :process_order,
  type: "function",
  metadata: { "region" => "us-east", "version" => "v2" }
def process_order(order_id)
  # ...
end

# Runtime metadata (inside a span)
bitfab_span :process_order, type: "function"
def process_order(order_id)
  Bitfab.current_span.(
    "user_id" => current_user.id,
    "request_id" => request.id
  )
  # Runtime metadata merges with definition-time metadata
end

Wrapping External Code

Wrap third-party library methods without modifying them:

class ExternalHttpClient
  def get(url)
    # Third-party code
  end
end

# Add tracing via wrap
Bitfab::Traceable.wrap(
  ExternalHttpClient,
  :get,
  trace_function_key: "http-client",
  type: "function"
)

# Now traced automatically
client = ExternalHttpClient.new
client.get("https://api.example.com")

Fluent API: client.get_function

Bind a trace_function_key once and wrap multiple methods or classes against it. Mirrors client.get_function in the Python SDK and client.getFunction in TypeScript.

fn = Bitfab.client.get_function("openai")

fn.wrap(OpenAI::Client, :chat, name: "Chat", type: "llm")
fn.wrap(OpenAI::Client, :embeddings, name: "Embed", type: "llm")

#wrap accepts the same options as Bitfab::Traceable.wrap (name, type, mock_on_replay), but the trace_function_key is fixed to the one bound on the BitfabFunction.

Replay with Mock Strategies

Replay reruns historical traces through your code so you can compare outputs after an iteration. By default, spans tagged with mock_on_replay: true return their historical output and other child spans run real code. Three strategies control whether child spans return their historical output instead of executing:

# "marked" (default): only spans tagged with `mock_on_replay: true` return historical output
client.replay(pipeline, :process, trace_function_key: "my-fn")
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "marked")

# "none": everything runs real code
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "none")

# "all": every child span returns its historical output
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "all")

Tag the spans you want mocked at definition time:

class Pipeline
  include Bitfab::Traceable
  bitfab_function "my-fn"

  # mock_on_replay: true → returns historical output under mock: "marked"
  bitfab_span :call_llm, type: "llm", mock_on_replay: true
  def call_llm(prompt)
    # paid OpenAI call - skip during replay
  end

  bitfab_span :transform, type: "function"
  def transform(text)
    # cheap, deterministic - keep running real
  end

  bitfab_span :process, type: "agent"
  def process(text)
    call_llm(text)
    transform(text)
  end
end

Use the default mock: "marked" behavior when you want to iterate on process's logic without paying for the LLM call each run. Use mock: "all" for the cheapest possible replay (every child span returns its recorded output).

Error Handling

Errors are automatically captured and re-raised:

bitfab_span :risky_operation, type: "function"
def risky_operation
  raise StandardError, "Something went wrong"
end

begin
  service.risky_operation
rescue StandardError => e
  # Error is captured in span and re-raised
  puts "Caught: #{e.message}"
end

Lifecycle

Automatic Flush

Spans are queued on a batch worker and delivered on exit via an at_exit hook that flushes and shuts the worker down. Every other API call is blocking.

Manual Flush

Wait for all pending spans to be sent:

raise "Bitfab traces were not delivered" unless Bitfab.flush_traces(timeout: 30)

The return value is false when an export fails or the deadline expires.

Closing a Client

A long-running process that builds transient clients should release each one when it is done:

client = Bitfab::Client.new(api_key: ENV.fetch("BITFAB_API_KEY"))
begin
  # ... traced work ...
ensure
  client.close(timeout: 30)
end

A shared client needs no explicit close; at_exit shuts remaining transports down.

OpenTelemetry Transport

Bitfab keeps its public API (Bitfab::Traceable, bitfab_span, Bitfab.span), while one private OpenTelemetry TracerProvider and BatchSpanProcessor per client manage the bounded queue, batch worker, export scheduling, flush, and shutdown lifecycle. Pipelines are created lazily on the first trace send and are never installed globally, so an unused or disabled client starts no OTel worker and the SDK does not replace an application's own OpenTelemetry setup.

By default, batches are sent directly to Bitfab as OTLP/JSON. To send through a local OTel Collector instead, add the opentelemetry-exporter-otlp gem and set BITFAB_OTEL_EXPORTER_ENDPOINT to the Collector's OTLP/HTTP base URL (for example, http://localhost:4318). The SDK appends /v1/traces and sends protobuf through OTel's official exporter. Production and replay traffic share the same pipeline. Before finalizing a replay, the SDK flushes OTel and polls Bitfab's replay-status API until every expected trace completion and span count is persisted, so Collector acceptance is never mistaken for Bitfab persistence.

Direct requests carry at most eight carriers each and up to 32 run concurrently; set BITFAB_OTEL_EXPORT_CONCURRENCY to an integer from 1 through 64 to tune that. Direct JSON and Collector protobuf requests are partitioned at roughly 3 MB; set BITFAB_OTEL_MAX_REQUEST_BYTES to a positive integer no greater than 3000000 for a Collector or proxy with a stricter limit. Invalid values warn once and fall back to the defaults. If Bitfab accepts only part of a batch, it returns the standard OTLP partialSuccess response and the SDK warns with the rejected-span count and reason.

receivers:
  otlp:
    protocols:
      http:

exporters:
  otlphttp/bitfab:
    endpoint: https://bitfab.ai/api/sdk/otel
    encoding: json
    headers:
      Authorization: Bearer ${env:BITFAB_API_KEY}

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp/bitfab]

See the OpenTelemetry Transport Architecture for the full component ownership, carrier format, replay barrier, batching, and lifecycle design.

Reset Client

Clear the global client (useful for testing):

Bitfab.reset!

Thread Safety

  • Each thread has its own span context stack (using Thread.current)
  • Nested spans only work within the same thread
  • Background span sending is thread-safe

Examples

See bitfab-ruby-example/ for complete working examples:

Development

See DEVELOPMENT.md for development setup, testing, and publishing instructions.

License

MIT