tasker-rb 0.1.3-x86_64-linux
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 +7 -0
- data/README.md +55 -0
- data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
- data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
- data/lib/tasker_core/bootstrap.rb +394 -0
- data/lib/tasker_core/client.rb +165 -0
- data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
- data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
- data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
- data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
- data/lib/tasker_core/domain_events.rb +43 -0
- data/lib/tasker_core/errors/CLAUDE.md +7 -0
- data/lib/tasker_core/errors/common.rb +305 -0
- data/lib/tasker_core/errors/error_classifier.rb +61 -0
- data/lib/tasker_core/errors.rb +4 -0
- data/lib/tasker_core/event_bridge.rb +330 -0
- data/lib/tasker_core/handlers.rb +159 -0
- data/lib/tasker_core/internal.rb +31 -0
- data/lib/tasker_core/logger.rb +234 -0
- data/lib/tasker_core/models.rb +337 -0
- data/lib/tasker_core/observability/types.rb +158 -0
- data/lib/tasker_core/observability.rb +292 -0
- data/lib/tasker_core/registry/handler_registry.rb +453 -0
- data/lib/tasker_core/registry/resolver_chain.rb +258 -0
- data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
- data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
- data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
- data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
- data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
- data/lib/tasker_core/registry/resolvers.rb +42 -0
- data/lib/tasker_core/registry.rb +12 -0
- data/lib/tasker_core/step_handler/api.rb +48 -0
- data/lib/tasker_core/step_handler/base.rb +354 -0
- data/lib/tasker_core/step_handler/batchable.rb +50 -0
- data/lib/tasker_core/step_handler/decision.rb +53 -0
- data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
- data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
- data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
- data/lib/tasker_core/step_handler/mixins.rb +66 -0
- data/lib/tasker_core/subscriber.rb +212 -0
- data/lib/tasker_core/task_handler/base.rb +254 -0
- data/lib/tasker_core/tasker_rb.so +0 -0
- data/lib/tasker_core/template_discovery.rb +181 -0
- data/lib/tasker_core/test_environment.rb +313 -0
- data/lib/tasker_core/tracing.rb +166 -0
- data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
- data/lib/tasker_core/types/client_types.rb +145 -0
- data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
- data/lib/tasker_core/types/error_types.rb +72 -0
- data/lib/tasker_core/types/simple_message.rb +151 -0
- data/lib/tasker_core/types/step_context.rb +328 -0
- data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
- data/lib/tasker_core/types/step_message.rb +112 -0
- data/lib/tasker_core/types/step_types.rb +207 -0
- data/lib/tasker_core/types/task_template.rb +240 -0
- data/lib/tasker_core/types/task_types.rb +148 -0
- data/lib/tasker_core/types.rb +132 -0
- data/lib/tasker_core/version.rb +13 -0
- data/lib/tasker_core/worker/CLAUDE.md +7 -0
- data/lib/tasker_core/worker/event_poller.rb +224 -0
- data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
- data/lib/tasker_core.rb +161 -0
- metadata +292 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
module TestEnvironment
|
|
5
|
+
# Test environment conditional loading that only activates when TASKER_ENV=test
|
|
6
|
+
# Loads example handlers and configures test-specific paths for template discovery
|
|
7
|
+
class ConditionalLoader
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
attr_reader :logger, :loaded, :example_handlers_path, :template_fixtures_path
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@logger = nil # Will be set when logger is available
|
|
14
|
+
@loaded = false
|
|
15
|
+
@example_handlers_path = File.expand_path('../../spec/handlers/examples', __dir__)
|
|
16
|
+
@template_fixtures_path = File.expand_path('../../spec/fixtures/templates', __dir__)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check if we should load test environment components
|
|
20
|
+
def should_load_test_environment?
|
|
21
|
+
test_env = ENV['TASKER_ENV']&.downcase == 'test'
|
|
22
|
+
rails_test_env = ENV['RAILS_ENV']&.downcase == 'test'
|
|
23
|
+
force_examples = ENV['TASKER_FORCE_EXAMPLE_HANDLERS'] == 'true'
|
|
24
|
+
|
|
25
|
+
test_env || rails_test_env || force_examples
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Load test environment components if appropriate
|
|
29
|
+
def load_if_test_environment!
|
|
30
|
+
return false unless should_load_test_environment?
|
|
31
|
+
return true if @loaded # Already loaded
|
|
32
|
+
|
|
33
|
+
# Defer logger initialization until after TaskerCore loads
|
|
34
|
+
@logger = TaskerCore::Logger.instance
|
|
35
|
+
|
|
36
|
+
log_info('🧪 Test environment detected, loading example handlers and templates')
|
|
37
|
+
|
|
38
|
+
# Set template path override for test fixtures
|
|
39
|
+
setup_test_template_path!
|
|
40
|
+
|
|
41
|
+
# Pre-load all example handler files
|
|
42
|
+
load_example_handler_files!
|
|
43
|
+
|
|
44
|
+
# Verify example handlers are available
|
|
45
|
+
verify_test_setup!
|
|
46
|
+
|
|
47
|
+
@loaded = true
|
|
48
|
+
log_info("✅ Test environment setup complete: #{loaded_handler_count} example handlers loaded")
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get count of loaded handler classes for verification
|
|
53
|
+
def loaded_handler_count
|
|
54
|
+
return 0 unless @loaded
|
|
55
|
+
|
|
56
|
+
count = 0
|
|
57
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
58
|
+
if klass.name&.end_with?('Handler') &&
|
|
59
|
+
klass.ancestors.any? { |ancestor| ancestor.name&.include?('StepHandler') }
|
|
60
|
+
count += 1
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError
|
|
63
|
+
# Skip classes that can't be introspected
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
count
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get list of loaded example handler class names
|
|
70
|
+
def loaded_handler_names
|
|
71
|
+
return [] unless @loaded
|
|
72
|
+
|
|
73
|
+
names = []
|
|
74
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
75
|
+
if klass.name&.end_with?('Handler') &&
|
|
76
|
+
klass.ancestors.any? { |ancestor| ancestor.name&.include?('StepHandler') }
|
|
77
|
+
names << klass.name
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError
|
|
80
|
+
# Skip classes that can't be introspected
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
names.sort
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get template discovery info for debugging
|
|
87
|
+
def test_template_info
|
|
88
|
+
return {} unless @loaded
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
template_path: ENV.fetch('TASKER_TEMPLATE_PATH', nil),
|
|
92
|
+
fixtures_path: @template_fixtures_path,
|
|
93
|
+
fixtures_exist: Dir.exist?(@template_fixtures_path),
|
|
94
|
+
template_files: Dir.exist?(@template_fixtures_path) ? Dir.glob("#{@template_fixtures_path}/*.yaml").count : 0,
|
|
95
|
+
example_handlers_path: @example_handlers_path,
|
|
96
|
+
examples_exist: Dir.exist?(@example_handlers_path),
|
|
97
|
+
handler_files: Dir.exist?(@example_handlers_path) ? Dir.glob("#{@example_handlers_path}/**/*_handler.rb").count : 0,
|
|
98
|
+
subscriber_files: Dir.exist?(@example_handlers_path) ? Dir.glob("#{@example_handlers_path}/**/*_subscriber.rb").count : 0,
|
|
99
|
+
registered_subscribers: TaskerCore::DomainEvents::SubscriberRegistry.instance.count
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# TAS-65: Get count of loaded subscriber classes
|
|
104
|
+
def loaded_subscriber_count
|
|
105
|
+
return 0 unless @loaded
|
|
106
|
+
|
|
107
|
+
count = 0
|
|
108
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
109
|
+
if klass < TaskerCore::DomainEvents::BaseSubscriber &&
|
|
110
|
+
klass != TaskerCore::DomainEvents::BaseSubscriber
|
|
111
|
+
count += 1
|
|
112
|
+
end
|
|
113
|
+
rescue StandardError
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
count
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# TAS-65: Get list of loaded subscriber class names
|
|
120
|
+
def loaded_subscriber_names
|
|
121
|
+
return [] unless @loaded
|
|
122
|
+
|
|
123
|
+
names = []
|
|
124
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
125
|
+
if klass < TaskerCore::DomainEvents::BaseSubscriber &&
|
|
126
|
+
klass != TaskerCore::DomainEvents::BaseSubscriber
|
|
127
|
+
names << klass.name
|
|
128
|
+
end
|
|
129
|
+
rescue StandardError
|
|
130
|
+
next
|
|
131
|
+
end
|
|
132
|
+
names.sort
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def log_info(message)
|
|
138
|
+
if @logger
|
|
139
|
+
@logger.info(message)
|
|
140
|
+
else
|
|
141
|
+
puts message # Fallback if logger not available yet
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def log_debug(message)
|
|
146
|
+
if @logger.respond_to?(:debug)
|
|
147
|
+
@logger.debug(message)
|
|
148
|
+
elsif ENV['LOG_LEVEL'] == 'debug'
|
|
149
|
+
puts "[DEBUG] #{message}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def log_warn(message)
|
|
154
|
+
if @logger
|
|
155
|
+
@logger.warn(message)
|
|
156
|
+
else
|
|
157
|
+
puts "[WARN] #{message}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def setup_test_template_path!
|
|
162
|
+
# Override template path to use test fixtures if not already set
|
|
163
|
+
return if ENV['TASKER_TEMPLATE_PATH']
|
|
164
|
+
|
|
165
|
+
if Dir.exist?(@template_fixtures_path)
|
|
166
|
+
ENV['TASKER_TEMPLATE_PATH'] = @template_fixtures_path
|
|
167
|
+
log_debug("📁 Set TASKER_TEMPLATE_PATH to: #{@template_fixtures_path}")
|
|
168
|
+
else
|
|
169
|
+
log_warn("⚠️ Test template fixtures directory not found: #{@template_fixtures_path}")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def load_example_handler_files!
|
|
174
|
+
return unless Dir.exist?(@example_handlers_path)
|
|
175
|
+
|
|
176
|
+
handler_files = Dir.glob("#{@example_handlers_path}/**/*_handler.rb")
|
|
177
|
+
log_debug("🔍 Found #{handler_files.count} example handler files")
|
|
178
|
+
|
|
179
|
+
loaded_count = 0
|
|
180
|
+
handler_files.each do |handler_file|
|
|
181
|
+
# Use require instead of require_relative to avoid duplicate loading
|
|
182
|
+
require handler_file
|
|
183
|
+
loaded_count += 1
|
|
184
|
+
log_debug("✅ Loaded handler file: #{File.basename(handler_file)}")
|
|
185
|
+
rescue LoadError => e
|
|
186
|
+
log_warn("❌ Failed to load handler file #{handler_file}: #{e.message}")
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
log_warn("❌ Error loading handler file #{handler_file}: #{e.class} - #{e.message}")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
log_info("📚 Loaded #{loaded_count}/#{handler_files.count} example handler files")
|
|
192
|
+
|
|
193
|
+
# TAS-65: Also load subscriber files
|
|
194
|
+
load_example_subscriber_files!
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# TAS-65: Load example domain event subscriber files
|
|
198
|
+
def load_example_subscriber_files!
|
|
199
|
+
return unless Dir.exist?(@example_handlers_path)
|
|
200
|
+
|
|
201
|
+
subscriber_files = Dir.glob("#{@example_handlers_path}/**/*_subscriber.rb")
|
|
202
|
+
return if subscriber_files.empty?
|
|
203
|
+
|
|
204
|
+
log_debug("🔍 Found #{subscriber_files.count} example subscriber files")
|
|
205
|
+
|
|
206
|
+
loaded_count = 0
|
|
207
|
+
subscriber_files.each do |subscriber_file|
|
|
208
|
+
require subscriber_file
|
|
209
|
+
loaded_count += 1
|
|
210
|
+
log_debug("✅ Loaded subscriber file: #{File.basename(subscriber_file)}")
|
|
211
|
+
rescue LoadError => e
|
|
212
|
+
log_warn("❌ Failed to load subscriber file #{subscriber_file}: #{e.message}")
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
log_warn("❌ Error loading subscriber file #{subscriber_file}: #{e.class} - #{e.message}")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
log_info("📚 Loaded #{loaded_count}/#{subscriber_files.count} example subscriber files")
|
|
218
|
+
|
|
219
|
+
# Auto-register loaded subscribers with the registry
|
|
220
|
+
register_loaded_subscribers!
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# TAS-65: Auto-register any loaded subscriber classes with SubscriberRegistry
|
|
224
|
+
def register_loaded_subscribers!
|
|
225
|
+
registry = TaskerCore::DomainEvents::SubscriberRegistry.instance
|
|
226
|
+
registered_count = 0
|
|
227
|
+
|
|
228
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
229
|
+
# Skip if not a BaseSubscriber subclass
|
|
230
|
+
next unless klass < TaskerCore::DomainEvents::BaseSubscriber
|
|
231
|
+
# Skip the BaseSubscriber itself
|
|
232
|
+
next if klass == TaskerCore::DomainEvents::BaseSubscriber
|
|
233
|
+
|
|
234
|
+
registry.register(klass)
|
|
235
|
+
registered_count += 1
|
|
236
|
+
log_debug("✅ Registered subscriber: #{klass.name}")
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
log_warn("❌ Failed to register subscriber #{klass}: #{e.message}")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
log_info("📚 Registered #{registered_count} domain event subscribers") if registered_count.positive?
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def verify_test_setup!
|
|
245
|
+
# Verify template path is set correctly
|
|
246
|
+
template_path = ENV.fetch('TASKER_TEMPLATE_PATH', nil)
|
|
247
|
+
if template_path.nil?
|
|
248
|
+
log_warn('⚠️ TASKER_TEMPLATE_PATH not set, template discovery may fail')
|
|
249
|
+
elsif !Dir.exist?(template_path)
|
|
250
|
+
log_warn("⚠️ TASKER_TEMPLATE_PATH directory does not exist: #{template_path}")
|
|
251
|
+
else
|
|
252
|
+
yaml_files = Dir.glob("#{template_path}/*.yaml").count
|
|
253
|
+
log_debug("📄 Found #{yaml_files} YAML template files in #{template_path}")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Verify example handlers directory exists
|
|
257
|
+
unless Dir.exist?(@example_handlers_path)
|
|
258
|
+
log_warn("⚠️ Example handlers directory not found: #{@example_handlers_path}")
|
|
259
|
+
return
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Count available handler classes
|
|
263
|
+
handler_count = loaded_handler_count
|
|
264
|
+
if handler_count.zero?
|
|
265
|
+
log_warn('⚠️ No example handler classes found after loading')
|
|
266
|
+
else
|
|
267
|
+
log_debug("🎯 #{handler_count} example handler classes are available")
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Main interface - call this to conditionally load test environment
|
|
273
|
+
def self.load_if_test!
|
|
274
|
+
ConditionalLoader.instance.load_if_test_environment!
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Check if test environment is loaded
|
|
278
|
+
def self.loaded?
|
|
279
|
+
ConditionalLoader.instance.loaded
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Get test environment info for debugging
|
|
283
|
+
def self.info
|
|
284
|
+
loader = ConditionalLoader.instance
|
|
285
|
+
base_info = {
|
|
286
|
+
should_load: loader.should_load_test_environment?,
|
|
287
|
+
loaded: loader.loaded,
|
|
288
|
+
handler_count: loader.loaded_handler_count
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if loader.loaded
|
|
292
|
+
base_info.merge(loader.test_template_info)
|
|
293
|
+
else
|
|
294
|
+
base_info
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Get loaded handler names for debugging
|
|
299
|
+
def self.handler_names
|
|
300
|
+
ConditionalLoader.instance.loaded_handler_names
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# TAS-65: Get loaded subscriber names for debugging
|
|
304
|
+
def self.subscriber_names
|
|
305
|
+
ConditionalLoader.instance.loaded_subscriber_names
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# TAS-65: Get subscriber count
|
|
309
|
+
def self.subscriber_count
|
|
310
|
+
ConditionalLoader.instance.loaded_subscriber_count
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
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
|