tasker-rb 0.1.1

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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/DEVELOPMENT.md +548 -0
  3. data/README.md +87 -0
  4. data/ext/tasker_core/Cargo.lock +4720 -0
  5. data/ext/tasker_core/Cargo.toml +76 -0
  6. data/ext/tasker_core/extconf.rb +38 -0
  7. data/ext/tasker_core/src/CLAUDE.md +7 -0
  8. data/ext/tasker_core/src/bootstrap.rs +320 -0
  9. data/ext/tasker_core/src/bridge.rs +400 -0
  10. data/ext/tasker_core/src/client_ffi.rs +173 -0
  11. data/ext/tasker_core/src/conversions.rs +131 -0
  12. data/ext/tasker_core/src/diagnostics.rs +57 -0
  13. data/ext/tasker_core/src/event_handler.rs +179 -0
  14. data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
  15. data/ext/tasker_core/src/ffi_logging.rs +245 -0
  16. data/ext/tasker_core/src/global_event_system.rs +16 -0
  17. data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
  18. data/ext/tasker_core/src/lib.rs +41 -0
  19. data/ext/tasker_core/src/observability_ffi.rs +339 -0
  20. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  21. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  22. data/lib/tasker_core/bootstrap.rb +394 -0
  23. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  24. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  25. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  26. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  27. data/lib/tasker_core/domain_events.rb +43 -0
  28. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  29. data/lib/tasker_core/errors/common.rb +305 -0
  30. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  31. data/lib/tasker_core/errors.rb +4 -0
  32. data/lib/tasker_core/event_bridge.rb +330 -0
  33. data/lib/tasker_core/handlers.rb +159 -0
  34. data/lib/tasker_core/internal.rb +31 -0
  35. data/lib/tasker_core/logger.rb +234 -0
  36. data/lib/tasker_core/models.rb +337 -0
  37. data/lib/tasker_core/observability/types.rb +158 -0
  38. data/lib/tasker_core/observability.rb +292 -0
  39. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  40. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  41. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  42. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  43. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  44. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  45. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  46. data/lib/tasker_core/registry/resolvers.rb +42 -0
  47. data/lib/tasker_core/registry.rb +12 -0
  48. data/lib/tasker_core/step_handler/api.rb +48 -0
  49. data/lib/tasker_core/step_handler/base.rb +354 -0
  50. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  51. data/lib/tasker_core/step_handler/decision.rb +53 -0
  52. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  53. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  54. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  55. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  56. data/lib/tasker_core/subscriber.rb +212 -0
  57. data/lib/tasker_core/task_handler/base.rb +254 -0
  58. data/lib/tasker_core/tasker_rb.so +0 -0
  59. data/lib/tasker_core/template_discovery.rb +181 -0
  60. data/lib/tasker_core/tracing.rb +166 -0
  61. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  62. data/lib/tasker_core/types/client_types.rb +145 -0
  63. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  64. data/lib/tasker_core/types/error_types.rb +72 -0
  65. data/lib/tasker_core/types/simple_message.rb +151 -0
  66. data/lib/tasker_core/types/step_context.rb +328 -0
  67. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  68. data/lib/tasker_core/types/step_message.rb +112 -0
  69. data/lib/tasker_core/types/step_types.rb +207 -0
  70. data/lib/tasker_core/types/task_template.rb +240 -0
  71. data/lib/tasker_core/types/task_types.rb +148 -0
  72. data/lib/tasker_core/types.rb +132 -0
  73. data/lib/tasker_core/version.rb +13 -0
  74. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  75. data/lib/tasker_core/worker/event_poller.rb +224 -0
  76. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  77. data/lib/tasker_core.rb +160 -0
  78. metadata +322 -0
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'logger'
5
+ require 'securerandom'
6
+
7
+ module TaskerCore
8
+ module TaskHandler
9
+ class Base
10
+ # Ruby task handler for pgmq-based orchestration
11
+ #
12
+ # This class provides a simplified interface for task processing using the new
13
+ # pgmq architecture. Instead of TCP commands, it uses direct pgmq communication
14
+ # and the embedded orchestrator for step enqueueing.
15
+ #
16
+ # Key changes from TCP architecture:
17
+ # - No command_client dependencies
18
+ # - Direct pgmq messaging for task initialization
19
+ # - Embedded orchestrator for step enqueueing
20
+ # - Simplified error handling and validation
21
+
22
+ attr_reader :logger, :task_config
23
+
24
+ def initialize(task_config_path: nil, task_config: nil)
25
+ @logger = TaskerCore::Logger.instance
26
+ @task_config = task_config || (task_config_path ? load_task_config_from_path(task_config_path) : {})
27
+ @pgmq_client = nil # Lazy initialization to avoid database connection during setup
28
+ end
29
+
30
+ # Get or create pgmq client (lazy initialization)
31
+ # @return [TaskerCore::Messaging::PgmqClient] pgmq client instance
32
+ def pgmq_client
33
+ @pgmq_client ||= TaskerCore::Messaging::PgmqClient.new
34
+ end
35
+
36
+ # Main task processing method - Rails engine signature: handle(task_uuid)
37
+ #
38
+ # Mode-aware processing: Uses embedded FFI orchestrator in embedded mode,
39
+ # or pure pgmq communication in distributed mode.
40
+ #
41
+ # @param task_uuid [Integer] ID of the task to process
42
+ # @return [Hash] Result of step enqueueing operation
43
+ def handle(task_uuid)
44
+ unless task_uuid.is_a?(Integer)
45
+ raise TaskerCore::Errors::ValidationError.new('task_uuid is required and must be an integer', :task_uuid)
46
+ end
47
+
48
+ mode = orchestration_mode
49
+ logger.info "🚀 Processing task #{task_uuid} with pgmq orchestration (#{mode} mode)"
50
+
51
+ case mode
52
+ when 'embedded'
53
+ handle_embedded_mode(task_uuid)
54
+ when 'distributed'
55
+ handle_distributed_mode(task_uuid)
56
+ else
57
+ raise TaskerCore::Errors::OrchestrationError,
58
+ "Unknown orchestration mode: #{mode}. Expected 'embedded' or 'distributed'"
59
+ end
60
+ rescue TaskerCore::Errors::OrchestrationError => e
61
+ logger.error "❌ Orchestration error for task #{task_uuid}: #{e.message}"
62
+ {
63
+ success: false,
64
+ task_uuid: task_uuid,
65
+ error: e.message,
66
+ error_type: 'OrchestrationError',
67
+ architecture: 'pgmq',
68
+ processed_at: Time.now.utc.iso8601
69
+ }
70
+ rescue StandardError => e
71
+ logger.error "❌ Unexpected error processing task #{task_uuid}: #{e.class.name}: #{e.message}"
72
+ {
73
+ success: false,
74
+ task_uuid: task_uuid,
75
+ error: e.message,
76
+ error_type: e.class.name,
77
+ architecture: 'pgmq',
78
+ processed_at: Time.now.utc.iso8601
79
+ }
80
+ end
81
+
82
+ # Initialize a new task with workflow steps
83
+ #
84
+ # In the pgmq architecture, this sends a task request message to the orchestration
85
+ # core monitored task_requests_queue, which will be processed by the Rust orchestrator
86
+ # to create the task record and enqueue initial steps.
87
+ #
88
+ # @param task_request [Hash] Task initialization data
89
+ # @return [void] No return value - operation is async via pgmq
90
+ def initialize_task(task_request)
91
+ logger.info '🚀 Initializing task with pgmq architecture'
92
+
93
+ task_request = TaskerCore::Types::TaskTypes::TaskRequest.from_hash(task_request)
94
+
95
+ # Prepare task request message for pgmq
96
+ task_request_message = {
97
+ message_type: 'task_request',
98
+ task_request: task_request.to_ffi_hash,
99
+ enqueued_at: Time.now.utc.iso8601,
100
+ message_id: SecureRandom.uuid
101
+ }
102
+
103
+ # Send message to task_requests_queue for orchestration core processing
104
+ begin
105
+ pgmq_client.send_message('task_requests_queue', task_request_message)
106
+ logger.info "✅ Task request sent to orchestration queue: #{task_request.namespace}/#{task_request.name}"
107
+
108
+ # Return void - this is now an async operation
109
+ nil
110
+ rescue StandardError => e
111
+ logger.error "❌ Failed to send task request to orchestration queue: #{e.message}"
112
+ raise TaskerCore::Errors::OrchestrationError, "Failed to send task request: #{e.message}"
113
+ end
114
+ rescue TaskerCore::Errors::ValidationError => e
115
+ logger.error "❌ Validation error initializing task: #{e.message}"
116
+ raise e
117
+ rescue StandardError => e
118
+ logger.error "❌ Unexpected error initializing task: #{e.class.name}: #{e.message}"
119
+ raise TaskerCore::Errors::OrchestrationError, "Task initialization failed: #{e.message}"
120
+ end
121
+
122
+ # Check if the pgmq orchestration system is available and ready
123
+ # @return [Boolean] true if system is ready for task processing
124
+ def orchestration_ready?
125
+ orchestrator = TaskerCore.embedded_orchestrator
126
+ orchestrator.running?
127
+ rescue StandardError => e
128
+ logger.warn "⚠️ Failed to check orchestration status: #{e.message}"
129
+ false
130
+ end
131
+
132
+ # Get status information for this task handler
133
+ # @return [Hash] Status information including mode and pgmq connectivity
134
+ def status
135
+ mode = orchestration_mode
136
+
137
+ # Check pgmq availability without forcing connection
138
+ pgmq_available = begin
139
+ !pgmq_client.nil?
140
+ rescue TaskerCore::Errors::Error => e
141
+ logger.debug "🔍 PGMQ not available: #{e.message}"
142
+ false
143
+ end
144
+
145
+ status_info = {
146
+ handler_type: 'TaskHandler::Base',
147
+ architecture: 'pgmq',
148
+ orchestration_mode: mode,
149
+ orchestration_ready: orchestration_ready?,
150
+ pgmq_available: pgmq_available,
151
+ task_config_loaded: !task_config.empty?,
152
+ checked_at: Time.now.utc.iso8601
153
+ }
154
+
155
+ # Include embedded orchestrator status only in embedded mode
156
+ status_info[:embedded_orchestrator] = embedded_orchestrator_status if mode == 'embedded'
157
+
158
+ status_info
159
+ end
160
+
161
+ private
162
+
163
+ def load_task_config_from_path(path)
164
+ return {} unless path && File.exist?(path)
165
+
166
+ YAML.load_file(path)
167
+ rescue StandardError => e
168
+ logger.warn "Error loading task configuration: #{e.message}"
169
+ {}
170
+ end
171
+
172
+ def embedded_orchestrator_status
173
+ orchestrator = TaskerCore.embedded_orchestrator
174
+ {
175
+ running: orchestrator.running?,
176
+ namespaces: orchestrator.namespaces,
177
+ started_at: orchestrator.started_at&.iso8601
178
+ }
179
+ rescue StandardError => e
180
+ logger.warn "⚠️ Failed to get embedded orchestrator status: #{e.message}"
181
+ { running: false, error: e.message }
182
+ end
183
+
184
+ # Determine orchestration mode from configuration
185
+ # @return [String] 'embedded' or 'distributed'
186
+ def orchestration_mode
187
+ config = TaskerCore::Config.instance
188
+ mode = config.orchestration_config.mode
189
+
190
+ # Default to embedded mode if not specified or in test environment
191
+ if mode.nil?
192
+ if config.test_environment?
193
+ 'embedded'
194
+ else
195
+ 'distributed'
196
+ end
197
+ else
198
+ mode
199
+ end
200
+ rescue StandardError => e
201
+ logger.warn "⚠️ Failed to determine orchestration mode: #{e.message}, defaulting to distributed"
202
+ 'distributed'
203
+ end
204
+
205
+ # Handle task processing in embedded mode using FFI orchestrator
206
+ # @param task_uuid [Integer] ID of the task to process
207
+ # @return [Hash] Result of step enqueueing operation
208
+ def handle_embedded_mode(task_uuid)
209
+ # Use embedded orchestrator to enqueue ready steps for the task
210
+ orchestrator = TaskerCore.embedded_orchestrator
211
+
212
+ unless orchestrator.running?
213
+ raise TaskerCore::Errors::OrchestrationError,
214
+ 'Embedded orchestration system not running. Call TaskerCore.start_embedded_orchestration! first.'
215
+ end
216
+
217
+ # Enqueue steps for the task - this will publish step messages to appropriate queues
218
+ result = orchestrator.enqueue_steps(task_uuid)
219
+
220
+ logger.info "✅ Task #{task_uuid} step enqueueing completed (embedded): #{result}"
221
+
222
+ {
223
+ success: true,
224
+ task_uuid: task_uuid,
225
+ message: result,
226
+ mode: 'embedded',
227
+ architecture: 'pgmq',
228
+ processed_at: Time.now.utc.iso8601
229
+ }
230
+ end
231
+
232
+ # Handle task processing in distributed mode using pure pgmq
233
+ # @param task_uuid [Integer] ID of the task to process
234
+ # @return [Hash] Result of step enqueueing operation
235
+ def handle_distributed_mode(task_uuid)
236
+ # In distributed mode, we don't directly enqueue steps via FFI
237
+ # Instead, we could publish a task processing request to a queue
238
+ # For now, return a message indicating distributed mode handling
239
+
240
+ logger.info "✅ Task #{task_uuid} queued for distributed processing"
241
+
242
+ {
243
+ success: true,
244
+ task_uuid: task_uuid,
245
+ message: 'Task queued for distributed orchestration processing',
246
+ mode: 'distributed',
247
+ architecture: 'pgmq',
248
+ processed_at: Time.now.utc.iso8601,
249
+ note: 'Phase 4.5 will complete distributed orchestration integration'
250
+ }
251
+ end
252
+ end
253
+ end
254
+ end
Binary file
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module TaskerCore
6
+ module TemplateDiscovery
7
+ # Template path discovery following the same patterns as Rust TaskTemplateManager
8
+ # Finds task template configuration directory using environment variables and workspace detection
9
+ class TemplatePath
10
+ class << self
11
+ # Find the template configuration directory using the same logic as Rust
12
+ # Returns path to directory containing .yaml template files
13
+ def find_template_config_directory
14
+ # First try TASKER_TEMPLATE_PATH environment variable
15
+ return ENV['TASKER_TEMPLATE_PATH'] if ENV['TASKER_TEMPLATE_PATH'] && Dir.exist?(ENV['TASKER_TEMPLATE_PATH'])
16
+
17
+ # Try WORKSPACE_PATH environment variable (for compatibility)
18
+ if ENV['WORKSPACE_PATH']
19
+ template_dir = File.join(ENV['WORKSPACE_PATH'], 'config', 'tasks')
20
+ return template_dir if Dir.exist?(template_dir)
21
+ end
22
+
23
+ # Try workspace detection from current directory
24
+ workspace_root = find_workspace_root
25
+ if workspace_root
26
+ template_dir = File.join(workspace_root, 'config', 'tasks')
27
+ return template_dir if Dir.exist?(template_dir)
28
+ end
29
+
30
+ # Default fallback to current directory
31
+ default_dir = 'config/tasks'
32
+ return default_dir if Dir.exist?(default_dir)
33
+
34
+ # If nothing found, return nil
35
+ nil
36
+ end
37
+
38
+ # Discover all YAML template files in the template directory
39
+ def discover_template_files(template_dir = nil)
40
+ template_dir ||= find_template_config_directory
41
+ return [] unless template_dir
42
+
43
+ # Look for both .yaml and .yml file extensions
44
+ yaml_files = Dir.glob(File.join(template_dir, '**', '*.yaml'))
45
+ yml_files = Dir.glob(File.join(template_dir, '**', '*.yml'))
46
+ (yaml_files + yml_files).sort.uniq
47
+ end
48
+
49
+ private
50
+
51
+ # Find workspace root by looking for Cargo.toml or other workspace indicators
52
+ def find_workspace_root(start_dir = Dir.pwd)
53
+ current_dir = File.expand_path(start_dir)
54
+
55
+ loop do
56
+ # Look for workspace indicators
57
+ return current_dir if workspace_indicators.any? do |indicator|
58
+ File.exist?(File.join(current_dir, indicator))
59
+ end
60
+
61
+ parent_dir = File.dirname(current_dir)
62
+ break if parent_dir == current_dir # Reached filesystem root
63
+
64
+ current_dir = parent_dir
65
+ end
66
+
67
+ nil
68
+ end
69
+
70
+ def workspace_indicators
71
+ ['Cargo.toml', '.git', 'Gemfile', 'package.json', 'tasker-core.code-workspace']
72
+ end
73
+ end
74
+ end
75
+
76
+ # YAML template parser that extracts handler information
77
+ class TemplateParser
78
+ class << self
79
+ # Parse a YAML template file and extract handler callables
80
+ # Returns array of handler class names found in the template
81
+ def extract_handler_callables(template_file)
82
+ return [] unless File.exist?(template_file)
83
+
84
+ begin
85
+ template_data = YAML.load_file(template_file)
86
+ extract_handlers_from_template(template_data)
87
+ rescue StandardError => e
88
+ TaskerCore::Logger.instance.warn("Failed to parse template #{template_file}: #{e.message}")
89
+ []
90
+ end
91
+ end
92
+
93
+ # Extract handler callables from parsed template data
94
+ def extract_handlers_from_template(template_data)
95
+ handlers = []
96
+
97
+ # Add task handler if present
98
+ if template_data['task_handler'] && template_data['task_handler']['callable']
99
+ handlers << template_data['task_handler']['callable']
100
+ end
101
+
102
+ # Add step handlers
103
+ if template_data['steps'].is_a?(Array)
104
+ template_data['steps'].each do |step|
105
+ handlers << step['handler']['callable'] if step['handler'] && step['handler']['callable']
106
+ end
107
+ end
108
+
109
+ handlers.uniq
110
+ end
111
+
112
+ # Get template metadata for debugging/introspection
113
+ def extract_template_metadata(template_file)
114
+ return nil unless File.exist?(template_file)
115
+
116
+ begin
117
+ template_data = YAML.load_file(template_file)
118
+ {
119
+ name: template_data['name'],
120
+ namespace_name: template_data['namespace_name'],
121
+ version: template_data['version'],
122
+ description: template_data['description'],
123
+ file_path: template_file,
124
+ handlers: extract_handlers_from_template(template_data)
125
+ }
126
+ rescue StandardError => e
127
+ TaskerCore::Logger.instance.warn("Failed to extract metadata from #{template_file}: #{e.message}")
128
+ nil
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ # Main discovery coordinator that combines path and parsing logic
135
+ class HandlerDiscovery
136
+ class << self
137
+ # Discover all handlers from templates in the configured template directory
138
+ # Returns array of unique handler class names
139
+ def discover_all_handlers(template_dir = nil)
140
+ template_files = TemplatePath.discover_template_files(template_dir)
141
+
142
+ handlers = template_files.flat_map do |file|
143
+ TemplateParser.extract_handler_callables(file)
144
+ end
145
+
146
+ handlers.uniq.sort
147
+ end
148
+
149
+ # Get detailed information about discovered templates
150
+ # Returns array of template metadata hashes
151
+ def discover_template_metadata(template_dir = nil)
152
+ template_files = TemplatePath.discover_template_files(template_dir)
153
+
154
+ template_files.filter_map do |file|
155
+ TemplateParser.extract_template_metadata(file)
156
+ end
157
+ end
158
+
159
+ # Get handlers grouped by namespace
160
+ # Returns hash of namespace => [handler_names]
161
+ def discover_handlers_by_namespace(template_dir = nil)
162
+ metadata = discover_template_metadata(template_dir)
163
+
164
+ handlers_by_namespace = {}
165
+ metadata.each do |meta|
166
+ namespace = meta[:namespace_name] || 'default'
167
+ handlers_by_namespace[namespace] ||= []
168
+ handlers_by_namespace[namespace].concat(meta[:handlers])
169
+ end
170
+
171
+ # Remove duplicates and sort
172
+ handlers_by_namespace.each do |namespace, handlers|
173
+ handlers_by_namespace[namespace] = handlers.uniq.sort
174
+ end
175
+
176
+ handlers_by_namespace
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module TaskerCore
6
+ # Unified structured logging via Rust tracing FFI
7
+ #
8
+ # TAS-29 Phase 6: Replace Ruby's Logger with FFI calls to Rust's tracing
9
+ # infrastructure, enabling unified structured logging across Ruby and Rust.
10
+ #
11
+ # ## Architecture
12
+ #
13
+ # Ruby Handler → Tracing.info() → FFI Bridge → Rust tracing → OpenTelemetry
14
+ #
15
+ # ## Usage
16
+ #
17
+ # # Simple message
18
+ # Tracing.info("Task initialized")
19
+ #
20
+ # # With structured fields
21
+ # Tracing.info("Step completed", {
22
+ # correlation_id: correlation_id,
23
+ # task_uuid: task.uuid,
24
+ # step_uuid: step.uuid,
25
+ # namespace: "order_fulfillment",
26
+ # operation: "validate_inventory",
27
+ # duration_ms: elapsed_ms
28
+ # })
29
+ #
30
+ # # Error logging
31
+ # Tracing.error("Payment processing failed", {
32
+ # correlation_id: correlation_id,
33
+ # error_class: error.class.name,
34
+ # error_message: error.message
35
+ # })
36
+ #
37
+ # ## Structured Field Conventions (TAS-29 Phase 6.2)
38
+ #
39
+ # **Required fields** (when applicable):
40
+ # - correlation_id: Always include for distributed tracing
41
+ # - task_uuid: Include for task-level operations
42
+ # - step_uuid: Include for step-level operations
43
+ # - namespace: Include for namespace-specific operations
44
+ # - operation: Operation identifier (e.g., "validate_inventory")
45
+ #
46
+ # **Optional fields**:
47
+ # - duration_ms: For timed operations
48
+ # - error_class, error_message: For error context
49
+ # - entity_id: For domain entity operations
50
+ # - retry_count: For retryable operations
51
+ #
52
+ # ## Log Level Guidelines (TAS-29 Phase 6.1)
53
+ #
54
+ # - ERROR: Unrecoverable failures requiring intervention
55
+ # - WARN: Degraded operation, retryable failures
56
+ # - INFO: Lifecycle events, state transitions
57
+ # - DEBUG: Detailed diagnostic information
58
+ # - TRACE: Very verbose, hot-path entry/exit
59
+ #
60
+ class Tracing
61
+ include Singleton
62
+
63
+ # Log ERROR level message with structured fields
64
+ #
65
+ # @param message [String] Log message
66
+ # @param fields [Hash] Structured fields (correlation_id, task_uuid, etc.)
67
+ # @return [void]
68
+ def self.error(message, fields = {})
69
+ fields_hash = normalize_fields(fields)
70
+ TaskerCore::FFI.log_error(message.to_s, fields_hash)
71
+ rescue StandardError => e
72
+ # Fallback to stderr if FFI logging fails
73
+ warn "FFI logging failed: #{e.message}"
74
+ warn "#{message} | #{fields.inspect}"
75
+ end
76
+
77
+ # Log WARN level message with structured fields
78
+ #
79
+ # @param message [String] Log message
80
+ # @param fields [Hash] Structured fields
81
+ # @return [void]
82
+ def self.warn(message, fields = {})
83
+ fields_hash = normalize_fields(fields)
84
+ TaskerCore::FFI.log_warn(message.to_s, fields_hash)
85
+ rescue StandardError => e
86
+ warn "FFI logging failed: #{e.message}"
87
+ warn "#{message} | #{fields.inspect}"
88
+ end
89
+
90
+ # Log INFO level message with structured fields
91
+ #
92
+ # @param message [String] Log message
93
+ # @param fields [Hash] Structured fields
94
+ # @return [void]
95
+ def self.info(message, fields = {})
96
+ fields_hash = normalize_fields(fields)
97
+ TaskerCore::FFI.log_info(message.to_s, fields_hash)
98
+ rescue StandardError => e
99
+ warn "FFI logging failed: #{e.message}"
100
+ warn "#{message} | #{fields.inspect}"
101
+ end
102
+
103
+ # Log DEBUG level message with structured fields
104
+ #
105
+ # @param message [String] Log message
106
+ # @param fields [Hash] Structured fields
107
+ # @return [void]
108
+ def self.debug(message, fields = {})
109
+ fields_hash = normalize_fields(fields)
110
+ TaskerCore::FFI.log_debug(message.to_s, fields_hash)
111
+ rescue StandardError => e
112
+ warn "FFI logging failed: #{e.message}"
113
+ warn "#{message} | #{fields.inspect}"
114
+ end
115
+
116
+ # Log TRACE level message with structured fields
117
+ #
118
+ # @param message [String] Log message
119
+ # @param fields [Hash] Structured fields
120
+ # @return [void]
121
+ def self.trace(message, fields = {})
122
+ fields_hash = normalize_fields(fields)
123
+ TaskerCore::FFI.log_trace(message.to_s, fields_hash)
124
+ rescue StandardError => e
125
+ warn "FFI logging failed: #{e.message}"
126
+ warn "#{message} | #{fields.inspect}"
127
+ end
128
+
129
+ # Normalize fields to string keys and values for FFI
130
+ #
131
+ # @param fields [Hash] Input fields
132
+ # @return [Hash<String, String>] Normalized fields
133
+ # @api private
134
+ def self.normalize_fields(fields)
135
+ return {} if fields.nil? || fields.empty?
136
+
137
+ fields.transform_keys(&:to_s).transform_values { |v| normalize_value(v) }
138
+ end
139
+
140
+ # Normalize a value to string for FFI
141
+ #
142
+ # @param value [Object] Value to normalize
143
+ # @return [String] String representation
144
+ # @api private
145
+ def self.normalize_value(value)
146
+ case value
147
+ when nil
148
+ 'nil'
149
+ when String
150
+ value
151
+ when Symbol
152
+ value.to_s
153
+ when Numeric, TrueClass, FalseClass
154
+ value.to_s
155
+ when Exception
156
+ "#{value.class}: #{value.message}"
157
+ else
158
+ value.to_s
159
+ end
160
+ rescue StandardError
161
+ '<serialization_error>'
162
+ end
163
+
164
+ private_class_method :normalize_fields, :normalize_value
165
+ end
166
+ end