Ruby SDK

The 2Signal Ruby SDK instruments your Ruby AI agents with automatic tracing, token counting, and cost tracking. Ruby 3.0+. Zero runtime dependencies — uses only stdlib. Thread-safe with background daemon export.

Installation

Gemfile

gem "twosignal"

Direct install

gem install twosignal

Quick Start

require "twosignal"

TwoSignal.configure do |c|
  c.api_key = "ts_..."  # or set TWOSIGNAL_API_KEY env var
end

client = TwoSignal::Client.new

client.span("support-agent", span_type: :AGENT).run do |s|
  s.input = "What is your refund policy?"
  answer = "Our refund policy is..."
  s.output = answer
end

client.shutdown

Initialization

TwoSignal.configure do |c|
  c.api_key = "ts_..."           # or TWOSIGNAL_API_KEY env var
  c.base_url = "https://..."     # or TWOSIGNAL_BASE_URL env var
  c.enabled = true                # set false to disable all tracing
  c.flush_interval = 1.0          # seconds between batch flushes
  c.max_batch_size = 100          # max events per batch
  c.session_id = "session-123"    # optional session grouping
end

client = TwoSignal::Client.new

# the constructor sets the global instance automatically
instance = TwoSignal.instance

The SDK is a singleton. It starts a daemon thread that batches and flushes events. Call shutdown before your process exits to flush remaining events.

Configuration

OptionEnv VarDefaultDescription
api_keyTWOSIGNAL_API_KEYYour project API key
base_urlTWOSIGNAL_BASE_URLhttp://localhost:3000API endpoint
enabledtrueEnable/disable all tracing
flush_interval1.0Seconds between background flushes
max_batch_size100Max events per HTTP request
session_idOptional session ID for grouping traces

Creating Spans

client.span("classify-intent", span_type: :TOOL).run do |s|
  s.input = user_query
  s.metadata = { model: "gpt-4o" }
  classification = classifier.classify(user_query)
  s.output = classification
end

Fields you can set on the span inside the block:

AttributeTypeDescription
s.inputanySpan input (any JSON-serializable value)
s.outputanySpan output (any JSON-serializable value)
s.metadataHashArbitrary metadata
s.modelStringModel name (for LLM spans)
s.model_parametersHashTemperature, max_tokens, etc.
s.usageSpanUsageprompt_tokens, completion_tokens, total_tokens
s.costFloatCost in USD

If your block raises an exception, the span is automatically marked as ERROR with the exception message recorded. The exception is re-raised to the caller.

Observe Decorator

Use the TwoSignal::Observe module to automatically trace methods:

class MyAgent
  include TwoSignal::Observe

  def classify(query)
    # method body is automatically wrapped in a span
    openai_client.chat(parameters: { model: "gpt-4o", messages: [...] })
  end
  observe :classify, name: "classify-intent", span_type: :TOOL

  def run(query)
    classify(query)
  end
  observe :run, name: "agent-run", span_type: :AGENT
end

The first argument to the method becomes the span's input. The return value becomes the output.

Context Propagation

The SDK uses Thread.current to propagate trace context. Nested spans automatically become children of their parent:

client.span("outer-agent", span_type: :AGENT).run do |outer|
  # this span automatically becomes a child of "outer-agent"
  client.span("inner-tool", span_type: :TOOL).run do |inner|
    # nested correctly — same trace ID, parent set automatically
    inner.output = "done"
  end
end

Span Types

SymbolValueUse for
:AGENTAGENTTop-level agent function
:LLMLLMLLM API calls
:TOOLTOOLTool / function calls
:RETRIEVALRETRIEVALRAG / vector search
:CHAINCHAINPipeline steps
:CUSTOMCUSTOMEverything else (default)

OpenAI Wrapper

If you use the ruby-openai gem, the wrapper auto-traces chat completions:

require "twosignal/wrappers/openai"

# Wrap your OpenAI client
wrapped_client = TwoSignal::Wrappers::OpenAI.wrap(openai_client)

# This automatically creates an LLM span with model, usage, and cost
response = wrapped_client.chat(parameters: {
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello!" }]
})

Cost Calculation

cost = TwoSignal::CostCalculator.calculate("gpt-4o", 100, 50)
# returns nil if model not in pricing table

Supports OpenAI, Anthropic, Google Gemini, Mistral, Cohere, and Groq models.

Manual LLM Span Example

client.span("openai-chat", span_type: :LLM).run do |s|
  s.input = messages

  response = openai_client.chat(parameters: {
    model: "gpt-4o",
    messages: messages
  })

  s.model = "gpt-4o"
  s.output = response.dig("choices", 0, "message", "content")
  usage = response["usage"]
  s.usage = TwoSignal::Models::SpanUsage.new(
    prompt_tokens: usage["prompt_tokens"],
    completion_tokens: usage["completion_tokens"],
    total_tokens: usage["total_tokens"]
  )
  s.cost = TwoSignal::CostCalculator.calculate(
    "gpt-4o", usage["prompt_tokens"], usage["completion_tokens"]
  )
end

Sessions

# Group traces under a session
client.with_session("user-123-session") do
  client.span("turn-1", span_type: :AGENT).run do |s|
    s.output = handle_turn_1
  end
  client.span("turn-2", span_type: :AGENT).run do |s|
    s.output = handle_turn_2
  end
end

Lifecycle

# force-flush all pending events
client.flush

# graceful shutdown — flushes and stops the daemon thread (5s timeout)
client.shutdown

Always call shutdown before your application exits. In Rails, register an at_exit hook or use an initializer.

Rails Integration

# config/initializers/twosignal.rb
TwoSignal.configure do |c|
  c.api_key = ENV["TWOSIGNAL_API_KEY"]
  c.base_url = ENV["TWOSIGNAL_BASE_URL"]
end

TwoSignal::Client.new

at_exit { TwoSignal.instance&.shutdown }

Thread Safety

The SDK is fully thread-safe. The exporter uses Ruby's built-in Queue (thread-safe) and context is stored in Thread.current so each thread has its own trace context.

Have questions? Join our community!

Connect with other developers and the 2Signal team.

Join Discord