Class: ClaudeAgentSDK::Instrumentation::OTelObserver
- Inherits:
-
Object
- Object
- ClaudeAgentSDK::Instrumentation::OTelObserver
- 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.
Constant Summary collapse
- TRACER_NAME =
'claude_agent_sdk'- MAX_ATTRIBUTE_LENGTH =
4096
Instance Method Summary collapse
-
#initialize(tracer_name: TRACER_NAME, **default_attributes) ⇒ OTelObserver
constructor
A new instance of OTelObserver.
- #on_close ⇒ Object
-
#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.
- #on_message(message) ⇒ Object
- #on_user_prompt(prompt) ⇒ Object
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 56 |
# 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 # first user prompt of the current trace @pending_prompt = nil # prompt that belongs to the NEXT trace (see on_user_prompt) @last_assistant_text = nil # capture last assistant text for trace output end |
Instance Method Details
#on_close ⇒ Object
112 113 114 115 116 117 118 119 |
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 112 def on_close finish_open_spans reset_session_buffers # Unlike the per-trace buffers, the pending next-trace prompt survives # end_trace/supersede resets by design — but the session is over now, # and a reused observer must not leak it into the next session. @pending_prompt = nil 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.
105 106 107 108 109 110 |
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 105 def on_error(error) return unless @root_span @root_span.record_exception(error) @root_span.status = OpenTelemetry::Trace::Status.error(error.) end |
#on_message(message) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 81 def () case when ClaudeAgentSDK::InitMessage start_trace() when ClaudeAgentSDK::AssistantMessage handle_assistant() when ClaudeAgentSDK::UserMessage handle_user() when ClaudeAgentSDK::ResultMessage end_trace() when ClaudeAgentSDK::APIRetryMessage record_retry_event() when ClaudeAgentSDK::RateLimitEvent record_rate_limit_event() when ClaudeAgentSDK::ToolProgressMessage record_tool_progress_event() end end |
#on_user_prompt(prompt) ⇒ Object
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/claude_agent_sdk/instrumentation/otel.rb', line 58 def on_user_prompt(prompt) text = prompt.to_s return if text.empty? if @root_span.nil? # Pre-init: buffered for the trace start_trace is about to open. # First prompt wins as the trace input. @first_user_input ||= text elsif @first_user_input.nil? # Open trace with no input yet (init arrived before the prompt). @first_user_input = text @root_span.set_attribute('input.value', truncate(text)) else # The open trace already has its input, so this prompt belongs to # the NEXT trace: either the current turn was interrupted (/clear — # a superseding init follows with no ResultMessage) or the user # queued the next turn mid-stream (its init follows the current # ResultMessage). A first-prompt-forever latch here dropped these # prompts from the new trace entirely. @pending_prompt ||= text end end |