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 twosignalQuick 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.shutdownInitialization
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.instanceThe 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
| Option | Env Var | Default | Description |
|---|---|---|---|
api_key | TWOSIGNAL_API_KEY | — | Your project API key |
base_url | TWOSIGNAL_BASE_URL | http://localhost:3000 | API endpoint |
enabled | — | true | Enable/disable all tracing |
flush_interval | — | 1.0 | Seconds between background flushes |
max_batch_size | — | 100 | Max events per HTTP request |
session_id | — | — | Optional 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
endFields you can set on the span inside the block:
| Attribute | Type | Description |
|---|---|---|
s.input | any | Span input (any JSON-serializable value) |
s.output | any | Span output (any JSON-serializable value) |
s.metadata | Hash | Arbitrary metadata |
s.model | String | Model name (for LLM spans) |
s.model_parameters | Hash | Temperature, max_tokens, etc. |
s.usage | SpanUsage | prompt_tokens, completion_tokens, total_tokens |
s.cost | Float | Cost 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
endThe 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
endSpan Types
| Symbol | Value | Use for |
|---|---|---|
:AGENT | AGENT | Top-level agent function |
:LLM | LLM | LLM API calls |
:TOOL | TOOL | Tool / function calls |
:RETRIEVAL | RETRIEVAL | RAG / vector search |
:CHAIN | CHAIN | Pipeline steps |
:CUSTOM | CUSTOM | Everything 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 tableSupports 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"]
)
endSessions
# 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
endLifecycle
# force-flush all pending events
client.flush
# graceful shutdown — flushes and stops the daemon thread (5s timeout)
client.shutdownAlways 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.