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.
- checksums.yaml +4 -4
- data/.claude/commands/persona.md +9 -0
- data/.claude/commands/task.md +46 -1
- data/.rubocop.yml +13 -0
- data/.rubocop_custom/use_ux_helper.rb +44 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +12 -1
- data/Makefile +26 -7
- data/Makefile.common +50 -0
- data/bin/aictl +8 -1
- data/components/agent/Gemfile +1 -1
- data/components/agent/bin/langop-agent +7 -0
- data/docs/README.md +58 -0
- data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
- data/docs/cli-reference.md +274 -0
- data/docs/{dsl/constraints.md → constraints.md} +5 -5
- data/docs/how-agents-work.md +156 -0
- data/docs/installation.md +218 -0
- data/docs/quickstart.md +299 -0
- data/docs/understanding-generated-code.md +265 -0
- data/docs/using-tools.md +457 -0
- data/docs/webhooks.md +509 -0
- data/examples/ux_helpers_demo.rb +296 -0
- data/lib/language_operator/agent/base.rb +11 -1
- data/lib/language_operator/agent/executor.rb +23 -6
- data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
- data/lib/language_operator/agent/task_executor.rb +346 -63
- data/lib/language_operator/agent/web_server.rb +110 -14
- data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
- data/lib/language_operator/agent.rb +88 -2
- data/lib/language_operator/cli/base_command.rb +17 -11
- data/lib/language_operator/cli/command_loader.rb +72 -0
- data/lib/language_operator/cli/commands/agent/base.rb +837 -0
- data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
- data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
- data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
- data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
- data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
- data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
- data/lib/language_operator/cli/commands/cluster.rb +129 -84
- data/lib/language_operator/cli/commands/install.rb +1 -1
- data/lib/language_operator/cli/commands/model/base.rb +215 -0
- data/lib/language_operator/cli/commands/model/test.rb +165 -0
- data/lib/language_operator/cli/commands/persona.rb +16 -34
- data/lib/language_operator/cli/commands/quickstart.rb +3 -2
- data/lib/language_operator/cli/commands/status.rb +40 -67
- data/lib/language_operator/cli/commands/system/base.rb +44 -0
- data/lib/language_operator/cli/commands/system/exec.rb +147 -0
- data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
- data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
- data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
- data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
- data/lib/language_operator/cli/commands/system/schema.rb +92 -0
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
- data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
- data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
- data/lib/language_operator/cli/commands/tool/base.rb +271 -0
- data/lib/language_operator/cli/commands/tool/install.rb +255 -0
- data/lib/language_operator/cli/commands/tool/search.rb +69 -0
- data/lib/language_operator/cli/commands/tool/test.rb +115 -0
- data/lib/language_operator/cli/commands/use.rb +29 -6
- data/lib/language_operator/cli/errors/handler.rb +20 -17
- data/lib/language_operator/cli/errors/suggestions.rb +3 -5
- data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
- data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
- data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
- data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
- data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
- data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
- data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
- data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
- data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
- data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
- data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
- data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
- data/lib/language_operator/cli/main.rb +50 -40
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
- data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
- data/lib/language_operator/client/base.rb +28 -0
- data/lib/language_operator/client/config.rb +4 -1
- data/lib/language_operator/client/mcp_connector.rb +1 -1
- data/lib/language_operator/config/cluster_config.rb +3 -2
- data/lib/language_operator/config.rb +38 -11
- data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
- data/lib/language_operator/constants.rb +13 -0
- data/lib/language_operator/dsl/http.rb +127 -10
- data/lib/language_operator/dsl.rb +153 -6
- data/lib/language_operator/errors.rb +50 -0
- data/lib/language_operator/kubernetes/client.rb +11 -6
- data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +118 -34
- data/lib/language_operator/utils/secure_path.rb +74 -0
- data/lib/language_operator/utils.rb +7 -0
- data/lib/language_operator/validators.rb +54 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +10 -2
- data/synth/001/agent.rb +16 -15
- data/synth/001/output.log +27 -10
- data/synth/002/Makefile +10 -2
- data/synth/003/Makefile +1 -1
- data/synth/003/README.md +205 -133
- data/synth/003/agent.optimized.rb +66 -0
- data/synth/003/agent.synthesized.rb +41 -0
- metadata +111 -35
- data/docs/dsl/agent-reference.md +0 -604
- data/docs/dsl/mcp-integration.md +0 -1177
- data/docs/dsl/webhooks.md +0 -932
- data/docs/dsl/workflows.md +0 -744
- data/lib/language_operator/cli/commands/agent.rb +0 -1712
- data/lib/language_operator/cli/commands/model.rb +0 -366
- data/lib/language_operator/cli/commands/system.rb +0 -1259
- data/lib/language_operator/cli/commands/tool.rb +0 -654
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
- data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
- data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
- data/lib/language_operator/learning/optimizer.rb +0 -319
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -288
- data/lib/language_operator/learning/trace_analyzer.rb +0 -285
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
- data/lib/language_operator/ux/base.rb +0 -81
- data/lib/language_operator/ux/concerns/README.md +0 -155
- data/lib/language_operator/ux/concerns/headings.rb +0 -90
- data/lib/language_operator/ux/create_agent.rb +0 -255
- data/lib/language_operator/ux/create_model.rb +0 -267
- data/lib/language_operator/ux/quickstart.rb +0 -594
- data/synth/003/agent.rb +0 -41
- data/synth/003/output.log +0 -68
- /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
- /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
- /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
|