activeagent 1.0.3 → 1.2.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -0
  3. data/README.md +39 -3
  4. data/lib/active_agent/base.rb +4 -0
  5. data/lib/active_agent/model_capabilities.rb +89 -0
  6. data/lib/active_agent/providers/_base_provider.rb +23 -2
  7. data/lib/active_agent/providers/concerns/exception_handler.rb +12 -1
  8. data/lib/active_agent/providers/errors.rb +140 -0
  9. data/lib/active_agent/providers/ollama/chat/transforms.rb +9 -3
  10. data/lib/active_agent/railtie.rb +5 -0
  11. data/lib/active_agent/telemetry/configuration.rb +115 -171
  12. data/lib/active_agent/telemetry/instrumentation.rb +141 -14
  13. data/lib/active_agent/telemetry/reporter.rb +12 -165
  14. data/lib/active_agent/telemetry/span.rb +18 -245
  15. data/lib/active_agent/telemetry/tool_origin.rb +90 -0
  16. data/lib/active_agent/telemetry/tracer.rb +67 -70
  17. data/lib/active_agent/telemetry.rb +2 -2
  18. data/lib/active_agent/version.rb +1 -1
  19. data/lib/active_agent.rb +1 -1
  20. metadata +38 -42
  21. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +0 -117
  22. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +0 -54
  23. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +0 -126
  24. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +0 -103
  25. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +0 -56
  26. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +0 -14
  27. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +0 -49
  28. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +0 -65
  29. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +0 -77
  30. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +0 -256
  31. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +0 -113
  32. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +0 -208
  33. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +0 -60
  34. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +0 -46
  35. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +0 -125
  36. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +0 -83
  37. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +0 -52
  38. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +0 -169
  39. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +0 -193
  40. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +0 -198
  41. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +0 -105
  42. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +0 -135
  43. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +0 -143
  44. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +0 -36
  45. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +0 -94
  46. data/lib/active_agent/dashboard/config/routes.rb +0 -78
  47. data/lib/active_agent/dashboard/engine.rb +0 -39
  48. data/lib/active_agent/dashboard.rb +0 -151
  49. data/lib/generators/active_agent/dashboard/install/install_generator.rb +0 -96
  50. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +0 -89
  51. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +0 -42
  52. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +0 -38
  53. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +0 -22
  54. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +0 -53
  55. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +0 -28
  56. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +0 -43
  57. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +0 -44
  58. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +0 -56
  59. data/lib/generators/active_agent/dashboard/install_generator.rb +0 -64
  60. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +0 -30
  61. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +0 -30
@@ -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
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Telemetry
5
+ # Classifies where a tool call came from, so dashboards can group tool
6
+ # traffic by the service that serves it instead of showing a flat list
7
+ # of names.
8
+ #
9
+ # Telemetry only ever sees the name a provider used to invoke the tool,
10
+ # so the origin has to be recovered from naming conventions. The MCP
11
+ # ecosystem settled on a namespaced form — +mcp__<server>__<tool>+ —
12
+ # which most clients (Claude Code, Cursor, the Ruby MCP clients) emit
13
+ # verbatim, and that is the strongest signal available. Everything else
14
+ # falls back to "the agent class defines this method".
15
+ #
16
+ # Classification happens at instrumentation time rather than at read
17
+ # time so the attribution is recorded in the span itself: a trace stays
18
+ # self-describing, and consumers (the gem dashboard, activeagents.ai,
19
+ # any OTLP exporter) all read the same fields instead of each
20
+ # re-implementing the guess.
21
+ #
22
+ # @example Namespaced MCP tool
23
+ # ToolOrigin.classify("mcp__playwright__browser_navigate")
24
+ # # => { origin: "mcp", server: "playwright", tool: "browser_navigate" }
25
+ #
26
+ # @example Agent-defined method
27
+ # ToolOrigin.classify("lookup_order")
28
+ # # => { origin: "agent", server: nil, tool: "lookup_order" }
29
+ module ToolOrigin
30
+ # +mcp__<server>__<tool>+. The tool half may itself contain "__", so
31
+ # only the first two segments are structural.
32
+ MCP_PATTERN = /\Amcp__([^_]+(?:_[^_]+)*?)__(.+)\z/
33
+
34
+ # Origin values written to +tool.origin+.
35
+ MCP = "mcp"
36
+ AGENT = "agent"
37
+
38
+ module_function
39
+
40
+ # Classifies a tool name.
41
+ #
42
+ # @param name [String, Symbol] the tool name as the provider invoked it
43
+ # @return [Hash] +:origin+ ("mcp" or "agent"), +:server+ (MCP server
44
+ # name or nil), and +:tool+ (the bare tool name with any namespace
45
+ # stripped)
46
+ def classify(name)
47
+ name = name.to_s
48
+
49
+ if (match = MCP_PATTERN.match(name))
50
+ { origin: MCP, server: match[1], tool: match[2] }
51
+ else
52
+ { origin: AGENT, server: nil, tool: name }
53
+ end
54
+ end
55
+
56
+ # Whether a tool name is namespaced to an MCP server.
57
+ #
58
+ # @param name [String, Symbol]
59
+ # @return [Boolean]
60
+ def mcp?(name)
61
+ MCP_PATTERN.match?(name.to_s)
62
+ end
63
+
64
+ # The MCP server a tool belongs to, if any.
65
+ #
66
+ # @param name [String, Symbol]
67
+ # @return [String, nil]
68
+ def server_for(name)
69
+ classify(name)[:server]
70
+ end
71
+
72
+ # Writes the classification onto a span. Only sets +tool.mcp_server+
73
+ # when there is a server to name, so agent-defined tools don't carry
74
+ # an empty attribute.
75
+ #
76
+ # @param span [ActiveAgent::Telemetry::Span]
77
+ # @param name [String, Symbol] the tool name
78
+ # @return [Hash] the classification, for callers that also want it
79
+ def annotate(span, name)
80
+ classification = classify(name)
81
+ span.set_attribute("tool.origin", classification[:origin])
82
+ if classification[:server]
83
+ span.set_attribute("tool.mcp_server", classification[:server])
84
+ span.set_attribute("tool.base_name", classification[:tool])
85
+ end
86
+ classification
87
+ end
88
+ end
89
+ end
90
+ end