language-operator 0.1.59 → 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 (144) 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 +14 -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 +369 -68
  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 +31 -1
  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/task_definition.rb +7 -6
  95. data/lib/language_operator/dsl.rb +153 -6
  96. data/lib/language_operator/errors.rb +50 -0
  97. data/lib/language_operator/kubernetes/client.rb +11 -6
  98. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  99. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  100. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  101. data/lib/language_operator/type_coercion.rb +118 -34
  102. data/lib/language_operator/utils/secure_path.rb +74 -0
  103. data/lib/language_operator/utils.rb +7 -0
  104. data/lib/language_operator/validators.rb +54 -2
  105. data/lib/language_operator/version.rb +1 -1
  106. data/synth/001/Makefile +10 -2
  107. data/synth/001/agent.rb +16 -15
  108. data/synth/001/output.log +27 -10
  109. data/synth/002/Makefile +10 -2
  110. data/synth/003/Makefile +3 -3
  111. data/synth/003/README.md +205 -133
  112. data/synth/003/agent.optimized.rb +66 -0
  113. data/synth/003/agent.synthesized.rb +41 -0
  114. metadata +111 -35
  115. data/docs/dsl/agent-reference.md +0 -604
  116. data/docs/dsl/mcp-integration.md +0 -1177
  117. data/docs/dsl/webhooks.md +0 -932
  118. data/docs/dsl/workflows.md +0 -744
  119. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  120. data/lib/language_operator/cli/commands/model.rb +0 -366
  121. data/lib/language_operator/cli/commands/system.rb +0 -1259
  122. data/lib/language_operator/cli/commands/tool.rb +0 -654
  123. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  124. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  125. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -147
  126. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -218
  127. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -432
  128. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -236
  129. data/lib/language_operator/learning/optimizer.rb +0 -318
  130. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  131. data/lib/language_operator/learning/task_synthesizer.rb +0 -261
  132. data/lib/language_operator/learning/trace_analyzer.rb +0 -280
  133. data/lib/language_operator/templates/task_synthesis.tmpl +0 -97
  134. data/lib/language_operator/ux/base.rb +0 -81
  135. data/lib/language_operator/ux/concerns/README.md +0 -155
  136. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  137. data/lib/language_operator/ux/create_agent.rb +0 -255
  138. data/lib/language_operator/ux/create_model.rb +0 -267
  139. data/lib/language_operator/ux/quickstart.rb +0 -594
  140. data/synth/003/agent.rb +0 -41
  141. data/synth/003/output.log +0 -68
  142. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  143. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  144. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -1,432 +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
- # Additional attribute filters
220
- if filter[:attributes].is_a?(Hash)
221
- filter[:attributes].each do |key, value|
222
- expressions << "#{key} = '#{value}'"
223
- end
224
- end
225
-
226
- # Return filter expression (v5 format)
227
- if expressions.empty?
228
- { expression: '' }
229
- else
230
- { expression: expressions.join(' AND ') }
231
- end
232
- end
233
-
234
- # Build filter items for SigNoz query (legacy, kept for reference)
235
- #
236
- # @param filter [Hash] Filter criteria
237
- # @return [Hash] Filters structure
238
- def build_filters(filter)
239
- items = []
240
-
241
- # Filter by task name (tag attribute)
242
- if filter[:task_name]
243
- items << {
244
- key: {
245
- key: 'task.name',
246
- dataType: 'string',
247
- type: 'tag'
248
- },
249
- op: '=',
250
- value: filter[:task_name]
251
- }
252
- end
253
-
254
- # Additional attribute filters
255
- if filter[:attributes].is_a?(Hash)
256
- filter[:attributes].each do |key, value|
257
- items << {
258
- key: {
259
- key: key.to_s,
260
- dataType: infer_data_type(value),
261
- type: 'tag'
262
- },
263
- op: '=',
264
- value: value
265
- }
266
- end
267
- end
268
-
269
- {
270
- items: items,
271
- op: 'AND'
272
- }
273
- end
274
-
275
- # Infer SigNoz data type from value
276
- #
277
- # @param value [Object] Value to inspect
278
- # @return [String] Data type ('string', 'int64', 'float64', 'bool')
279
- def infer_data_type(value)
280
- case value
281
- when Integer then 'int64'
282
- when Float then 'float64'
283
- when TrueClass, FalseClass then 'bool'
284
- else 'string'
285
- end
286
- end
287
-
288
- # Execute HTTP query to SigNoz
289
- #
290
- # @param request_body [Hash] Request body
291
- # @return [Hash] Parsed response
292
- def execute_query(request_body)
293
- uri = URI.join(@endpoint, QUERY_PATH)
294
-
295
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 30, read_timeout: 60) do |http|
296
- request = Net::HTTP::Post.new(uri.path)
297
- request['Content-Type'] = 'application/json'
298
- request['SIGNOZ-API-KEY'] = @api_key if @api_key
299
- request.body = JSON.generate(request_body)
300
-
301
- @logger&.debug("SigNoz Query: #{JSON.pretty_generate(request_body)}")
302
-
303
- response = http.request(request)
304
-
305
- unless response.is_a?(Net::HTTPSuccess)
306
- @logger&.error("SigNoz Error Response: #{response.body}")
307
- raise "SigNoz query failed: #{response.code} #{response.message}"
308
- end
309
-
310
- JSON.parse(response.body, symbolize_names: true)
311
- end
312
- end
313
-
314
- # Parse SigNoz v5 response into normalized spans
315
- #
316
- # @param response [Hash] SigNoz API response
317
- # @return [Array<Hash>] Normalized span data
318
- def parse_response(response)
319
- # SigNoz v5 response structure:
320
- # {
321
- # data: {
322
- # data: {
323
- # results: [
324
- # {
325
- # queryName: 'A',
326
- # rows: [
327
- # { data: { spanID, traceID, ... }, timestamp: '...' }
328
- # ]
329
- # }
330
- # ]
331
- # }
332
- # }
333
- # }
334
-
335
- results = response.dig(:data, :data, :results) || []
336
- spans = []
337
-
338
- results.each do |result|
339
- rows = result[:rows] || []
340
- rows.each do |row|
341
- span_data = row[:data] || {}
342
- spans << normalize_span(span_data)
343
- end
344
- end
345
-
346
- spans
347
- end
348
-
349
- # Normalize SigNoz span to common format
350
- #
351
- # @param span_data [Hash] Raw SigNoz span
352
- # @return [Hash] Normalized span
353
- def normalize_span(span_data)
354
- {
355
- span_id: span_data[:spanID] || span_data[:span_id] || span_data[:span_id],
356
- trace_id: span_data[:traceID] || span_data[:trace_id] || span_data[:trace_id],
357
- name: span_data[:name] || span_data[:serviceName],
358
- timestamp: parse_timestamp(span_data[:timestamp]),
359
- duration_ms: (span_data[:durationNano] || span_data[:duration_nano] || 0) / 1_000_000.0,
360
- attributes: extract_attributes_from_span_data(span_data)
361
- }
362
- end
363
-
364
- # Parse SigNoz timestamp (v5 uses ISO 8601 strings, legacy uses nanoseconds)
365
- #
366
- # @param timestamp [String, Integer] ISO 8601 timestamp string or nanoseconds
367
- # @return [Time] Parsed time
368
- def parse_timestamp(timestamp)
369
- return Time.now unless timestamp
370
-
371
- # v5 returns ISO 8601 strings
372
- if timestamp.is_a?(String)
373
- Time.parse(timestamp)
374
- else
375
- # Legacy format: nanoseconds
376
- Time.at(timestamp / 1_000_000_000.0)
377
- end
378
- end
379
-
380
- # Extract attributes from flat span data structure
381
- #
382
- # SigNoz v5 returns selected fields as flat keys in the span data object.
383
- # We extract the attribute fields we requested in selectFields.
384
- #
385
- # @param span_data [Hash] Raw span data from SigNoz v5
386
- # @return [Hash] Extracted attributes
387
- def extract_attributes_from_span_data(span_data)
388
- attrs = extract_known_attributes(span_data)
389
- extract_tool_name_fallback(attrs, span_data)
390
- attrs
391
- end
392
-
393
- def extract_known_attributes(span_data)
394
- keys = %w[task.name task.input.keys task.input.count task.output.keys task.output.count
395
- gen_ai.operation.name gen_ai.tool.name gen_ai.tool.call.arguments.size
396
- gen_ai.tool.call.result.size]
397
- keys.each_with_object({}) do |key, attrs|
398
- val = span_data[key.to_sym]
399
- attrs[key] = val if val
400
- end
401
- end
402
-
403
- def extract_tool_name_fallback(attrs, span_data)
404
- return unless attrs['gen_ai.tool.name'].nil? && span_data[:name]&.start_with?('execute_tool.')
405
-
406
- tool_name = span_data[:name].sub('execute_tool.', '')
407
- attrs['gen_ai.tool.name'] = tool_name unless tool_name.empty?
408
- end
409
-
410
- # Parse SigNoz tag maps into flat attributes hash (legacy)
411
- #
412
- # @param string_tags [Hash] String tag map
413
- # @param number_tags [Hash] Number tag map
414
- # @return [Hash] Flat attributes
415
- def parse_attributes(string_tags, number_tags)
416
- attrs = {}
417
-
418
- (string_tags || {}).each do |key, value|
419
- attrs[key.to_s] = value
420
- end
421
-
422
- (number_tags || {}).each do |key, value|
423
- attrs[key.to_s] = value
424
- end
425
-
426
- attrs
427
- end
428
- end
429
- # rubocop:enable Metrics/ClassLength
430
- end
431
- end
432
- end
@@ -1,236 +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
- # Additional attribute filters
80
- if filter[:attributes].is_a?(Hash)
81
- filter[:attributes].each do |key, value|
82
- conditions << "span.\"#{escape_traceql_key(key)}\" = \"#{escape_traceql_value(value)}\""
83
- end
84
- end
85
-
86
- # Combine conditions with AND
87
- query = conditions.any? ? conditions.join(' && ') : ''
88
- "{ #{query} }"
89
- end
90
-
91
- # Escape TraceQL attribute key
92
- #
93
- # @param key [String, Symbol] Attribute key
94
- # @return [String] Escaped key
95
- def escape_traceql_key(key)
96
- key.to_s.gsub('"', '\"')
97
- end
98
-
99
- # Escape TraceQL value
100
- #
101
- # @param value [Object] Attribute value
102
- # @return [String] Escaped value
103
- def escape_traceql_value(value)
104
- value.to_s.gsub('"', '\"').gsub('\\', '\\\\')
105
- end
106
-
107
- # Search traces via Tempo HTTP API
108
- #
109
- # @param traceql_query [String] TraceQL query
110
- # @param times [Hash] Start and end times
111
- # @param limit [Integer] Result limit
112
- # @return [Array<Hash>] Trace data
113
- def search_traces(traceql_query, times, limit)
114
- uri = build_search_uri(traceql_query, times, limit)
115
-
116
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 5, read_timeout: 30) do |http|
117
- request = Net::HTTP::Get.new(uri.request_uri)
118
- request['Accept'] = 'application/json'
119
-
120
- response = http.request(request)
121
-
122
- raise "Tempo query failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
123
-
124
- result = JSON.parse(response.body, symbolize_names: true)
125
- result[:traces] || []
126
- end
127
- end
128
-
129
- # Build Tempo search URI with query parameters
130
- #
131
- # @param traceql_query [String] TraceQL query
132
- # @param times [Hash] Start and end times
133
- # @param limit [Integer] Result limit
134
- # @return [URI] Complete URI with query params
135
- def build_search_uri(traceql_query, times, limit)
136
- params = {
137
- q: traceql_query,
138
- limit: limit,
139
- start: times[:start].to_i, # Unix seconds
140
- end: times[:end].to_i
141
- }
142
-
143
- uri = URI.join(@endpoint, SEARCH_PATH)
144
- uri.query = URI.encode_www_form(params)
145
- uri
146
- end
147
-
148
- # Extract all spans from traces
149
- #
150
- # @param traces [Array<Hash>] Tempo trace data
151
- # @return [Array<Hash>] Normalized spans
152
- def extract_spans_from_traces(traces)
153
- spans = []
154
-
155
- traces.each do |trace|
156
- trace_id = trace[:traceID]
157
-
158
- # Tempo returns spanSets (matched span groups)
159
- (trace[:spanSets] || []).each do |span_set|
160
- (span_set[:spans] || []).each do |span_data|
161
- spans << normalize_span(span_data, trace_id)
162
- end
163
- end
164
- end
165
-
166
- spans
167
- end
168
-
169
- # Normalize Tempo span to common format
170
- #
171
- # @param span_data [Hash] Raw Tempo span
172
- # @param trace_id [String] Trace ID
173
- # @return [Hash] Normalized span
174
- def normalize_span(span_data, trace_id)
175
- {
176
- span_id: span_data[:spanID],
177
- trace_id: trace_id,
178
- name: span_data[:name] || 'unknown',
179
- timestamp: parse_timestamp(span_data[:startTimeUnixNano]),
180
- duration_ms: parse_duration(span_data[:durationNanos]),
181
- attributes: parse_attributes(span_data[:attributes])
182
- }
183
- end
184
-
185
- # Parse Tempo timestamp (nanoseconds) to Time
186
- #
187
- # @param timestamp [String, Integer] Timestamp in nanoseconds
188
- # @return [Time] Parsed time
189
- def parse_timestamp(timestamp)
190
- return Time.now unless timestamp
191
-
192
- nanos = timestamp.is_a?(String) ? timestamp.to_i : timestamp
193
- Time.at(nanos / 1_000_000_000.0)
194
- end
195
-
196
- # Parse Tempo duration (nanoseconds) to milliseconds
197
- #
198
- # @param duration [Integer] Duration in nanoseconds
199
- # @return [Float] Duration in milliseconds
200
- def parse_duration(duration)
201
- return 0.0 unless duration
202
-
203
- duration / 1_000_000.0
204
- end
205
-
206
- # Parse Tempo attributes into flat hash
207
- #
208
- # Tempo attributes format:
209
- # [
210
- # { key: "http.method", value: { stringValue: "GET" } },
211
- # { key: "http.status_code", value: { intValue: 200 } }
212
- # ]
213
- #
214
- # @param attributes [Array<Hash>] Attribute array
215
- # @return [Hash] Flat attributes
216
- def parse_attributes(attributes)
217
- return {} unless attributes.is_a?(Array)
218
-
219
- attributes.each_with_object({}) do |attr, hash|
220
- key = attr[:key].to_s
221
- value_obj = attr[:value] || {}
222
-
223
- # Extract value based on type
224
- value = value_obj[:stringValue] ||
225
- value_obj[:intValue] ||
226
- value_obj[:doubleValue] ||
227
- value_obj[:boolValue] ||
228
- value_obj[:bytesValue]
229
-
230
- hash[key] = value if value
231
- end
232
- end
233
- end
234
- end
235
- end
236
- end