activeagent 1.0.2 → 1.1.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/README.md +26 -0
  4. data/lib/active_agent/base.rb +4 -0
  5. data/lib/active_agent/concerns/view.rb +1 -1
  6. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +27 -6
  7. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +11 -1
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +15 -12
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +27 -7
  10. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +9 -0
  11. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +1 -1
  12. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +18 -2
  13. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +15 -3
  14. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +3 -1
  15. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +4 -4
  16. data/lib/active_agent/dashboard/config/routes.rb +5 -64
  17. data/lib/active_agent/dashboard/engine.rb +19 -15
  18. data/lib/active_agent/dashboard.rb +13 -3
  19. data/lib/active_agent/model_capabilities.rb +89 -0
  20. data/lib/active_agent/providers/_base_provider.rb +23 -2
  21. data/lib/active_agent/providers/anthropic/request.rb +3 -1
  22. data/lib/active_agent/providers/anthropic/transforms.rb +88 -0
  23. data/lib/active_agent/providers/bedrock_provider.rb +2 -2
  24. data/lib/active_agent/providers/concerns/exception_handler.rb +12 -1
  25. data/lib/active_agent/providers/errors.rb +140 -0
  26. data/lib/active_agent/providers/mock/request.rb +1 -4
  27. data/lib/active_agent/providers/ollama/chat/transforms.rb +9 -3
  28. data/lib/active_agent/providers/open_ai/chat_provider.rb +3 -3
  29. data/lib/active_agent/providers/requesty/_types.rb +16 -0
  30. data/lib/active_agent/providers/requesty/options.rb +39 -0
  31. data/lib/active_agent/providers/requesty_provider.rb +63 -0
  32. data/lib/active_agent/railtie.rb +5 -0
  33. data/lib/active_agent/telemetry/configuration.rb +112 -172
  34. data/lib/active_agent/telemetry/instrumentation.rb +137 -14
  35. data/lib/active_agent/telemetry/reporter.rb +12 -165
  36. data/lib/active_agent/telemetry/span.rb +18 -245
  37. data/lib/active_agent/telemetry/tracer.rb +67 -70
  38. data/lib/active_agent/telemetry.rb +1 -2
  39. data/lib/active_agent/version.rb +1 -1
  40. data/lib/active_agent.rb +5 -0
  41. data/lib/generators/active_agent/dashboard/install_generator.rb +30 -2
  42. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +41 -4
  43. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +20 -4
  44. metadata +39 -16
  45. data/lib/generators/active_agent/dashboard/install/install_generator.rb +0 -96
  46. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +0 -89
  47. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +0 -42
  48. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +0 -38
  49. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +0 -22
  50. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +0 -53
  51. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +0 -28
  52. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +0 -43
  53. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +0 -44
  54. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +0 -56
@@ -1,175 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
- require "json"
5
- require "uri"
6
-
7
3
  module ActiveAgent
8
4
  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.
5
+ # The framework's reporter, now provided by the activeagents-telemetry
6
+ # gem's BatchingReporter: buffered delivery on batch_size/flush_interval,
7
+ # a blocking flush, and a shutdown that waits out in-flight sends so
8
+ # traces reported near process exit are delivered rather than dropped.
13
9
  #
14
- # @example
15
- # reporter = Reporter.new(configuration)
16
- # reporter.report(trace_payload)
17
- # reporter.flush # Send immediately
18
- # reporter.shutdown # Clean shutdown
10
+ # Local storage is handled by Configuration#local_store, which routes
11
+ # traces through the dashboard's trace model instead of HTTP.
19
12
  #
20
- class Reporter
21
- # @return [Configuration] Telemetry configuration
22
- attr_reader :configuration
23
-
13
+ # @see ActiveAgents::Telemetry::BatchingReporter
14
+ class Reporter < ActiveAgents::Telemetry::BatchingReporter
24
15
  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}")
16
+ # sample: false — the Tracer applies head-based sampling at trace
17
+ # creation, before any span is built; sampling again here would
18
+ # compound the rate to rate².
19
+ super(configuration, sdk_name: "activeagent", sdk_version: ActiveAgent::VERSION, sample: false)
173
20
  end
174
21
  end
175
22
  end
@@ -2,103 +2,28 @@
2
2
 
3
3
  module ActiveAgent
4
4
  module Telemetry
5
- # Represents a single span in a trace.
5
+ # The framework's span, now provided by the activeagents-telemetry gem.
6
+ # This subclass translates the framework's historical constructor
7
+ # (span_type: keyword, attributes as a keyword splat) onto the shared
8
+ # class; everything else — set_attribute, set_tokens, set_status,
9
+ # record_error, add_span, measure, finish — is inherited.
6
10
  #
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.
11
+ # One deliberate change rides along from the shared core: record_error
12
+ # truncates the message and no longer puts a backtrace on the wire.
10
13
  #
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
14
+ # @see ActiveAgents::Telemetry::Span
15
+ class Span < ActiveAgents::Telemetry::Span
80
16
  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 }
17
+ super(
18
+ name,
19
+ type: span_type,
20
+ trace_id: trace_id,
21
+ parent_span_id: parent_span_id,
22
+ attributes: attributes
23
+ )
94
24
  end
95
25
 
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
26
+ # Creates a child span, keeping the framework's keyword shape.
102
27
  def add_span(name, span_type: :root, **attributes)
103
28
  child = Span.new(
104
29
  name,
@@ -107,161 +32,9 @@ module ActiveAgent
107
32
  span_type: span_type,
108
33
  **attributes
109
34
  )
110
- @children << child
35
+ children << child
111
36
  child
112
37
  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
38
  end
266
39
  end
267
40
  end
@@ -2,18 +2,17 @@
2
2
 
3
3
  module ActiveAgent
4
4
  module Telemetry
5
- # Manages trace creation and lifecycle.
5
+ # Manages trace creation and lifecycle on the activeagents-telemetry
6
+ # core: builds an ActiveAgents::Telemetry::Trace per unit of work, tracks
7
+ # the current span in thread-local storage, and hands finished traces to
8
+ # the Reporter.
6
9
  #
7
- # The Tracer creates traces, manages the current trace context,
8
- # and coordinates with the Reporter for async transmission.
9
- #
10
- # @example Basic usage
10
+ # @example
11
11
  # tracer = Tracer.new(configuration)
12
12
  # tracer.trace("MyAgent.greet") do |span|
13
13
  # span.set_attribute("user_id", 123)
14
14
  # span.add_span("llm.generate", span_type: :llm)
15
15
  # end
16
- #
17
16
  class Tracer
18
17
  # @return [Configuration] Telemetry configuration
19
18
  attr_reader :configuration
@@ -27,44 +26,47 @@ module ActiveAgent
27
26
  def initialize(configuration)
28
27
  @configuration = configuration
29
28
  @reporter = Reporter.new(configuration)
30
- @mutex = Mutex.new
31
29
  end
32
30
 
33
31
  # Creates and executes a new trace.
34
32
  #
33
+ # Callers may supply the trace id (instrumentation passes the
34
+ # generation's prompt_options[:trace_id]) so external records — e.g.
35
+ # solid_agent's persisted generations — can correlate with this trace;
36
+ # otherwise one is generated.
37
+ #
35
38
  # @param name [String] Trace name (typically "AgentClass.action")
36
- # @param attributes [Hash] Root span attributes
39
+ # @param attributes [Hash] Root span attributes; :trace_id and
40
+ # :span_type are extracted rather than recorded
37
41
  # @yield [span] Yields the root span for adding child spans
38
42
  # @return [Object] Result of the block
39
- #
40
- # @example
41
- # tracer.trace("WeatherAgent.forecast") do |span|
42
- # span.set_attribute("location", "Seattle")
43
- # result = do_llm_call
44
- # span.set_tokens(input: 100, output: 50)
45
- # result
46
- # end
47
- def trace(name, **attributes, &block)
43
+ def trace(name, **attributes)
48
44
  return yield(Telemetry::NullSpan.new) unless should_trace?
49
45
 
50
- trace_id = generate_trace_id
46
+ trace_id = attributes.delete(:trace_id) || SecureRandom.hex(16)
47
+ span_type = attributes.delete(:span_type) || :root
48
+
49
+ trace = build_trace(trace_id)
51
50
  root_span = Span.new(
52
51
  name,
53
52
  trace_id: trace_id,
54
- span_type: :root,
53
+ span_type: span_type,
55
54
  **default_attributes.merge(attributes)
56
55
  )
56
+ trace.add_span(root_span)
57
57
 
58
58
  with_span(root_span) do
59
59
  result = yield(root_span)
60
60
  root_span.finish
61
- report_trace(root_span)
61
+ reporter.report(redact_trace!(trace))
62
62
  result
63
63
  end
64
64
  rescue StandardError => e
65
- root_span&.record_error(e)
66
- root_span&.finish
67
- report_trace(root_span) if root_span
65
+ if root_span && !root_span.finished?
66
+ root_span.record_error(e)
67
+ root_span.finish
68
+ reporter.report(redact_trace!(trace))
69
+ end
68
70
  raise
69
71
  end
70
72
 
@@ -76,11 +78,12 @@ module ActiveAgent
76
78
  def span(name, **attributes)
77
79
  return Telemetry::NullSpan.new unless should_trace?
78
80
 
81
+ span_type = attributes.delete(:span_type) || :root
79
82
  current = current_span
80
83
  if current
81
- current.add_span(name, **attributes)
84
+ current.add_span(name, span_type: span_type, **attributes)
82
85
  else
83
- Span.new(name, trace_id: generate_trace_id, **default_attributes.merge(attributes))
86
+ Span.new(name, trace_id: SecureRandom.hex(16), span_type: span_type, **default_attributes.merge(attributes))
84
87
  end
85
88
  end
86
89
 
@@ -91,7 +94,9 @@ module ActiveAgent
91
94
  Thread.current[CURRENT_SPAN_KEY]
92
95
  end
93
96
 
94
- # Flushes buffered traces immediately.
97
+ # Flushes buffered traces and waits for delivery to complete, so
98
+ # callers (tests, rails runner, job shutdown) can rely on the traces
99
+ # having been delivered/stored when this returns.
95
100
  #
96
101
  # @return [void]
97
102
  def flush
@@ -107,11 +112,16 @@ module ActiveAgent
107
112
 
108
113
  private
109
114
 
115
+ def build_trace(trace_id)
116
+ ActiveAgents::Telemetry::Trace.new(
117
+ trace_id: trace_id,
118
+ service_name: configuration.resolved_service_name,
119
+ environment: configuration.resolved_environment,
120
+ resource_attributes: configuration.resource_attributes
121
+ )
122
+ end
123
+
110
124
  # Executes block with span as current context.
111
- #
112
- # @param span [Span] Span to set as current
113
- # @yield Block to execute
114
- # @return [Object] Result of block
115
125
  def with_span(span)
116
126
  previous = Thread.current[CURRENT_SPAN_KEY]
117
127
  Thread.current[CURRENT_SPAN_KEY] = span
@@ -120,61 +130,48 @@ module ActiveAgent
120
130
  Thread.current[CURRENT_SPAN_KEY] = previous
121
131
  end
122
132
 
123
- # Reports a completed trace to the reporter.
124
- #
125
- # @param span [Span] Root span of the trace
126
- def report_trace(span)
127
- reporter.report(build_trace_payload(span))
133
+ # Redacts span (and span-event) attributes whose keys match any
134
+ # configured redact_attributes entry, before the trace is handed to
135
+ # the reporter. Matching is case-insensitive substring deliberately
136
+ # over-broad: better to redact a harmless "max_tokens" than to ship
137
+ # an "api_key". (Ported from the pre-shared-core payload builder,
138
+ # which no longer exists — the gem's reporter ships the trace as-is.)
139
+ REDACTED = "[REDACTED]"
140
+
141
+ def redact_trace!(trace)
142
+ patterns = Array(configuration.redact_attributes).map(&:to_s).reject(&:empty?)
143
+ return trace if patterns.empty?
144
+
145
+ matcher = Regexp.union(patterns.map { |pattern| Regexp.new(Regexp.escape(pattern), Regexp::IGNORECASE) })
146
+ Array(trace.spans).each { |span| redact_span!(span, matcher) }
147
+ trace
128
148
  end
129
149
 
130
- # Builds the trace payload for transmission.
131
- #
132
- # @param root_span [Span] Root span
133
- # @return [Hash] Trace payload
134
- def build_trace_payload(root_span)
135
- {
136
- trace_id: root_span.trace_id,
137
- service_name: configuration.resolved_service_name,
138
- environment: configuration.environment,
139
- timestamp: Time.current.iso8601(6),
140
- resource_attributes: configuration.resource_attributes,
141
- spans: flatten_spans(root_span)
142
- }
150
+ def redact_span!(span, matcher)
151
+ redact_hash!(span.attributes, matcher) if span.attributes.is_a?(Hash)
152
+ Array(span.events).each do |event|
153
+ attributes = event.is_a?(Hash) ? (event["attributes"] || event[:attributes]) : nil
154
+ redact_hash!(attributes, matcher) if attributes.is_a?(Hash)
155
+ end
156
+ Array(span.children).each { |child| redact_span!(child, matcher) }
143
157
  end
144
158
 
145
- # Flattens span hierarchy into array.
146
- #
147
- # @param span [Span] Root span
148
- # @return [Array<Hash>] Flattened span data
149
- def flatten_spans(span)
150
- result = [ span.to_h.except(:children) ]
151
- span.children.each do |child|
152
- result.concat(flatten_spans(child))
159
+ def redact_hash!(attributes, matcher)
160
+ attributes.each_key do |key|
161
+ attributes[key] = REDACTED if key.to_s.match?(matcher)
153
162
  end
154
- result
155
163
  end
156
164
 
157
- # Returns whether this trace should be sampled.
158
- #
159
- # @return [Boolean]
165
+ # Returns whether this trace should be collected at all.
160
166
  def should_trace?
161
167
  configuration.enabled? && configuration.configured? && configuration.should_sample?
162
168
  end
163
169
 
164
- # Generates a unique trace ID.
165
- #
166
- # @return [String] 32-character hex trace ID
167
- def generate_trace_id
168
- SecureRandom.hex(16)
169
- end
170
-
171
170
  # Returns default attributes for all spans.
172
- #
173
- # @return [Hash] Default attributes
174
171
  def default_attributes
175
172
  {
176
173
  "service.name" => configuration.resolved_service_name,
177
- "service.environment" => configuration.environment,
174
+ "service.environment" => configuration.resolved_environment,
178
175
  "telemetry.sdk.name" => "activeagent",
179
176
  "telemetry.sdk.version" => ActiveAgent::VERSION
180
177
  }