Class: ClaudeAgentSDK::Instrumentation::OTelObserver

Inherits:
Object
  • Object
show all
Includes:
Observer
Defined in:
lib/claude_agent_sdk/instrumentation/otel.rb

Overview

OpenTelemetry observer that emits spans for Claude Agent SDK messages.

Uses standard gen_ai.* semantic conventions recognized by Langfuse, Datadog, Jaeger, and other OTel-compatible backends.

Requires the ‘opentelemetry-api` gem at runtime. Users must configure `opentelemetry-sdk` and an exporter (e.g., `opentelemetry-exporter-otlp`) themselves before creating this observer.

Examples:

With Langfuse via OTLP

require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'claude_agent_sdk/instrumentation'

OpenTelemetry::SDK.configure do |c|
  c.service_name = 'my-app'
  c.add_span_processor(
    OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
      OpenTelemetry::Exporter::OTLP::Exporter.new(
        endpoint: 'https://cloud.langfuse.com/api/public/otel/v1/traces',
        headers: { 'Authorization' => "Basic #{auth}" }
      )
    )
  )
end

observer = ClaudeAgentSDK::Instrumentation::OTelObserver.new
options = ClaudeAgentSDK::ClaudeAgentOptions.new(observers: [observer])
ClaudeAgentSDK.query(prompt: "Hello", options: options) { |msg| ... }

Constant Summary collapse

TRACER_NAME =
'claude_agent_sdk'
MAX_ATTRIBUTE_LENGTH =
4096

Instance Method Summary collapse

Constructor Details

#initialize(tracer_name: TRACER_NAME, **default_attributes) ⇒ OTelObserver

Returns a new instance of OTelObserver.



43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 43

def initialize(tracer_name: TRACER_NAME, **default_attributes)
  require 'opentelemetry'
  @tracer = OpenTelemetry.tracer_provider.tracer(
    tracer_name,
    defined?(ClaudeAgentSDK::VERSION) ? ClaudeAgentSDK::VERSION : '0.0.0'
  )
  @default_attributes = default_attributes
  @root_span = nil
  @root_context = nil
  @tool_spans = {} # tool_use_id => span
  @first_user_input = nil # capture first user prompt for trace input
  @last_assistant_text = nil # capture last assistant text for trace output
end

Instance Method Details

#on_closeObject



91
92
93
94
95
96
97
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 91

def on_close
  @tool_spans.each_value(&:finish)
  @tool_spans.clear
  @root_span&.finish
  @root_span = nil
  @root_context = nil
end

#on_error(error) ⇒ Object



84
85
86
87
88
89
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 84

def on_error(error)
  return unless @root_span

  @root_span.record_exception(error)
  @root_span.status = OpenTelemetry::Trace::Status.error(error.message)
end

#on_message(message) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 65

def on_message(message)
  case message
  when ClaudeAgentSDK::InitMessage
    start_trace(message)
  when ClaudeAgentSDK::AssistantMessage
    handle_assistant(message)
  when ClaudeAgentSDK::UserMessage
    handle_user(message)
  when ClaudeAgentSDK::ResultMessage
    end_trace(message)
  when ClaudeAgentSDK::APIRetryMessage
    record_retry_event(message)
  when ClaudeAgentSDK::RateLimitEvent
    record_rate_limit_event(message)
  when ClaudeAgentSDK::ToolProgressMessage
    record_tool_progress_event(message)
  end
end

#on_user_prompt(prompt) ⇒ Object



57
58
59
60
61
62
63
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 57

def on_user_prompt(prompt)
  return if @first_user_input # only capture the first prompt

  @first_user_input = prompt.to_s
  # If root span already exists, set immediately; otherwise start_trace will apply it
  @root_span&.set_attribute('input.value', truncate(@first_user_input)) unless @first_user_input.empty?
end