activeagent 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +10 -4
- data/lib/active_agent/base.rb +3 -2
- data/lib/active_agent/concerns/provider.rb +6 -2
- data/lib/active_agent/concerns/rescue.rb +39 -0
- data/lib/active_agent/concerns/streaming.rb +2 -1
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
- data/lib/active_agent/dashboard/config/routes.rb +78 -0
- data/lib/active_agent/dashboard/engine.rb +39 -0
- data/lib/active_agent/dashboard.rb +151 -0
- data/lib/active_agent/providers/_base_provider.rb +2 -1
- data/lib/active_agent/providers/anthropic_provider.rb +14 -4
- data/lib/active_agent/providers/azure/_types.rb +5 -0
- data/lib/active_agent/providers/azure/options.rb +111 -0
- data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_provider.rb +133 -0
- data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
- data/lib/active_agent/providers/bedrock/_types.rb +8 -0
- data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
- data/lib/active_agent/providers/bedrock/options.rb +77 -0
- data/lib/active_agent/providers/bedrock_provider.rb +84 -0
- data/lib/active_agent/providers/common/messages/_types.rb +6 -2
- data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
- data/lib/active_agent/providers/gemini/_types.rb +19 -0
- data/lib/active_agent/providers/gemini/options.rb +41 -0
- data/lib/active_agent/providers/gemini_provider.rb +94 -0
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
- data/lib/active_agent/providers/open_ai/chat_provider.rb +2 -0
- data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
- data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
- data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
- data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
- data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
- data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
- data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
- data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
- data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
- data/lib/active_agent/railtie.rb +32 -1
- data/lib/active_agent/telemetry/configuration.rb +213 -0
- data/lib/active_agent/telemetry/instrumentation.rb +155 -0
- data/lib/active_agent/telemetry/reporter.rb +176 -0
- data/lib/active_agent/telemetry/span.rb +267 -0
- data/lib/active_agent/telemetry/tracer.rb +184 -0
- data/lib/active_agent/telemetry.rb +162 -0
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +2 -0
- data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
- data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
- data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
- metadata +99 -13
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Telemetry
|
|
5
|
+
# Auto-instrumentation for ActiveAgent generation lifecycle.
|
|
6
|
+
#
|
|
7
|
+
# When included in ActiveAgent::Base, automatically traces:
|
|
8
|
+
# - Agent generation (prompt_now, generate_now)
|
|
9
|
+
# - Tool calls
|
|
10
|
+
# - Streaming events
|
|
11
|
+
# - Errors
|
|
12
|
+
#
|
|
13
|
+
# @example Enabling instrumentation
|
|
14
|
+
# # In config/initializers/activeagent.rb
|
|
15
|
+
# ActiveAgent::Telemetry.configure do |config|
|
|
16
|
+
# config.enabled = true
|
|
17
|
+
# config.endpoint = "https://api.activeagents.ai/v1/traces"
|
|
18
|
+
# config.api_key = Rails.application.credentials.activeagents_api_key
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # Instrumentation is automatically applied when telemetry is enabled
|
|
22
|
+
#
|
|
23
|
+
module Instrumentation
|
|
24
|
+
extend ActiveSupport::Concern
|
|
25
|
+
|
|
26
|
+
included do
|
|
27
|
+
# Hook into generation lifecycle
|
|
28
|
+
around_generate :trace_generation if respond_to?(:around_generate)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class_methods do
|
|
32
|
+
# Installs instrumentation on the agent class.
|
|
33
|
+
#
|
|
34
|
+
# Called automatically when telemetry is enabled.
|
|
35
|
+
def instrument_telemetry!
|
|
36
|
+
return if @telemetry_instrumented
|
|
37
|
+
|
|
38
|
+
prepend GenerationInstrumentation
|
|
39
|
+
@telemetry_instrumented = true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Module prepended to intercept generation methods.
|
|
44
|
+
module GenerationInstrumentation
|
|
45
|
+
# Wraps process_prompt with telemetry tracing.
|
|
46
|
+
def process_prompt
|
|
47
|
+
return super unless Telemetry.enabled?
|
|
48
|
+
|
|
49
|
+
Telemetry.trace("#{self.class.name}.#{action_name}", span_type: :root) do |span|
|
|
50
|
+
span.set_attribute("agent.class", self.class.name)
|
|
51
|
+
span.set_attribute("agent.action", action_name.to_s)
|
|
52
|
+
span.set_attribute("agent.provider", provider_name) if respond_to?(:provider_name)
|
|
53
|
+
span.set_attribute("agent.model", model_name) if respond_to?(:model_name)
|
|
54
|
+
|
|
55
|
+
# Add prompt span
|
|
56
|
+
prompt_span = span.add_span("agent.prompt", span_type: :prompt)
|
|
57
|
+
prompt_span.set_attribute("messages.count", messages.size) if respond_to?(:messages)
|
|
58
|
+
prompt_span.finish
|
|
59
|
+
|
|
60
|
+
# Execute generation with LLM span
|
|
61
|
+
llm_span = span.add_span("llm.generate", span_type: :llm)
|
|
62
|
+
llm_span.set_attribute("llm.provider", provider_name) if respond_to?(:provider_name)
|
|
63
|
+
llm_span.set_attribute("llm.model", model_name) if respond_to?(:model_name)
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
result = super
|
|
67
|
+
|
|
68
|
+
# Record token usage from response
|
|
69
|
+
if result.respond_to?(:usage) && result.usage.present?
|
|
70
|
+
usage = result.usage
|
|
71
|
+
# Usage model uses methods, not hash access
|
|
72
|
+
input_tokens = (usage.input_tokens rescue 0) || 0
|
|
73
|
+
output_tokens = (usage.output_tokens rescue 0) || 0
|
|
74
|
+
reasoning_tokens = (usage.reasoning_tokens rescue 0) || 0
|
|
75
|
+
|
|
76
|
+
llm_span.set_tokens(
|
|
77
|
+
input: input_tokens.to_i,
|
|
78
|
+
output: output_tokens.to_i,
|
|
79
|
+
thinking: reasoning_tokens.to_i
|
|
80
|
+
)
|
|
81
|
+
span.set_tokens(
|
|
82
|
+
input: input_tokens.to_i,
|
|
83
|
+
output: output_tokens.to_i,
|
|
84
|
+
thinking: reasoning_tokens.to_i
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Record tool calls if present
|
|
89
|
+
if result.respond_to?(:tool_calls) && result.tool_calls.present?
|
|
90
|
+
result.tool_calls.each do |tool_call|
|
|
91
|
+
tool_span = span.add_span("tool.#{tool_call[:name]}", span_type: :tool)
|
|
92
|
+
tool_span.set_attribute("tool.name", tool_call[:name])
|
|
93
|
+
tool_span.set_attribute("tool.id", tool_call[:id]) if tool_call[:id]
|
|
94
|
+
tool_span.finish
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
llm_span.set_status(:ok)
|
|
99
|
+
llm_span.finish
|
|
100
|
+
span.set_status(:ok)
|
|
101
|
+
|
|
102
|
+
result
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
llm_span.record_error(e)
|
|
105
|
+
llm_span.finish
|
|
106
|
+
span.record_error(e)
|
|
107
|
+
raise
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Wraps process_embed with telemetry tracing.
|
|
113
|
+
def process_embed
|
|
114
|
+
return super unless Telemetry.enabled?
|
|
115
|
+
|
|
116
|
+
Telemetry.trace("#{self.class.name}.embed", span_type: :embedding) do |span|
|
|
117
|
+
span.set_attribute("agent.class", self.class.name)
|
|
118
|
+
span.set_attribute("agent.action", "embed")
|
|
119
|
+
span.set_attribute("agent.provider", provider_name) if respond_to?(:provider_name)
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
result = super
|
|
123
|
+
|
|
124
|
+
if result.respond_to?(:usage) && result.usage.present?
|
|
125
|
+
usage = result.usage
|
|
126
|
+
input_tokens = (usage.input_tokens rescue 0) || 0
|
|
127
|
+
span.set_tokens(input: input_tokens.to_i)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
span.set_status(:ok)
|
|
131
|
+
result
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
span.record_error(e)
|
|
134
|
+
raise
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def provider_name
|
|
142
|
+
self.class.generation_provider&.to_s || "unknown"
|
|
143
|
+
rescue StandardError
|
|
144
|
+
"unknown"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def model_name
|
|
148
|
+
prompt_options[:model] || "unknown"
|
|
149
|
+
rescue StandardError
|
|
150
|
+
"unknown"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module ActiveAgent
|
|
8
|
+
module Telemetry
|
|
9
|
+
# Asynchronously reports traces to the telemetry endpoint.
|
|
10
|
+
#
|
|
11
|
+
# Buffers traces and sends them in batches to reduce network overhead.
|
|
12
|
+
# Uses a background thread for non-blocking transmission.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# reporter = Reporter.new(configuration)
|
|
16
|
+
# reporter.report(trace_payload)
|
|
17
|
+
# reporter.flush # Send immediately
|
|
18
|
+
# reporter.shutdown # Clean shutdown
|
|
19
|
+
#
|
|
20
|
+
class Reporter
|
|
21
|
+
# @return [Configuration] Telemetry configuration
|
|
22
|
+
attr_reader :configuration
|
|
23
|
+
|
|
24
|
+
def initialize(configuration)
|
|
25
|
+
@configuration = configuration
|
|
26
|
+
@buffer = []
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
@running = false
|
|
29
|
+
@thread = nil
|
|
30
|
+
@shutdown = false
|
|
31
|
+
|
|
32
|
+
start_flush_thread if configuration.enabled?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Adds a trace to the buffer for transmission.
|
|
36
|
+
#
|
|
37
|
+
# @param trace [Hash] Trace payload
|
|
38
|
+
# @return [void]
|
|
39
|
+
def report(trace)
|
|
40
|
+
return unless configuration.enabled?
|
|
41
|
+
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
@buffer << trace
|
|
44
|
+
|
|
45
|
+
# Flush immediately if buffer is full
|
|
46
|
+
if @buffer.size >= configuration.batch_size
|
|
47
|
+
flush_buffer
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Flushes all buffered traces immediately.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def flush
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
flush_buffer
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Shuts down the reporter, flushing remaining traces.
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
64
|
+
def shutdown
|
|
65
|
+
@shutdown = true
|
|
66
|
+
flush
|
|
67
|
+
@thread&.join(5) # Wait up to 5 seconds for thread to finish
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Starts the background flush thread.
|
|
73
|
+
def start_flush_thread
|
|
74
|
+
@running = true
|
|
75
|
+
@thread = Thread.new do
|
|
76
|
+
Thread.current.name = "activeagent-telemetry-reporter"
|
|
77
|
+
|
|
78
|
+
while @running && !@shutdown
|
|
79
|
+
sleep(configuration.flush_interval)
|
|
80
|
+
|
|
81
|
+
@mutex.synchronize do
|
|
82
|
+
flush_buffer if @buffer.any?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Flushes the buffer by sending traces to the endpoint.
|
|
89
|
+
#
|
|
90
|
+
# Must be called within @mutex synchronization.
|
|
91
|
+
def flush_buffer
|
|
92
|
+
return if @buffer.empty?
|
|
93
|
+
|
|
94
|
+
traces = @buffer.dup
|
|
95
|
+
@buffer.clear
|
|
96
|
+
|
|
97
|
+
Thread.new { send_traces(traces) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Sends traces to the configured endpoint.
|
|
101
|
+
#
|
|
102
|
+
# @param traces [Array<Hash>] Traces to send
|
|
103
|
+
def send_traces(traces)
|
|
104
|
+
# Use direct database storage for local mode
|
|
105
|
+
if configuration.local_storage?
|
|
106
|
+
store_traces_locally(traces)
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
uri = URI.parse(configuration.endpoint)
|
|
111
|
+
|
|
112
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
113
|
+
http.use_ssl = uri.scheme == "https"
|
|
114
|
+
http.open_timeout = configuration.timeout
|
|
115
|
+
http.read_timeout = configuration.timeout
|
|
116
|
+
|
|
117
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
118
|
+
request["Content-Type"] = "application/json"
|
|
119
|
+
request["Authorization"] = "Bearer #{configuration.api_key}"
|
|
120
|
+
request["User-Agent"] = "ActiveAgent/#{ActiveAgent::VERSION} Ruby/#{RUBY_VERSION}"
|
|
121
|
+
request["X-Service-Name"] = configuration.resolved_service_name
|
|
122
|
+
request["X-Environment"] = configuration.environment
|
|
123
|
+
|
|
124
|
+
payload = {
|
|
125
|
+
traces: traces,
|
|
126
|
+
sdk: {
|
|
127
|
+
name: "activeagent",
|
|
128
|
+
version: ActiveAgent::VERSION,
|
|
129
|
+
language: "ruby",
|
|
130
|
+
runtime_version: RUBY_VERSION
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
request.body = JSON.generate(payload)
|
|
135
|
+
|
|
136
|
+
response = http.request(request)
|
|
137
|
+
|
|
138
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
139
|
+
log_error("Failed to send traces: #{response.code} #{response.message}")
|
|
140
|
+
end
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
log_error("Error sending traces: #{e.class} - #{e.message}")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Stores traces directly in the local database.
|
|
146
|
+
#
|
|
147
|
+
# @param traces [Array<Hash>] Traces to store
|
|
148
|
+
def store_traces_locally(traces)
|
|
149
|
+
sdk_info = {
|
|
150
|
+
name: "activeagent",
|
|
151
|
+
version: ActiveAgent::VERSION,
|
|
152
|
+
language: "ruby",
|
|
153
|
+
runtime_version: RUBY_VERSION
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
traces.each do |trace|
|
|
157
|
+
# Skip if trace already exists (idempotency)
|
|
158
|
+
next if ActiveAgent::TelemetryTrace.exists?(trace_id: trace["trace_id"])
|
|
159
|
+
|
|
160
|
+
ActiveAgent::TelemetryTrace.create_from_payload(trace, sdk_info)
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
log_error("Failed to store trace locally: #{e.class} - #{e.message}")
|
|
163
|
+
end
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
log_error("Error storing traces locally: #{e.class} - #{e.message}")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Logs an error message.
|
|
169
|
+
#
|
|
170
|
+
# @param message [String] Error message
|
|
171
|
+
def log_error(message)
|
|
172
|
+
configuration.resolved_logger.error("[ActiveAgent::Telemetry] #{message}")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Telemetry
|
|
5
|
+
# Represents a single span in a trace.
|
|
6
|
+
#
|
|
7
|
+
# Spans capture discrete operations within a trace, such as LLM calls,
|
|
8
|
+
# tool invocations, or prompt rendering. Each span has timing, attributes,
|
|
9
|
+
# and can have child spans.
|
|
10
|
+
#
|
|
11
|
+
# @example Creating a span
|
|
12
|
+
# span = Span.new("llm.generate", trace_id: trace.trace_id)
|
|
13
|
+
# span.set_attribute("provider", "anthropic")
|
|
14
|
+
# span.set_attribute("model", "claude-3-5-sonnet")
|
|
15
|
+
# span.set_tokens(input: 100, output: 50)
|
|
16
|
+
# span.finish
|
|
17
|
+
#
|
|
18
|
+
class Span
|
|
19
|
+
# Span types for categorization
|
|
20
|
+
TYPES = {
|
|
21
|
+
root: "root", # Root span for entire generation
|
|
22
|
+
prompt: "prompt", # Prompt preparation/rendering
|
|
23
|
+
llm: "llm", # LLM API call
|
|
24
|
+
tool: "tool", # Tool invocation
|
|
25
|
+
thinking: "thinking", # Extended thinking (Anthropic)
|
|
26
|
+
embedding: "embedding", # Embedding generation
|
|
27
|
+
error: "error" # Error handling
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Span status codes
|
|
31
|
+
STATUS = {
|
|
32
|
+
unset: "UNSET",
|
|
33
|
+
ok: "OK",
|
|
34
|
+
error: "ERROR"
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# @return [String] Unique identifier for this span
|
|
38
|
+
attr_reader :span_id
|
|
39
|
+
|
|
40
|
+
# @return [String] Trace ID this span belongs to
|
|
41
|
+
attr_reader :trace_id
|
|
42
|
+
|
|
43
|
+
# @return [String, nil] Parent span ID
|
|
44
|
+
attr_reader :parent_span_id
|
|
45
|
+
|
|
46
|
+
# @return [String] Span name (e.g., "llm.generate", "tool.get_weather")
|
|
47
|
+
attr_reader :name
|
|
48
|
+
|
|
49
|
+
# @return [String] Span type from TYPES
|
|
50
|
+
attr_reader :span_type
|
|
51
|
+
|
|
52
|
+
# @return [Time] When the span started
|
|
53
|
+
attr_reader :start_time
|
|
54
|
+
|
|
55
|
+
# @return [Time, nil] When the span ended
|
|
56
|
+
attr_reader :end_time
|
|
57
|
+
|
|
58
|
+
# @return [Hash] Span attributes
|
|
59
|
+
attr_reader :attributes
|
|
60
|
+
|
|
61
|
+
# @return [Array<Span>] Child spans
|
|
62
|
+
attr_reader :children
|
|
63
|
+
|
|
64
|
+
# @return [String] Status code from STATUS
|
|
65
|
+
attr_reader :status
|
|
66
|
+
|
|
67
|
+
# @return [String, nil] Status message
|
|
68
|
+
attr_reader :status_message
|
|
69
|
+
|
|
70
|
+
# @return [Array<Hash>] Events recorded during the span
|
|
71
|
+
attr_reader :events
|
|
72
|
+
|
|
73
|
+
# Creates a new span.
|
|
74
|
+
#
|
|
75
|
+
# @param name [String] Span name
|
|
76
|
+
# @param trace_id [String] Parent trace ID
|
|
77
|
+
# @param parent_span_id [String, nil] Parent span ID
|
|
78
|
+
# @param span_type [Symbol] Type of span
|
|
79
|
+
# @param attributes [Hash] Initial attributes
|
|
80
|
+
def initialize(name, trace_id:, parent_span_id: nil, span_type: :root, **attributes)
|
|
81
|
+
@span_id = SecureRandom.hex(8)
|
|
82
|
+
@trace_id = trace_id
|
|
83
|
+
@parent_span_id = parent_span_id
|
|
84
|
+
@name = name
|
|
85
|
+
@span_type = TYPES[span_type] || span_type.to_s
|
|
86
|
+
@start_time = Time.current
|
|
87
|
+
@end_time = nil
|
|
88
|
+
@attributes = attributes.transform_keys(&:to_s)
|
|
89
|
+
@children = []
|
|
90
|
+
@status = STATUS[:unset]
|
|
91
|
+
@status_message = nil
|
|
92
|
+
@events = []
|
|
93
|
+
@tokens = { input: 0, output: 0, thinking: 0, total: 0 }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Creates a child span.
|
|
97
|
+
#
|
|
98
|
+
# @param name [String] Child span name
|
|
99
|
+
# @param span_type [Symbol] Type of span
|
|
100
|
+
# @param attributes [Hash] Span attributes
|
|
101
|
+
# @return [Span] The child span
|
|
102
|
+
def add_span(name, span_type: :root, **attributes)
|
|
103
|
+
child = Span.new(
|
|
104
|
+
name,
|
|
105
|
+
trace_id: trace_id,
|
|
106
|
+
parent_span_id: span_id,
|
|
107
|
+
span_type: span_type,
|
|
108
|
+
**attributes
|
|
109
|
+
)
|
|
110
|
+
@children << child
|
|
111
|
+
child
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Sets a single attribute.
|
|
115
|
+
#
|
|
116
|
+
# @param key [String, Symbol] Attribute key
|
|
117
|
+
# @param value [Object] Attribute value
|
|
118
|
+
# @return [self]
|
|
119
|
+
def set_attribute(key, value)
|
|
120
|
+
@attributes[key.to_s] = value
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Sets multiple attributes at once.
|
|
125
|
+
#
|
|
126
|
+
# @param attrs [Hash] Attributes to set
|
|
127
|
+
# @return [self]
|
|
128
|
+
def set_attributes(attrs)
|
|
129
|
+
attrs.each { |k, v| set_attribute(k, v) }
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Sets token usage for LLM spans.
|
|
134
|
+
#
|
|
135
|
+
# @param input [Integer] Input token count
|
|
136
|
+
# @param output [Integer] Output token count
|
|
137
|
+
# @param thinking [Integer] Thinking token count (Anthropic extended thinking)
|
|
138
|
+
# @return [self]
|
|
139
|
+
def set_tokens(input: 0, output: 0, thinking: 0)
|
|
140
|
+
@tokens = {
|
|
141
|
+
input: input,
|
|
142
|
+
output: output,
|
|
143
|
+
thinking: thinking,
|
|
144
|
+
total: input + output + thinking
|
|
145
|
+
}
|
|
146
|
+
set_attribute("tokens.input", input)
|
|
147
|
+
set_attribute("tokens.output", output)
|
|
148
|
+
set_attribute("tokens.thinking", thinking) if thinking > 0
|
|
149
|
+
set_attribute("tokens.total", @tokens[:total])
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Returns token usage.
|
|
154
|
+
#
|
|
155
|
+
# @return [Hash] Token counts
|
|
156
|
+
def tokens
|
|
157
|
+
@tokens.dup
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Sets the span status.
|
|
161
|
+
#
|
|
162
|
+
# @param code [Symbol] Status code (:ok, :error, :unset)
|
|
163
|
+
# @param message [String, nil] Optional status message
|
|
164
|
+
# @return [self]
|
|
165
|
+
def set_status(code, message = nil)
|
|
166
|
+
@status = STATUS[code] || STATUS[:unset]
|
|
167
|
+
@status_message = message
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Records an error on the span.
|
|
172
|
+
#
|
|
173
|
+
# @param error [Exception] The error to record
|
|
174
|
+
# @return [self]
|
|
175
|
+
def record_error(error)
|
|
176
|
+
set_status(:error, error.message)
|
|
177
|
+
set_attribute("error.type", error.class.name)
|
|
178
|
+
set_attribute("error.message", error.message)
|
|
179
|
+
set_attribute("error.backtrace", error.backtrace&.first(10)&.join("\n"))
|
|
180
|
+
|
|
181
|
+
add_event("exception", {
|
|
182
|
+
"exception.type" => error.class.name,
|
|
183
|
+
"exception.message" => error.message,
|
|
184
|
+
"exception.stacktrace" => error.backtrace&.join("\n")
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
self
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Adds an event to the span.
|
|
191
|
+
#
|
|
192
|
+
# @param name [String] Event name
|
|
193
|
+
# @param attributes [Hash] Event attributes
|
|
194
|
+
# @return [self]
|
|
195
|
+
def add_event(name, attributes = {})
|
|
196
|
+
@events << {
|
|
197
|
+
name: name,
|
|
198
|
+
timestamp: Time.current.iso8601(6),
|
|
199
|
+
attributes: attributes.transform_keys(&:to_s)
|
|
200
|
+
}
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Marks the span as finished.
|
|
205
|
+
#
|
|
206
|
+
# @return [self]
|
|
207
|
+
def finish
|
|
208
|
+
@end_time = Time.current
|
|
209
|
+
set_status(:ok) if @status == STATUS[:unset]
|
|
210
|
+
self
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Returns whether the span is finished.
|
|
214
|
+
#
|
|
215
|
+
# @return [Boolean]
|
|
216
|
+
def finished?
|
|
217
|
+
!@end_time.nil?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Returns the duration in milliseconds.
|
|
221
|
+
#
|
|
222
|
+
# @return [Float, nil] Duration or nil if not finished
|
|
223
|
+
def duration_ms
|
|
224
|
+
return nil unless finished?
|
|
225
|
+
|
|
226
|
+
((@end_time - @start_time) * 1000).round(2)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Serializes the span for transmission.
|
|
230
|
+
#
|
|
231
|
+
# @return [Hash] Serialized span data
|
|
232
|
+
def to_h
|
|
233
|
+
{
|
|
234
|
+
span_id: span_id,
|
|
235
|
+
trace_id: trace_id,
|
|
236
|
+
parent_span_id: parent_span_id,
|
|
237
|
+
name: name,
|
|
238
|
+
type: span_type,
|
|
239
|
+
start_time: start_time.iso8601(6),
|
|
240
|
+
end_time: end_time&.iso8601(6),
|
|
241
|
+
duration_ms: duration_ms,
|
|
242
|
+
status: status,
|
|
243
|
+
status_message: status_message,
|
|
244
|
+
attributes: attributes,
|
|
245
|
+
tokens: tokens,
|
|
246
|
+
events: events,
|
|
247
|
+
children: children.map(&:to_h)
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Executes a block and records timing/errors.
|
|
252
|
+
#
|
|
253
|
+
# @yield Block to execute within the span
|
|
254
|
+
# @return [Object] Result of the block
|
|
255
|
+
def measure
|
|
256
|
+
result = yield
|
|
257
|
+
set_status(:ok)
|
|
258
|
+
result
|
|
259
|
+
rescue StandardError => e
|
|
260
|
+
record_error(e)
|
|
261
|
+
raise
|
|
262
|
+
ensure
|
|
263
|
+
finish
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|