language-operator 0.1.61 → 0.1.62

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -1,435 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'net/http'
4
- require 'json'
5
- require 'uri'
6
- require 'time'
7
- require_relative 'base_adapter'
8
-
9
- module LanguageOperator
10
- module Learning
11
- module Adapters
12
- # SigNoz backend adapter for trace queries
13
- #
14
- # Queries SigNoz's ClickHouse-backed trace storage via the /api/v5/query_range
15
- # HTTP endpoint. Supports filtering by span attributes with AND/OR logic.
16
- #
17
- # @example Basic usage
18
- # adapter = SignozAdapter.new(
19
- # 'https://example.signoz.io',
20
- # 'your-api-key'
21
- # )
22
- #
23
- # spans = adapter.query_spans(
24
- # filter: { task_name: 'fetch_data' },
25
- # time_range: (Time.now - 3600)..Time.now,
26
- # limit: 100
27
- # )
28
- # rubocop:disable Metrics/ClassLength
29
- class SignozAdapter < BaseAdapter
30
- # SigNoz query endpoint path
31
- QUERY_PATH = '/api/v5/query_range'
32
-
33
- # Check if SigNoz is available at endpoint
34
- #
35
- # @param endpoint [String] SigNoz endpoint URL
36
- # @param api_key [String, nil] API key for authentication (optional)
37
- # @return [Boolean] True if SigNoz API is reachable
38
- def self.available?(endpoint, api_key = nil)
39
- uri = URI.join(endpoint, QUERY_PATH)
40
-
41
- # Test with minimal POST request since HEAD returns HTML web UI
42
- response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 30, read_timeout: 30) do |http|
43
- request = Net::HTTP::Post.new(uri.path)
44
- request['Content-Type'] = 'application/json'
45
- request['SIGNOZ-API-KEY'] = api_key if api_key
46
- request.body = '{}'
47
- http.request(request)
48
- end
49
-
50
- # Accept both success (200) and error responses (400) - both indicate API is working
51
- # Reject only network/auth failures
52
- response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPClientError)
53
- rescue StandardError
54
- false
55
- end
56
-
57
- # Query spans from SigNoz
58
- #
59
- # @param filter [Hash] Filter criteria
60
- # @option filter [String] :task_name Task name to filter by
61
- # @param time_range [Range<Time>] Time range for query
62
- # @param limit [Integer] Maximum spans to return
63
- # @return [Array<Hash>] Normalized span data
64
- def query_spans(filter:, time_range:, limit:)
65
- times = parse_time_range(time_range)
66
-
67
- # First query: get task spans to find trace IDs
68
- task_request = build_query_request(filter, times, limit)
69
- task_response = execute_query(task_request)
70
- task_spans = parse_response(task_response)
71
-
72
- return task_spans if task_spans.empty?
73
-
74
- # Collect unique trace IDs
75
- trace_ids = task_spans.map { |s| s[:trace_id] }.compact.uniq
76
-
77
- return task_spans if trace_ids.empty?
78
-
79
- # Second query: get tool spans within those traces
80
- tool_spans = query_tool_spans_by_traces(trace_ids, times, limit * 10)
81
-
82
- # Merge task spans and tool spans
83
- task_spans + tool_spans
84
- end
85
-
86
- private
87
-
88
- # Query tool execution spans by trace IDs
89
- #
90
- # @param trace_ids [Array<String>] Trace IDs to query
91
- # @param times [Hash] Time range
92
- # @param limit [Integer] Max results
93
- # @return [Array<Hash>] Tool spans
94
- def query_tool_spans_by_traces(trace_ids, times, limit)
95
- return [] if trace_ids.empty?
96
-
97
- # Build filter for tool spans in these traces
98
- trace_filter = trace_ids.map { |id| "traceID = '#{id}'" }.join(' OR ')
99
- filter_expr = "(#{trace_filter}) AND gen_ai.operation.name = 'execute_tool'"
100
-
101
- request_body = build_tool_query_request(filter_expr, times, limit)
102
- response = execute_query(request_body)
103
- parse_response(response)
104
- rescue StandardError => e
105
- @logger.warn("Failed to query tool spans: #{e.message}")
106
- []
107
- end
108
-
109
- # Build query request for tool spans with explicit filter
110
- #
111
- # @param filter_expr [String] Filter expression
112
- # @param times [Hash] Time range
113
- # @param limit [Integer] Result limit
114
- # @return [Hash] Request body
115
- def build_tool_query_request(filter_expr, times, limit)
116
- {
117
- start: (times[:start].to_f * 1000).to_i,
118
- end: (times[:end].to_f * 1000).to_i,
119
- requestType: 'raw',
120
- variables: {},
121
- compositeQuery: {
122
- queries: [
123
- {
124
- type: 'builder_query',
125
- spec: {
126
- name: 'A',
127
- signal: 'traces',
128
- filter: { expression: filter_expr },
129
- selectFields: [
130
- { name: 'spanID' },
131
- { name: 'traceID' },
132
- { name: 'timestamp' },
133
- { name: 'durationNano' },
134
- { name: 'name' },
135
- { name: 'serviceName' },
136
- { name: 'gen_ai.operation.name' },
137
- { name: 'gen_ai.tool.name' },
138
- { name: 'gen_ai.tool.call.arguments.size' },
139
- { name: 'gen_ai.tool.call.result.size' }
140
- ],
141
- order: [{ key: { name: 'timestamp' }, direction: 'asc' }],
142
- limit: limit,
143
- offset: 0,
144
- disabled: false
145
- }
146
- }
147
- ]
148
- }
149
- }
150
- end
151
-
152
- # Build SigNoz v5 query request body
153
- #
154
- # @param filter [Hash] Filter criteria
155
- # @param times [Hash] Start and end times
156
- # @param limit [Integer] Result limit
157
- # @return [Hash] Request body
158
- # rubocop:disable Metrics/MethodLength
159
- def build_query_request(filter, times, limit)
160
- {
161
- start: (times[:start].to_f * 1000).to_i, # Unix milliseconds
162
- end: (times[:end].to_f * 1000).to_i,
163
- requestType: 'raw',
164
- variables: {},
165
- compositeQuery: {
166
- queries: [
167
- {
168
- type: 'builder_query',
169
- spec: {
170
- name: 'A',
171
- signal: 'traces',
172
- filter: build_filter_expression(filter),
173
- selectFields: [
174
- { name: 'spanID' },
175
- { name: 'traceID' },
176
- { name: 'timestamp' },
177
- { name: 'durationNano' },
178
- { name: 'name' },
179
- { name: 'serviceName' },
180
- { name: 'task.name' },
181
- { name: 'task.input.keys' },
182
- { name: 'task.input.count' },
183
- { name: 'task.output.keys' },
184
- { name: 'task.output.count' },
185
- { name: 'gen_ai.operation.name' },
186
- { name: 'gen_ai.tool.name' },
187
- { name: 'gen_ai.tool.call.arguments.size' },
188
- { name: 'gen_ai.tool.call.result.size' }
189
- ],
190
- order: [
191
- {
192
- key: { name: 'timestamp' },
193
- direction: 'desc'
194
- }
195
- ],
196
- limit: limit,
197
- offset: 0,
198
- disabled: false
199
- }
200
- }
201
- ]
202
- }
203
- }
204
- end
205
- # rubocop:enable Metrics/MethodLength
206
-
207
- # Build filter expression for SigNoz v5 query
208
- #
209
- # SigNoz v5 filter syntax: attribute_name = 'value' (attribute name unquoted)
210
- #
211
- # @param filter [Hash] Filter criteria
212
- # @return [Hash] Filter expression structure
213
- def build_filter_expression(filter)
214
- expressions = []
215
-
216
- # Filter by task name (attribute name should NOT be quoted)
217
- expressions << "task.name = '#{filter[:task_name]}'" if filter[:task_name]
218
-
219
- # Filter by agent name
220
- expressions << "agent.name = '#{filter[:agent_name]}'" if filter[:agent_name]
221
-
222
- # Additional attribute filters
223
- if filter[:attributes].is_a?(Hash)
224
- filter[:attributes].each do |key, value|
225
- expressions << "#{key} = '#{value}'"
226
- end
227
- end
228
-
229
- # Return filter expression (v5 format)
230
- if expressions.empty?
231
- { expression: '' }
232
- else
233
- { expression: expressions.join(' AND ') }
234
- end
235
- end
236
-
237
- # Build filter items for SigNoz query (legacy, kept for reference)
238
- #
239
- # @param filter [Hash] Filter criteria
240
- # @return [Hash] Filters structure
241
- def build_filters(filter)
242
- items = []
243
-
244
- # Filter by task name (tag attribute)
245
- if filter[:task_name]
246
- items << {
247
- key: {
248
- key: 'task.name',
249
- dataType: 'string',
250
- type: 'tag'
251
- },
252
- op: '=',
253
- value: filter[:task_name]
254
- }
255
- end
256
-
257
- # Additional attribute filters
258
- if filter[:attributes].is_a?(Hash)
259
- filter[:attributes].each do |key, value|
260
- items << {
261
- key: {
262
- key: key.to_s,
263
- dataType: infer_data_type(value),
264
- type: 'tag'
265
- },
266
- op: '=',
267
- value: value
268
- }
269
- end
270
- end
271
-
272
- {
273
- items: items,
274
- op: 'AND'
275
- }
276
- end
277
-
278
- # Infer SigNoz data type from value
279
- #
280
- # @param value [Object] Value to inspect
281
- # @return [String] Data type ('string', 'int64', 'float64', 'bool')
282
- def infer_data_type(value)
283
- case value
284
- when Integer then 'int64'
285
- when Float then 'float64'
286
- when TrueClass, FalseClass then 'bool'
287
- else 'string'
288
- end
289
- end
290
-
291
- # Execute HTTP query to SigNoz
292
- #
293
- # @param request_body [Hash] Request body
294
- # @return [Hash] Parsed response
295
- def execute_query(request_body)
296
- uri = URI.join(@endpoint, QUERY_PATH)
297
-
298
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 30, read_timeout: 60) do |http|
299
- request = Net::HTTP::Post.new(uri.path)
300
- request['Content-Type'] = 'application/json'
301
- request['SIGNOZ-API-KEY'] = @api_key if @api_key
302
- request.body = JSON.generate(request_body)
303
-
304
- @logger&.debug("SigNoz Query: #{JSON.pretty_generate(request_body)}")
305
-
306
- response = http.request(request)
307
-
308
- unless response.is_a?(Net::HTTPSuccess)
309
- @logger&.error("SigNoz Error Response: #{response.body}")
310
- raise "SigNoz query failed: #{response.code} #{response.message}"
311
- end
312
-
313
- JSON.parse(response.body, symbolize_names: true)
314
- end
315
- end
316
-
317
- # Parse SigNoz v5 response into normalized spans
318
- #
319
- # @param response [Hash] SigNoz API response
320
- # @return [Array<Hash>] Normalized span data
321
- def parse_response(response)
322
- # SigNoz v5 response structure:
323
- # {
324
- # data: {
325
- # data: {
326
- # results: [
327
- # {
328
- # queryName: 'A',
329
- # rows: [
330
- # { data: { spanID, traceID, ... }, timestamp: '...' }
331
- # ]
332
- # }
333
- # ]
334
- # }
335
- # }
336
- # }
337
-
338
- results = response.dig(:data, :data, :results) || []
339
- spans = []
340
-
341
- results.each do |result|
342
- rows = result[:rows] || []
343
- rows.each do |row|
344
- span_data = row[:data] || {}
345
- spans << normalize_span(span_data)
346
- end
347
- end
348
-
349
- spans
350
- end
351
-
352
- # Normalize SigNoz span to common format
353
- #
354
- # @param span_data [Hash] Raw SigNoz span
355
- # @return [Hash] Normalized span
356
- def normalize_span(span_data)
357
- {
358
- span_id: span_data[:spanID] || span_data[:span_id] || span_data[:span_id],
359
- trace_id: span_data[:traceID] || span_data[:trace_id] || span_data[:trace_id],
360
- name: span_data[:name] || span_data[:serviceName],
361
- timestamp: parse_timestamp(span_data[:timestamp]),
362
- duration_ms: (span_data[:durationNano] || span_data[:duration_nano] || 0) / 1_000_000.0,
363
- attributes: extract_attributes_from_span_data(span_data)
364
- }
365
- end
366
-
367
- # Parse SigNoz timestamp (v5 uses ISO 8601 strings, legacy uses nanoseconds)
368
- #
369
- # @param timestamp [String, Integer] ISO 8601 timestamp string or nanoseconds
370
- # @return [Time] Parsed time
371
- def parse_timestamp(timestamp)
372
- return Time.now unless timestamp
373
-
374
- # v5 returns ISO 8601 strings
375
- if timestamp.is_a?(String)
376
- Time.parse(timestamp)
377
- else
378
- # Legacy format: nanoseconds
379
- Time.at(timestamp / 1_000_000_000.0)
380
- end
381
- end
382
-
383
- # Extract attributes from flat span data structure
384
- #
385
- # SigNoz v5 returns selected fields as flat keys in the span data object.
386
- # We extract the attribute fields we requested in selectFields.
387
- #
388
- # @param span_data [Hash] Raw span data from SigNoz v5
389
- # @return [Hash] Extracted attributes
390
- def extract_attributes_from_span_data(span_data)
391
- attrs = extract_known_attributes(span_data)
392
- extract_tool_name_fallback(attrs, span_data)
393
- attrs
394
- end
395
-
396
- def extract_known_attributes(span_data)
397
- keys = %w[task.name task.input.keys task.input.count task.output.keys task.output.count
398
- gen_ai.operation.name gen_ai.tool.name gen_ai.tool.call.arguments.size
399
- gen_ai.tool.call.result.size]
400
- keys.each_with_object({}) do |key, attrs|
401
- val = span_data[key.to_sym]
402
- attrs[key] = val if val
403
- end
404
- end
405
-
406
- def extract_tool_name_fallback(attrs, span_data)
407
- return unless attrs['gen_ai.tool.name'].nil? && span_data[:name]&.start_with?('execute_tool.')
408
-
409
- tool_name = span_data[:name].sub('execute_tool.', '')
410
- attrs['gen_ai.tool.name'] = tool_name unless tool_name.empty?
411
- end
412
-
413
- # Parse SigNoz tag maps into flat attributes hash (legacy)
414
- #
415
- # @param string_tags [Hash] String tag map
416
- # @param number_tags [Hash] Number tag map
417
- # @return [Hash] Flat attributes
418
- def parse_attributes(string_tags, number_tags)
419
- attrs = {}
420
-
421
- (string_tags || {}).each do |key, value|
422
- attrs[key.to_s] = value
423
- end
424
-
425
- (number_tags || {}).each do |key, value|
426
- attrs[key.to_s] = value
427
- end
428
-
429
- attrs
430
- end
431
- end
432
- # rubocop:enable Metrics/ClassLength
433
- end
434
- end
435
- end
@@ -1,239 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'net/http'
4
- require 'json'
5
- require 'uri'
6
- require_relative 'base_adapter'
7
-
8
- module LanguageOperator
9
- module Learning
10
- module Adapters
11
- # Grafana Tempo backend adapter for trace queries
12
- #
13
- # Queries Tempo's Parquet-backed trace storage via the /api/search
14
- # HTTP endpoint with TraceQL query language support.
15
- #
16
- # TraceQL provides powerful span filtering with structural operators:
17
- # - { span.attribute = "value" } - Basic attribute filtering
18
- # - { span.foo = "bar" && span.baz > 100 } - Multiple conditions
19
- # - { span.parent } >> { span.child } - Structural relationships
20
- #
21
- # @example Basic usage
22
- # adapter = TempoAdapter.new('http://tempo:3200')
23
- #
24
- # spans = adapter.query_spans(
25
- # filter: { task_name: 'fetch_data' },
26
- # time_range: (Time.now - 3600)..Time.now,
27
- # limit: 100
28
- # )
29
- class TempoAdapter < BaseAdapter
30
- # Tempo search endpoint
31
- SEARCH_PATH = '/api/search'
32
-
33
- # Check if Tempo is available at endpoint
34
- #
35
- # @param endpoint [String] Tempo endpoint URL
36
- # @param _api_key [String, nil] API key (unused, Tempo typically doesn't require auth)
37
- # @return [Boolean] True if Tempo API is reachable
38
- def self.available?(endpoint, _api_key = nil)
39
- uri = URI.join(endpoint, SEARCH_PATH)
40
- # Test with minimal query
41
- uri.query = URI.encode_www_form(q: '{ }', limit: 1)
42
-
43
- response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 2, read_timeout: 2) do |http|
44
- request = Net::HTTP::Get.new(uri.request_uri)
45
- http.request(request)
46
- end
47
-
48
- response.is_a?(Net::HTTPSuccess)
49
- rescue StandardError
50
- false
51
- end
52
-
53
- # Query spans from Tempo using TraceQL
54
- #
55
- # @param filter [Hash] Filter criteria
56
- # @option filter [String] :task_name Task name to filter by
57
- # @param time_range [Range<Time>] Time range for query
58
- # @param limit [Integer] Maximum traces to return
59
- # @return [Array<Hash>] Normalized span data
60
- def query_spans(filter:, time_range:, limit:)
61
- times = parse_time_range(time_range)
62
- traceql_query = build_traceql_query(filter)
63
- traces = search_traces(traceql_query, times, limit)
64
- extract_spans_from_traces(traces)
65
- end
66
-
67
- private
68
-
69
- # Build TraceQL query from filter
70
- #
71
- # @param filter [Hash] Filter criteria
72
- # @return [String] TraceQL query string
73
- def build_traceql_query(filter)
74
- conditions = []
75
-
76
- # Filter by task name
77
- conditions << "span.\"task.name\" = \"#{escape_traceql_value(filter[:task_name])}\"" if filter[:task_name]
78
-
79
- # Filter by agent name
80
- conditions << "span.\"agent.name\" = \"#{escape_traceql_value(filter[:agent_name])}\"" if filter[:agent_name]
81
-
82
- # Additional attribute filters
83
- if filter[:attributes].is_a?(Hash)
84
- filter[:attributes].each do |key, value|
85
- conditions << "span.\"#{escape_traceql_key(key)}\" = \"#{escape_traceql_value(value)}\""
86
- end
87
- end
88
-
89
- # Combine conditions with AND
90
- query = conditions.any? ? conditions.join(' && ') : ''
91
- "{ #{query} }"
92
- end
93
-
94
- # Escape TraceQL attribute key
95
- #
96
- # @param key [String, Symbol] Attribute key
97
- # @return [String] Escaped key
98
- def escape_traceql_key(key)
99
- key.to_s.gsub('"', '\"')
100
- end
101
-
102
- # Escape TraceQL value
103
- #
104
- # @param value [Object] Attribute value
105
- # @return [String] Escaped value
106
- def escape_traceql_value(value)
107
- value.to_s.gsub('"', '\"').gsub('\\', '\\\\')
108
- end
109
-
110
- # Search traces via Tempo HTTP API
111
- #
112
- # @param traceql_query [String] TraceQL query
113
- # @param times [Hash] Start and end times
114
- # @param limit [Integer] Result limit
115
- # @return [Array<Hash>] Trace data
116
- def search_traces(traceql_query, times, limit)
117
- uri = build_search_uri(traceql_query, times, limit)
118
-
119
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 5, read_timeout: 30) do |http|
120
- request = Net::HTTP::Get.new(uri.request_uri)
121
- request['Accept'] = 'application/json'
122
-
123
- response = http.request(request)
124
-
125
- raise "Tempo query failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
126
-
127
- result = JSON.parse(response.body, symbolize_names: true)
128
- result[:traces] || []
129
- end
130
- end
131
-
132
- # Build Tempo search URI with query parameters
133
- #
134
- # @param traceql_query [String] TraceQL query
135
- # @param times [Hash] Start and end times
136
- # @param limit [Integer] Result limit
137
- # @return [URI] Complete URI with query params
138
- def build_search_uri(traceql_query, times, limit)
139
- params = {
140
- q: traceql_query,
141
- limit: limit,
142
- start: times[:start].to_i, # Unix seconds
143
- end: times[:end].to_i
144
- }
145
-
146
- uri = URI.join(@endpoint, SEARCH_PATH)
147
- uri.query = URI.encode_www_form(params)
148
- uri
149
- end
150
-
151
- # Extract all spans from traces
152
- #
153
- # @param traces [Array<Hash>] Tempo trace data
154
- # @return [Array<Hash>] Normalized spans
155
- def extract_spans_from_traces(traces)
156
- spans = []
157
-
158
- traces.each do |trace|
159
- trace_id = trace[:traceID]
160
-
161
- # Tempo returns spanSets (matched span groups)
162
- (trace[:spanSets] || []).each do |span_set|
163
- (span_set[:spans] || []).each do |span_data|
164
- spans << normalize_span(span_data, trace_id)
165
- end
166
- end
167
- end
168
-
169
- spans
170
- end
171
-
172
- # Normalize Tempo span to common format
173
- #
174
- # @param span_data [Hash] Raw Tempo span
175
- # @param trace_id [String] Trace ID
176
- # @return [Hash] Normalized span
177
- def normalize_span(span_data, trace_id)
178
- {
179
- span_id: span_data[:spanID],
180
- trace_id: trace_id,
181
- name: span_data[:name] || 'unknown',
182
- timestamp: parse_timestamp(span_data[:startTimeUnixNano]),
183
- duration_ms: parse_duration(span_data[:durationNanos]),
184
- attributes: parse_attributes(span_data[:attributes])
185
- }
186
- end
187
-
188
- # Parse Tempo timestamp (nanoseconds) to Time
189
- #
190
- # @param timestamp [String, Integer] Timestamp in nanoseconds
191
- # @return [Time] Parsed time
192
- def parse_timestamp(timestamp)
193
- return Time.now unless timestamp
194
-
195
- nanos = timestamp.is_a?(String) ? timestamp.to_i : timestamp
196
- Time.at(nanos / 1_000_000_000.0)
197
- end
198
-
199
- # Parse Tempo duration (nanoseconds) to milliseconds
200
- #
201
- # @param duration [Integer] Duration in nanoseconds
202
- # @return [Float] Duration in milliseconds
203
- def parse_duration(duration)
204
- return 0.0 unless duration
205
-
206
- duration / 1_000_000.0
207
- end
208
-
209
- # Parse Tempo attributes into flat hash
210
- #
211
- # Tempo attributes format:
212
- # [
213
- # { key: "http.method", value: { stringValue: "GET" } },
214
- # { key: "http.status_code", value: { intValue: 200 } }
215
- # ]
216
- #
217
- # @param attributes [Array<Hash>] Attribute array
218
- # @return [Hash] Flat attributes
219
- def parse_attributes(attributes)
220
- return {} unless attributes.is_a?(Array)
221
-
222
- attributes.each_with_object({}) do |attr, hash|
223
- key = attr[:key].to_s
224
- value_obj = attr[:value] || {}
225
-
226
- # Extract value based on type
227
- value = value_obj[:stringValue] ||
228
- value_obj[:intValue] ||
229
- value_obj[:doubleValue] ||
230
- value_obj[:boolValue] ||
231
- value_obj[:bytesValue]
232
-
233
- hash[key] = value if value
234
- end
235
- end
236
- end
237
- end
238
- end
239
- end