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



96
97
98
99
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 96

def on_close
  finish_open_spans
  reset_session_buffers
end

#on_error(error) ⇒ Object

Recording-only by design: a Client session can survive an error (the user may rescue one bad message and keep receiving), so finishing here would orphan later spans and break the next turn. Finish ownership stays with end_trace/on_close; start_trace also finishes any dangling span from a previous trace as the never-disconnected backstop.



89
90
91
92
93
94
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 89

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