language-operator 0.0.1 → 0.1.31
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/.rubocop.yml +125 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +82 -0
- data/README.md +3 -11
- data/Rakefile +63 -0
- data/bin/aictl +7 -0
- data/completions/_aictl +232 -0
- data/completions/aictl.bash +121 -0
- data/completions/aictl.fish +114 -0
- data/docs/architecture/agent-runtime.md +585 -0
- data/docs/dsl/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +604 -0
- data/docs/dsl/best-practices.md +1078 -0
- data/docs/dsl/chat-endpoints.md +895 -0
- data/docs/dsl/constraints.md +671 -0
- data/docs/dsl/mcp-integration.md +1177 -0
- data/docs/dsl/webhooks.md +932 -0
- data/docs/dsl/workflows.md +744 -0
- data/lib/language_operator/agent/base.rb +110 -0
- data/lib/language_operator/agent/executor.rb +440 -0
- data/lib/language_operator/agent/instrumentation.rb +54 -0
- data/lib/language_operator/agent/metrics_tracker.rb +183 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
- data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
- data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
- data/lib/language_operator/agent/safety/content_filter.rb +93 -0
- data/lib/language_operator/agent/safety/manager.rb +207 -0
- data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
- data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
- data/lib/language_operator/agent/scheduler.rb +183 -0
- data/lib/language_operator/agent/telemetry.rb +116 -0
- data/lib/language_operator/agent/web_server.rb +610 -0
- data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
- data/lib/language_operator/agent.rb +149 -0
- data/lib/language_operator/cli/commands/agent.rb +1205 -0
- data/lib/language_operator/cli/commands/cluster.rb +371 -0
- data/lib/language_operator/cli/commands/install.rb +404 -0
- data/lib/language_operator/cli/commands/model.rb +266 -0
- data/lib/language_operator/cli/commands/persona.rb +393 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +143 -0
- data/lib/language_operator/cli/commands/system.rb +772 -0
- data/lib/language_operator/cli/commands/tool.rb +537 -0
- data/lib/language_operator/cli/commands/use.rb +47 -0
- data/lib/language_operator/cli/errors/handler.rb +180 -0
- data/lib/language_operator/cli/errors/suggestions.rb +176 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
- data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
- data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
- data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
- data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
- data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
- data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
- data/lib/language_operator/cli/main.rb +236 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
- data/lib/language_operator/client/base.rb +214 -0
- data/lib/language_operator/client/config.rb +136 -0
- data/lib/language_operator/client/cost_calculator.rb +37 -0
- data/lib/language_operator/client/mcp_connector.rb +123 -0
- data/lib/language_operator/client.rb +19 -0
- data/lib/language_operator/config/cluster_config.rb +101 -0
- data/lib/language_operator/config/tool_patterns.yaml +57 -0
- data/lib/language_operator/config/tool_registry.rb +96 -0
- data/lib/language_operator/config.rb +138 -0
- data/lib/language_operator/dsl/adapter.rb +124 -0
- data/lib/language_operator/dsl/agent_context.rb +90 -0
- data/lib/language_operator/dsl/agent_definition.rb +427 -0
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
- data/lib/language_operator/dsl/config.rb +119 -0
- data/lib/language_operator/dsl/context.rb +50 -0
- data/lib/language_operator/dsl/execution_context.rb +47 -0
- data/lib/language_operator/dsl/helpers.rb +109 -0
- data/lib/language_operator/dsl/http.rb +184 -0
- data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
- data/lib/language_operator/dsl/parameter_definition.rb +124 -0
- data/lib/language_operator/dsl/registry.rb +36 -0
- data/lib/language_operator/dsl/schema.rb +1102 -0
- data/lib/language_operator/dsl/shell.rb +125 -0
- data/lib/language_operator/dsl/tool_definition.rb +112 -0
- data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
- data/lib/language_operator/dsl/webhook_definition.rb +106 -0
- data/lib/language_operator/dsl/workflow_definition.rb +259 -0
- data/lib/language_operator/dsl.rb +161 -0
- data/lib/language_operator/errors.rb +60 -0
- data/lib/language_operator/kubernetes/client.rb +279 -0
- data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
- data/lib/language_operator/loggable.rb +47 -0
- data/lib/language_operator/logger.rb +141 -0
- data/lib/language_operator/retry.rb +123 -0
- data/lib/language_operator/retryable.rb +132 -0
- data/lib/language_operator/templates/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
- data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
- data/lib/language_operator/templates/schema/.gitkeep +0 -0
- data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
- data/lib/language_operator/tool_loader.rb +242 -0
- data/lib/language_operator/validators.rb +170 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +65 -3
- data/requirements/tasks/challenge.md +9 -0
- data/requirements/tasks/iterate.md +36 -0
- data/requirements/tasks/optimize.md +21 -0
- data/requirements/tasks/tag.md +5 -0
- data/test_agent_dsl.rb +108 -0
- metadata +507 -20
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module Agent
|
|
5
|
+
module Safety
|
|
6
|
+
# Executes Ruby code in a sandboxed context with method whitelisting
|
|
7
|
+
# Wraps the execution context to prevent dangerous method calls at runtime
|
|
8
|
+
class SafeExecutor
|
|
9
|
+
class SecurityError < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Methods that are always safe to call
|
|
12
|
+
ALWAYS_SAFE_METHODS = %i[
|
|
13
|
+
nil? == != eql? equal? hash object_id class is_a? kind_of? instance_of?
|
|
14
|
+
respond_to? send_to? methods public_methods private_methods
|
|
15
|
+
instance_variables instance_variable_get instance_variable_set
|
|
16
|
+
to_s to_str inspect to_a to_h to_i to_f to_sym
|
|
17
|
+
freeze frozen? dup clone
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(context, validator: nil)
|
|
21
|
+
@context = context
|
|
22
|
+
@validator = validator || ASTValidator.new
|
|
23
|
+
@audit_log = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Execute code in the sandboxed context
|
|
27
|
+
# @param code [String] Ruby code to execute
|
|
28
|
+
# @param file_path [String] Path to file (for error reporting)
|
|
29
|
+
# @return [Object] Result of code execution
|
|
30
|
+
def eval(code, file_path = '(eval)')
|
|
31
|
+
# Step 1: Validate code with AST analysis
|
|
32
|
+
@validator.validate!(code, file_path)
|
|
33
|
+
|
|
34
|
+
# Step 2: Execute in sandboxed context
|
|
35
|
+
sandbox = SandboxProxy.new(@context, self)
|
|
36
|
+
|
|
37
|
+
# Step 3: Execute using instance_eval
|
|
38
|
+
# Note: We still use instance_eval but with validated code
|
|
39
|
+
# and wrapped context
|
|
40
|
+
sandbox.instance_eval(code, file_path)
|
|
41
|
+
rescue ASTValidator::SecurityError => e
|
|
42
|
+
# Re-raise validation errors as executor errors for clarity
|
|
43
|
+
raise SecurityError, "Code validation failed: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Log method calls for auditing
|
|
47
|
+
def log_call(receiver, method_name, args)
|
|
48
|
+
@audit_log << {
|
|
49
|
+
timestamp: Time.now,
|
|
50
|
+
receiver: receiver.class.name,
|
|
51
|
+
method: method_name,
|
|
52
|
+
args: args.map(&:class).map(&:name)
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get audit log
|
|
57
|
+
attr_reader :audit_log
|
|
58
|
+
|
|
59
|
+
# Proxy class that wraps the context and intercepts method calls
|
|
60
|
+
class SandboxProxy < BasicObject
|
|
61
|
+
def initialize(context, executor)
|
|
62
|
+
@__context__ = context
|
|
63
|
+
@__executor__ = executor
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Delegate method calls to the underlying context
|
|
67
|
+
# but check for dangerous methods first
|
|
68
|
+
def method_missing(method_name, *args, &)
|
|
69
|
+
# Log the call
|
|
70
|
+
@__executor__.log_call(@__context__, method_name, args)
|
|
71
|
+
|
|
72
|
+
# Special handling for require - allow only 'language_operator'
|
|
73
|
+
if %i[require require_relative].include?(method_name)
|
|
74
|
+
required_gem = args.first.to_s
|
|
75
|
+
return ::Kernel.require(required_gem) if required_gem == 'language_operator'
|
|
76
|
+
|
|
77
|
+
# Allow require 'language_operator'
|
|
78
|
+
|
|
79
|
+
::Kernel.raise ::LanguageOperator::Agent::Safety::SafeExecutor::SecurityError,
|
|
80
|
+
"Require '#{required_gem}' is not allowed. Only 'require \"language_operator\"' is permitted."
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check if method is safe
|
|
85
|
+
unless safe_method?(method_name)
|
|
86
|
+
::Kernel.raise ::LanguageOperator::Agent::Safety::SafeExecutor::SecurityError,
|
|
87
|
+
"Method '#{method_name}' is not allowed in sandboxed code"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Delegate to underlying context
|
|
91
|
+
@__context__.send(method_name, *args, &)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
95
|
+
@__context__.respond_to?(method_name, include_private)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Provide access to safe constants from the context
|
|
99
|
+
def const_missing(name)
|
|
100
|
+
# Allow access to HTTP and Shell helper classes
|
|
101
|
+
if name == :HTTP
|
|
102
|
+
return ::LanguageOperator::Dsl::HTTP
|
|
103
|
+
elsif name == :Shell
|
|
104
|
+
return ::LanguageOperator::Dsl::Shell
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Otherwise delegate to the context's module
|
|
108
|
+
@__context__.class.const_get(name)
|
|
109
|
+
rescue ::NameError
|
|
110
|
+
::Kernel.raise ::NameError, "uninitialized constant #{name}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def safe_method?(method_name)
|
|
116
|
+
# Always allow safe basic methods
|
|
117
|
+
return true if ::LanguageOperator::Agent::Safety::SafeExecutor::ALWAYS_SAFE_METHODS.include?(method_name)
|
|
118
|
+
|
|
119
|
+
# Check if the underlying context responds to the method
|
|
120
|
+
# (This allows DSL methods defined on the context)
|
|
121
|
+
@__context__.respond_to?(method_name)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rufus-scheduler'
|
|
4
|
+
require_relative 'executor'
|
|
5
|
+
require_relative 'instrumentation'
|
|
6
|
+
require_relative '../logger'
|
|
7
|
+
require_relative '../loggable'
|
|
8
|
+
|
|
9
|
+
module LanguageOperator
|
|
10
|
+
module Agent
|
|
11
|
+
# Task Scheduler
|
|
12
|
+
#
|
|
13
|
+
# Handles scheduled and event-driven task execution using rufus-scheduler.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# scheduler = Scheduler.new(agent)
|
|
17
|
+
# scheduler.start
|
|
18
|
+
class Scheduler
|
|
19
|
+
include LanguageOperator::Loggable
|
|
20
|
+
include LanguageOperator::Agent::Instrumentation
|
|
21
|
+
|
|
22
|
+
attr_reader :agent, :rufus_scheduler
|
|
23
|
+
|
|
24
|
+
# Initialize the scheduler
|
|
25
|
+
#
|
|
26
|
+
# @param agent [LanguageOperator::Agent::Base] The agent instance
|
|
27
|
+
def initialize(agent)
|
|
28
|
+
@agent = agent
|
|
29
|
+
@rufus_scheduler = Rufus::Scheduler.new
|
|
30
|
+
@executor = Executor.new(agent)
|
|
31
|
+
|
|
32
|
+
logger.debug('Scheduler initialized',
|
|
33
|
+
workspace: @agent.workspace_path,
|
|
34
|
+
servers: @agent.servers_info.length)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Start the scheduler
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
40
|
+
def start
|
|
41
|
+
logger.info('Agent starting in scheduled mode')
|
|
42
|
+
logger.info("Workspace: #{@agent.workspace_path}")
|
|
43
|
+
logger.info("Connected to #{@agent.servers_info.length} MCP server(s)")
|
|
44
|
+
|
|
45
|
+
setup_schedules
|
|
46
|
+
logger.info('Scheduler started, waiting for scheduled tasks')
|
|
47
|
+
@rufus_scheduler.join
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Start the scheduler with a workflow definition
|
|
51
|
+
#
|
|
52
|
+
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition with workflow
|
|
53
|
+
# @return [void]
|
|
54
|
+
def start_with_workflow(agent_def)
|
|
55
|
+
logger.info('Agent starting in scheduled mode with workflow',
|
|
56
|
+
agent_name: agent_def.name,
|
|
57
|
+
has_workflow: !agent_def.workflow.nil?)
|
|
58
|
+
logger.info("Workspace: #{@agent.workspace_path}")
|
|
59
|
+
logger.info("Connected to #{@agent.servers_info.length} MCP server(s)")
|
|
60
|
+
|
|
61
|
+
# Extract schedule from agent definition or use default
|
|
62
|
+
cron_schedule = agent_def.schedule&.cron || '0 6 * * *'
|
|
63
|
+
|
|
64
|
+
logger.info('Scheduling workflow', cron: cron_schedule, agent: agent_def.name)
|
|
65
|
+
|
|
66
|
+
@rufus_scheduler.cron(cron_schedule) do
|
|
67
|
+
with_span('agent.scheduler.execute', attributes: {
|
|
68
|
+
'scheduler.cron_expression' => cron_schedule,
|
|
69
|
+
'agent.name' => agent_def.name,
|
|
70
|
+
'scheduler.task_type' => 'workflow'
|
|
71
|
+
}) do
|
|
72
|
+
logger.timed('Scheduled workflow execution') do
|
|
73
|
+
logger.info('Executing scheduled workflow', agent: agent_def.name)
|
|
74
|
+
result = @executor.execute_workflow(agent_def)
|
|
75
|
+
result_text = result.is_a?(String) ? result : result.content
|
|
76
|
+
preview = result_text[0..200]
|
|
77
|
+
preview += '...' if result_text.length > 200
|
|
78
|
+
logger.info('Workflow completed', result_preview: preview)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
logger.info('Scheduler started, waiting for scheduled tasks')
|
|
84
|
+
@rufus_scheduler.join
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Stop the scheduler
|
|
88
|
+
#
|
|
89
|
+
# @return [void]
|
|
90
|
+
def stop
|
|
91
|
+
logger.info('Shutting down scheduler')
|
|
92
|
+
@rufus_scheduler.shutdown
|
|
93
|
+
logger.info('Scheduler stopped')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def logger_component
|
|
99
|
+
'Agent::Scheduler'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Setup schedules from config
|
|
103
|
+
#
|
|
104
|
+
# @return [void]
|
|
105
|
+
def setup_schedules
|
|
106
|
+
schedules = @agent.config.dig('agent', 'schedules') || []
|
|
107
|
+
|
|
108
|
+
logger.debug('Loading schedules from config', count: schedules.length)
|
|
109
|
+
|
|
110
|
+
if schedules.empty?
|
|
111
|
+
logger.warn('No schedules configured, using default daily schedule')
|
|
112
|
+
setup_default_schedule
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
schedules.each do |schedule|
|
|
117
|
+
add_schedule(schedule)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
logger.info("#{schedules.length} schedule(s) configured")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Add a single schedule
|
|
124
|
+
#
|
|
125
|
+
# @param schedule [Hash] Schedule configuration
|
|
126
|
+
# @return [void]
|
|
127
|
+
def add_schedule(schedule)
|
|
128
|
+
cron = schedule['cron']
|
|
129
|
+
task = schedule['task']
|
|
130
|
+
agent_name = @agent.config.dig('agent', 'name')
|
|
131
|
+
|
|
132
|
+
logger.info('Scheduling task', cron: cron, task: task[0..100])
|
|
133
|
+
|
|
134
|
+
@rufus_scheduler.cron(cron) do
|
|
135
|
+
with_span('agent.scheduler.execute', attributes: {
|
|
136
|
+
'scheduler.cron_expression' => cron,
|
|
137
|
+
'agent.name' => agent_name,
|
|
138
|
+
'scheduler.task_type' => 'scheduled'
|
|
139
|
+
}) do
|
|
140
|
+
logger.timed('Scheduled task execution') do
|
|
141
|
+
logger.info('Executing scheduled task', task: task[0..100])
|
|
142
|
+
result = @executor.execute(task)
|
|
143
|
+
preview = result[0..200]
|
|
144
|
+
preview += '...' if result.length > 200
|
|
145
|
+
logger.info('Task completed', result_preview: preview)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Setup default daily schedule
|
|
152
|
+
#
|
|
153
|
+
# @return [void]
|
|
154
|
+
def setup_default_schedule
|
|
155
|
+
instructions = @agent.config.dig('agent', 'instructions') ||
|
|
156
|
+
'Check for updates and report status'
|
|
157
|
+
agent_name = @agent.config.dig('agent', 'name')
|
|
158
|
+
cron = '0 6 * * *'
|
|
159
|
+
|
|
160
|
+
logger.info('Setting up default schedule', cron: cron,
|
|
161
|
+
instructions: instructions[0..100])
|
|
162
|
+
|
|
163
|
+
@rufus_scheduler.cron(cron) do
|
|
164
|
+
with_span('agent.scheduler.execute', attributes: {
|
|
165
|
+
'scheduler.cron_expression' => cron,
|
|
166
|
+
'agent.name' => agent_name,
|
|
167
|
+
'scheduler.task_type' => 'default'
|
|
168
|
+
}) do
|
|
169
|
+
logger.timed('Daily task execution') do
|
|
170
|
+
logger.info('Executing daily task')
|
|
171
|
+
result = @executor.execute(instructions)
|
|
172
|
+
preview = result[0..200]
|
|
173
|
+
preview += '...' if result.length > 200
|
|
174
|
+
logger.info('Daily task completed', result_preview: preview)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
logger.info('Scheduled: Daily at 6:00 AM')
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'opentelemetry/sdk'
|
|
4
|
+
require 'opentelemetry/exporter/otlp'
|
|
5
|
+
|
|
6
|
+
module LanguageOperator
|
|
7
|
+
module Agent
|
|
8
|
+
# OpenTelemetry configuration for agent runtime
|
|
9
|
+
#
|
|
10
|
+
# Initializes distributed tracing and telemetry for language operator agents.
|
|
11
|
+
# Reads configuration from environment variables and gracefully handles errors.
|
|
12
|
+
#
|
|
13
|
+
# @example Configure telemetry
|
|
14
|
+
# LanguageOperator::Agent::Telemetry.configure
|
|
15
|
+
module Telemetry
|
|
16
|
+
class << self
|
|
17
|
+
# Configure OpenTelemetry for the agent
|
|
18
|
+
#
|
|
19
|
+
# Reads configuration from environment variables:
|
|
20
|
+
# - OTEL_EXPORTER_OTLP_ENDPOINT: OTLP endpoint URL (required)
|
|
21
|
+
# - TRACEPARENT: W3C trace context for distributed tracing
|
|
22
|
+
# - AGENT_NAMESPACE: Kubernetes namespace
|
|
23
|
+
# - AGENT_NAME: Agent name
|
|
24
|
+
# - AGENT_MODE: Agent operating mode
|
|
25
|
+
# - HOSTNAME: Pod hostname
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
def configure
|
|
29
|
+
endpoint = ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil)
|
|
30
|
+
return unless endpoint
|
|
31
|
+
|
|
32
|
+
OpenTelemetry::SDK.configure do |c|
|
|
33
|
+
c.service_name = 'language-operator-agent'
|
|
34
|
+
c.service_version = LanguageOperator::VERSION
|
|
35
|
+
|
|
36
|
+
# Configure resource attributes
|
|
37
|
+
c.resource = OpenTelemetry::SDK::Resources::Resource.create(
|
|
38
|
+
build_resource_attributes
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Configure OTLP exporter
|
|
42
|
+
c.add_span_processor(
|
|
43
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
44
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
45
|
+
endpoint: endpoint
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Restore trace context from TRACEPARENT if present
|
|
52
|
+
restore_trace_context if ENV['TRACEPARENT']
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
warn "Failed to configure OpenTelemetry: #{e.message}"
|
|
55
|
+
warn e.backtrace.join("\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Build resource attributes from environment variables
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash] Resource attributes
|
|
63
|
+
def build_resource_attributes
|
|
64
|
+
attributes = {}
|
|
65
|
+
|
|
66
|
+
# Service namespace
|
|
67
|
+
if (namespace = ENV.fetch('AGENT_NAMESPACE', nil))
|
|
68
|
+
attributes['service.namespace'] = namespace
|
|
69
|
+
attributes['k8s.namespace.name'] = namespace
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Kubernetes pod name
|
|
73
|
+
attributes['k8s.pod.name'] = ENV['HOSTNAME'] if ENV['HOSTNAME']
|
|
74
|
+
|
|
75
|
+
# Agent-specific attributes
|
|
76
|
+
attributes['agent.name'] = ENV['AGENT_NAME'] if ENV['AGENT_NAME']
|
|
77
|
+
attributes['agent.mode'] = ENV['AGENT_MODE'] if ENV['AGENT_MODE']
|
|
78
|
+
|
|
79
|
+
attributes
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Restore trace context from TRACEPARENT environment variable
|
|
83
|
+
#
|
|
84
|
+
# Extracts W3C trace context and sets it as the current context,
|
|
85
|
+
# enabling distributed tracing across service boundaries.
|
|
86
|
+
#
|
|
87
|
+
# @return [void]
|
|
88
|
+
def restore_trace_context
|
|
89
|
+
traceparent = ENV.fetch('TRACEPARENT', nil)
|
|
90
|
+
return unless traceparent
|
|
91
|
+
|
|
92
|
+
# Parse TRACEPARENT (format: version-trace_id-parent_id-flags)
|
|
93
|
+
parts = traceparent.split('-')
|
|
94
|
+
return unless parts.length == 4
|
|
95
|
+
|
|
96
|
+
_version, trace_id, parent_id, _flags = parts
|
|
97
|
+
|
|
98
|
+
# Create span context from extracted values
|
|
99
|
+
span_context = OpenTelemetry::Trace::SpanContext.new(
|
|
100
|
+
trace_id: [trace_id].pack('H*'),
|
|
101
|
+
span_id: [parent_id].pack('H*'),
|
|
102
|
+
trace_flags: OpenTelemetry::Trace::TraceFlags::SAMPLED,
|
|
103
|
+
remote: true
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Set as current context using proper OpenTelemetry API
|
|
107
|
+
span = OpenTelemetry::Trace.non_recording_span(span_context)
|
|
108
|
+
context = OpenTelemetry::Trace.context_with_span(span)
|
|
109
|
+
OpenTelemetry::Context.attach(context)
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
warn "Failed to restore trace context: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|