fractor 0.1.4 → 0.1.7

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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
  3. data/.rubocop.yml +14 -8
  4. data/.rubocop_todo.yml +284 -43
  5. data/README.adoc +111 -950
  6. data/docs/.lycheeignore +16 -0
  7. data/docs/Gemfile +24 -0
  8. data/docs/README.md +157 -0
  9. data/docs/_config.yml +151 -0
  10. data/docs/_features/error-handling.adoc +1192 -0
  11. data/docs/_features/index.adoc +80 -0
  12. data/docs/_features/monitoring.adoc +589 -0
  13. data/docs/_features/signal-handling.adoc +202 -0
  14. data/docs/_features/workflows.adoc +1235 -0
  15. data/docs/_guides/continuous-mode.adoc +736 -0
  16. data/docs/_guides/cookbook.adoc +1133 -0
  17. data/docs/_guides/index.adoc +55 -0
  18. data/docs/_guides/pipeline-mode.adoc +730 -0
  19. data/docs/_guides/troubleshooting.adoc +358 -0
  20. data/docs/_pages/architecture.adoc +1390 -0
  21. data/docs/_pages/core-concepts.adoc +1392 -0
  22. data/docs/_pages/design-principles.adoc +862 -0
  23. data/docs/_pages/getting-started.adoc +290 -0
  24. data/docs/_pages/installation.adoc +143 -0
  25. data/docs/_reference/api.adoc +1080 -0
  26. data/docs/_reference/error-reporting.adoc +670 -0
  27. data/docs/_reference/examples.adoc +181 -0
  28. data/docs/_reference/index.adoc +96 -0
  29. data/docs/_reference/troubleshooting.adoc +862 -0
  30. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  31. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  32. data/docs/_tutorials/first-application.adoc +384 -0
  33. data/docs/_tutorials/index.adoc +48 -0
  34. data/docs/_tutorials/long-running-services.adoc +931 -0
  35. data/docs/assets/images/favicon-16.png +0 -0
  36. data/docs/assets/images/favicon-32.png +0 -0
  37. data/docs/assets/images/favicon-48.png +0 -0
  38. data/docs/assets/images/favicon.ico +0 -0
  39. data/docs/assets/images/favicon.png +0 -0
  40. data/docs/assets/images/favicon.svg +45 -0
  41. data/docs/assets/images/fractor-icon.svg +49 -0
  42. data/docs/assets/images/fractor-logo.svg +61 -0
  43. data/docs/index.adoc +131 -0
  44. data/docs/lychee.toml +39 -0
  45. data/examples/api_aggregator/README.adoc +627 -0
  46. data/examples/api_aggregator/api_aggregator.rb +376 -0
  47. data/examples/auto_detection/README.adoc +407 -29
  48. data/examples/auto_detection/auto_detection.rb +9 -9
  49. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  50. data/examples/continuous_chat_fractor/README.adoc +217 -0
  51. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  52. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  53. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  54. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  55. data/examples/continuous_chat_server/README.adoc +135 -0
  56. data/examples/continuous_chat_server/chat_client.rb +303 -0
  57. data/examples/continuous_chat_server/chat_server.rb +359 -0
  58. data/examples/continuous_chat_server/simulate.rb +343 -0
  59. data/examples/error_reporting.rb +207 -0
  60. data/examples/file_processor/README.adoc +170 -0
  61. data/examples/file_processor/file_processor.rb +615 -0
  62. data/examples/file_processor/sample_files/invalid.csv +1 -0
  63. data/examples/file_processor/sample_files/orders.xml +24 -0
  64. data/examples/file_processor/sample_files/products.json +23 -0
  65. data/examples/file_processor/sample_files/users.csv +6 -0
  66. data/examples/hierarchical_hasher/README.adoc +629 -41
  67. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  68. data/examples/image_processor/README.adoc +610 -0
  69. data/examples/image_processor/image_processor.rb +349 -0
  70. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  71. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  72. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  73. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  74. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  75. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  76. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  77. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  78. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  79. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  80. data/examples/image_processor/test_images/sample_1.png +1 -0
  81. data/examples/image_processor/test_images/sample_10.png +1 -0
  82. data/examples/image_processor/test_images/sample_2.png +1 -0
  83. data/examples/image_processor/test_images/sample_3.png +1 -0
  84. data/examples/image_processor/test_images/sample_4.png +1 -0
  85. data/examples/image_processor/test_images/sample_5.png +1 -0
  86. data/examples/image_processor/test_images/sample_6.png +1 -0
  87. data/examples/image_processor/test_images/sample_7.png +1 -0
  88. data/examples/image_processor/test_images/sample_8.png +1 -0
  89. data/examples/image_processor/test_images/sample_9.png +1 -0
  90. data/examples/log_analyzer/README.adoc +662 -0
  91. data/examples/log_analyzer/log_analyzer.rb +579 -0
  92. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  93. data/examples/log_analyzer/sample_logs/json.log +15 -0
  94. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  95. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  96. data/examples/multi_work_type/README.adoc +576 -26
  97. data/examples/multi_work_type/multi_work_type.rb +30 -29
  98. data/examples/performance_monitoring.rb +120 -0
  99. data/examples/pipeline_processing/README.adoc +740 -26
  100. data/examples/pipeline_processing/pipeline_processing.rb +16 -16
  101. data/examples/priority_work_example.rb +155 -0
  102. data/examples/producer_subscriber/README.adoc +889 -46
  103. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  104. data/examples/scatter_gather/README.adoc +829 -27
  105. data/examples/scatter_gather/scatter_gather.rb +29 -28
  106. data/examples/simple/README.adoc +347 -0
  107. data/examples/simple/sample.rb +5 -5
  108. data/examples/specialized_workers/README.adoc +622 -26
  109. data/examples/specialized_workers/specialized_workers.rb +88 -45
  110. data/examples/stream_processor/README.adoc +206 -0
  111. data/examples/stream_processor/stream_processor.rb +284 -0
  112. data/examples/web_scraper/README.adoc +625 -0
  113. data/examples/web_scraper/web_scraper.rb +285 -0
  114. data/examples/workflow/README.adoc +406 -0
  115. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  116. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  117. data/examples/workflow/conditional/README.adoc +483 -0
  118. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  119. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  120. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  121. data/examples/workflow/fan_out/README.adoc +381 -0
  122. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  123. data/examples/workflow/retry/README.adoc +248 -0
  124. data/examples/workflow/retry/retry_workflow.rb +195 -0
  125. data/examples/workflow/simple_linear/README.adoc +267 -0
  126. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  127. data/examples/workflow/simplified/README.adoc +329 -0
  128. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  129. data/exe/fractor +10 -0
  130. data/lib/fractor/cli.rb +288 -0
  131. data/lib/fractor/configuration.rb +307 -0
  132. data/lib/fractor/continuous_server.rb +183 -0
  133. data/lib/fractor/error_formatter.rb +72 -0
  134. data/lib/fractor/error_report_generator.rb +152 -0
  135. data/lib/fractor/error_reporter.rb +244 -0
  136. data/lib/fractor/error_statistics.rb +147 -0
  137. data/lib/fractor/execution_tracer.rb +162 -0
  138. data/lib/fractor/logger.rb +230 -0
  139. data/lib/fractor/main_loop_handler.rb +406 -0
  140. data/lib/fractor/main_loop_handler3.rb +135 -0
  141. data/lib/fractor/main_loop_handler4.rb +299 -0
  142. data/lib/fractor/performance_metrics_collector.rb +181 -0
  143. data/lib/fractor/performance_monitor.rb +215 -0
  144. data/lib/fractor/performance_report_generator.rb +202 -0
  145. data/lib/fractor/priority_work.rb +93 -0
  146. data/lib/fractor/priority_work_queue.rb +189 -0
  147. data/lib/fractor/result_aggregator.rb +33 -1
  148. data/lib/fractor/shutdown_handler.rb +168 -0
  149. data/lib/fractor/signal_handler.rb +80 -0
  150. data/lib/fractor/supervisor.rb +430 -144
  151. data/lib/fractor/supervisor_logger.rb +88 -0
  152. data/lib/fractor/version.rb +1 -1
  153. data/lib/fractor/work.rb +12 -0
  154. data/lib/fractor/work_distribution_manager.rb +151 -0
  155. data/lib/fractor/work_queue.rb +88 -0
  156. data/lib/fractor/work_result.rb +181 -9
  157. data/lib/fractor/worker.rb +75 -1
  158. data/lib/fractor/workflow/builder.rb +210 -0
  159. data/lib/fractor/workflow/chain_builder.rb +169 -0
  160. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  161. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  162. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  163. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  164. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  165. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  166. data/lib/fractor/workflow/execution_trace.rb +134 -0
  167. data/lib/fractor/workflow/helpers.rb +191 -0
  168. data/lib/fractor/workflow/job.rb +290 -0
  169. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  170. data/lib/fractor/workflow/logger.rb +110 -0
  171. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  172. data/lib/fractor/workflow/retry_config.rb +156 -0
  173. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  174. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  175. data/lib/fractor/workflow/structured_logger.rb +30 -0
  176. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  177. data/lib/fractor/workflow/visualizer.rb +211 -0
  178. data/lib/fractor/workflow/workflow_context.rb +132 -0
  179. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  180. data/lib/fractor/workflow/workflow_result.rb +55 -0
  181. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  182. data/lib/fractor/workflow.rb +333 -0
  183. data/lib/fractor/wrapped_ractor.rb +66 -91
  184. data/lib/fractor/wrapped_ractor3.rb +161 -0
  185. data/lib/fractor/wrapped_ractor4.rb +242 -0
  186. data/lib/fractor.rb +93 -3
  187. metadata +192 -6
  188. data/tests/sample.rb.bak +0 -309
  189. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fractor
4
+ # Error statistics for a specific category or job.
5
+ # Tracks error counts, severity distribution, and trends over time.
6
+ class ErrorStatistics
7
+ attr_reader :category, :total_count, :by_severity, :by_code, :recent_errors
8
+
9
+ def initialize(category)
10
+ @category = category
11
+ @total_count = 0
12
+ @by_severity = Hash.new(0)
13
+ @by_code = Hash.new(0)
14
+ @recent_errors = []
15
+ @first_seen = nil
16
+ @last_seen = nil
17
+ @mutex = Mutex.new
18
+ end
19
+
20
+ # Record an error from a work result
21
+ #
22
+ # @param work_result [WorkResult] The failed work result
23
+ # @return [void]
24
+ def record(work_result)
25
+ @mutex.synchronize do
26
+ @total_count += 1
27
+ @by_severity[work_result.error_severity] += 1
28
+ @by_code[work_result.error_code] += 1 if work_result.error_code
29
+
30
+ # Handle both String and Exception error types
31
+ error_obj = work_result.error
32
+ error_message = if error_obj.is_a?(Exception)
33
+ error_obj.message
34
+ elsif error_obj.is_a?(String)
35
+ error_obj
36
+ else
37
+ error_obj&.to_s
38
+ end
39
+
40
+ error_entry = {
41
+ timestamp: Time.now,
42
+ error_class: error_obj.is_a?(Exception) ? error_obj.class.name : nil,
43
+ error_message: error_message,
44
+ error_code: work_result.error_code,
45
+ error_severity: work_result.error_severity,
46
+ error_context: work_result.error_context,
47
+ }
48
+
49
+ @recent_errors << error_entry
50
+ @recent_errors.shift if @recent_errors.size > 100
51
+
52
+ @first_seen ||= Time.now
53
+ @last_seen = Time.now
54
+ end
55
+ end
56
+
57
+ # Get error rate (errors per second)
58
+ #
59
+ # @return [Float] Errors per second
60
+ def error_rate
61
+ return 0.0 unless @first_seen && @last_seen
62
+
63
+ duration = @last_seen - @first_seen
64
+ return 0.0 if duration <= 0
65
+
66
+ @total_count / duration
67
+ end
68
+
69
+ # Get most common error code
70
+ #
71
+ # @return [String, nil] Most common error code
72
+ def most_common_code
73
+ return nil if @by_code.empty?
74
+
75
+ @by_code.max_by { |_code, count| count }&.first
76
+ end
77
+
78
+ # Get most severe error level
79
+ #
80
+ # @return [String, nil] Highest severity level
81
+ def highest_severity
82
+ return nil if @by_severity.empty?
83
+
84
+ severities = [
85
+ WorkResult::SEVERITY_CRITICAL,
86
+ WorkResult::SEVERITY_ERROR,
87
+ WorkResult::SEVERITY_WARNING,
88
+ WorkResult::SEVERITY_INFO,
89
+ ]
90
+
91
+ severities.find { |severity| @by_severity[severity].positive? }
92
+ end
93
+
94
+ # Check if error rate is increasing
95
+ #
96
+ # @return [Boolean] True if errors are trending upward
97
+ def increasing?
98
+ return false if @recent_errors.size < 10
99
+
100
+ recent_10 = @recent_errors.last(10)
101
+
102
+ # Check if errors are happening in a short time span (rapid burst)
103
+ first_timestamp = recent_10.first[:timestamp]
104
+ last_timestamp = recent_10.last[:timestamp]
105
+ total_timespan = last_timestamp - first_timestamp
106
+
107
+ # If all errors happened in a very short time (burst), consider it increasing
108
+ return true if total_timespan < 1.0 # Less than 1 second for 10 errors
109
+
110
+ # Otherwise, check if the rate is increasing by comparing first half vs second half
111
+ first_5 = recent_10.first(5)
112
+ last_5 = recent_10.last(5)
113
+
114
+ first_5_timespan = first_5.last[:timestamp] - first_5.first[:timestamp]
115
+ last_5_timespan = last_5.last[:timestamp] - last_5.first[:timestamp]
116
+
117
+ # Avoid division by zero - use small epsilon if timespan is very small
118
+ first_5_timespan = 0.001 if first_5_timespan <= 0
119
+ last_5_timespan = 0.001 if last_5_timespan <= 0
120
+
121
+ # Calculate error rate (errors per second) for each group
122
+ first_5_rate = 5.0 / first_5_timespan
123
+ last_5_rate = 5.0 / last_5_timespan
124
+
125
+ # Consider increasing if the rate is 50% higher
126
+ last_5_rate > first_5_rate * 1.5
127
+ end
128
+
129
+ # Get summary hash
130
+ #
131
+ # @return [Hash] Summary of statistics
132
+ def to_h
133
+ {
134
+ category: @category,
135
+ total_count: @total_count,
136
+ error_rate: error_rate.round(2),
137
+ by_severity: @by_severity,
138
+ by_code: @by_code,
139
+ most_common_code: most_common_code,
140
+ highest_severity: highest_severity,
141
+ first_seen: @first_seen,
142
+ last_seen: @last_seen,
143
+ trending: increasing? ? "increasing" : "stable",
144
+ }
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fractor
4
+ # Traces work item flow through the Fractor system for debugging.
5
+ # When enabled via FRACTOR_TRACE=1, captures the complete lifecycle of each work item.
6
+ #
7
+ # @example Instance-based usage (recommended)
8
+ # tracer = ExecutionTracer.new(enabled: true)
9
+ # tracer.trace(:created, work, worker_name: "MyWorker")
10
+ #
11
+ # @example Class-based usage (for backward compatibility)
12
+ # ExecutionTracer.enabled = true
13
+ # ExecutionTracer.trace(:created, work, worker_name: "MyWorker")
14
+ class ExecutionTracer
15
+ attr_reader :enabled, :trace_stream
16
+
17
+ # Initialize a new execution tracer instance.
18
+ #
19
+ # @param enabled [Boolean] Whether tracing is enabled
20
+ # @param trace_stream [IO] Output stream for trace messages
21
+ # @param check_env [Boolean] Whether to check FRACTOR_TRACE env var
22
+ def initialize(enabled: nil, trace_stream: nil, check_env: true)
23
+ @enabled = enabled || (check_env && ENV["FRACTOR_TRACE"] == "1")
24
+ @trace_stream = trace_stream || $stderr
25
+ @check_env = check_env
26
+ end
27
+
28
+ # Trace an event in the work item lifecycle.
29
+ #
30
+ # @param event [Symbol] The event type (:created, :queued, :assigned, :processing, :completed, :failed)
31
+ # @param work [Work] The work item
32
+ # @param context [Hash] Additional context (worker_name, timestamp, etc.)
33
+ def trace(event, work = nil, context = {})
34
+ return unless enabled?
35
+
36
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
37
+ thread_id = Thread.current.object_id
38
+
39
+ # Build trace line
40
+ trace_line = build_trace_line(timestamp, event, work, context, thread_id)
41
+
42
+ # Output to trace stream
43
+ trace_stream.puts(trace_line)
44
+ end
45
+
46
+ # Set a custom trace stream.
47
+ #
48
+ # @param io [IO] The output stream
49
+ def trace_stream=(io)
50
+ @trace_stream = io
51
+ end
52
+
53
+ # Enable tracing.
54
+ def enable!
55
+ @enabled = true
56
+ end
57
+
58
+ # Disable tracing.
59
+ def disable!
60
+ @enabled = false
61
+ end
62
+
63
+ # Check if tracing is enabled.
64
+ #
65
+ # @return [Boolean] true if tracing is enabled
66
+ def enabled?
67
+ @enabled || (@check_env && ENV["FRACTOR_TRACE"] == "1")
68
+ end
69
+
70
+ # Reset tracer state.
71
+ def reset!
72
+ @enabled = nil
73
+ @trace_stream = $stderr
74
+ end
75
+
76
+ # Class-level convenience methods for backward compatibility.
77
+ # These use a singleton instance for global tracing.
78
+ class << self
79
+ # Enable or disable tracing (global).
80
+ #
81
+ # @param value [Boolean] Whether to enable tracing
82
+ def enabled=(value)
83
+ instance.enabled = value
84
+ end
85
+
86
+ # Check if global tracing is enabled.
87
+ #
88
+ # @return [Boolean] true if tracing is enabled
89
+ def enabled?
90
+ instance.enabled?
91
+ end
92
+
93
+ # Trace an event using the global tracer instance.
94
+ #
95
+ # @param event [Symbol] The event type
96
+ # @param work [Work] The work item
97
+ # @param context [Hash] Additional context
98
+ def trace(event, work = nil, context = {})
99
+ instance.trace(event, work, context)
100
+ end
101
+
102
+ # Get the global trace stream.
103
+ #
104
+ # @return [IO] The output stream
105
+ def trace_stream
106
+ instance.trace_stream
107
+ end
108
+
109
+ # Set a custom global trace stream.
110
+ #
111
+ # @param io [IO] The output stream
112
+ def trace_stream=(io)
113
+ instance.trace_stream = io
114
+ end
115
+
116
+ # Reset all global state (useful for testing and isolation).
117
+ def reset!
118
+ instance.reset!
119
+ end
120
+
121
+ # Get the singleton tracer instance.
122
+ #
123
+ # @return [ExecutionTracer] The global instance
124
+ def instance
125
+ @instance ||= new
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ # Build a formatted trace line.
132
+ #
133
+ # @param timestamp [String] Formatted timestamp
134
+ # @param event [Symbol] The event type
135
+ # @param work [Work] The work item
136
+ # @param context [Hash] Additional context
137
+ # @param thread_id [Integer] Thread ID
138
+ # @return [String] Formatted trace line
139
+ def build_trace_line(timestamp, event, work, context, thread_id)
140
+ parts = [
141
+ "[TRACE]",
142
+ timestamp,
143
+ "[T#{thread_id}]",
144
+ event.to_s.upcase,
145
+ ]
146
+
147
+ # Add work item info if available
148
+ if work
149
+ work_info = work.instance_of?(::Fractor::Work) ? "Work" : work.class.name
150
+ parts << "#{work_info}:#{work.object_id}"
151
+ end
152
+
153
+ # Add context info
154
+ parts << "worker=#{context[:worker_name]}" if context[:worker_name]
155
+ parts << "class=#{context[:worker_class]}" if context[:worker_class]
156
+ parts << "duration=#{context[:duration_ms]}ms" if context[:duration_ms]
157
+ parts << "queue_size=#{context[:queue_size]}" if context[:queue_size]
158
+
159
+ parts.join(" ")
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Fractor
6
+ class << self
7
+ attr_writer :logger
8
+
9
+ # Get the Fractor logger instance
10
+ # @return [Logger] Logger instance
11
+ def logger
12
+ @logger ||= create_default_logger
13
+ end
14
+
15
+ # Enable debug logging to STDOUT
16
+ # @return [Logger] The configured logger
17
+ def enable_logging(level = Logger::DEBUG)
18
+ main_logger = create_logger_for_output($stdout, level)
19
+ @logger = main_logger
20
+ main_logger
21
+ end
22
+
23
+ # Disable logging entirely
24
+ def disable_logging
25
+ @logger = create_disabled_logger
26
+ end
27
+
28
+ # Check if debug logging is enabled
29
+ def debug_enabled?
30
+ @logger&.debug?
31
+ end
32
+
33
+ private
34
+
35
+ # Create default logger with sensible defaults
36
+ # Respects FRACTOR_LOG_LEVEL and FRACTOR_LOG_OUTPUT environment variables
37
+ # @return [Logger] Configured logger instance
38
+ def create_default_logger
39
+ # Get log level from environment variable or use INFO as default
40
+ level = parse_log_level(ENV["FRACTOR_LOG_LEVEL"]) || Logger::INFO
41
+
42
+ # Get output destination from environment variable or use STDOUT as default
43
+ output = parse_log_output(ENV["FRACTOR_LOG_OUTPUT"]) || $stdout
44
+
45
+ create_logger_for_output(output, level)
46
+ end
47
+
48
+ # Create a logger for the specified output with the given level
49
+ # @param output [IO, String, nil] Output destination
50
+ # @param level [Integer] Log level
51
+ # @return [Logger] Configured logger instance
52
+ def create_logger_for_output(output, level)
53
+ logger = Logger.new(output)
54
+ logger.level = level
55
+ logger.formatter = proc do |severity, datetime, _progname, msg|
56
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
57
+ end
58
+ logger
59
+ end
60
+
61
+ # Parse log level from environment variable string
62
+ # @param level_str [String, nil] Log level string (DEBUG, INFO, WARN, ERROR)
63
+ # @return [Integer, nil] Logger constant or nil if invalid
64
+ def parse_log_level(level_str)
65
+ return nil unless level_str
66
+
67
+ case level_str.to_s.upcase
68
+ when "DEBUG" then Logger::DEBUG
69
+ when "INFO" then Logger::INFO
70
+ when "WARN" then Logger::WARN
71
+ when "ERROR" then Logger::ERROR
72
+ when "FATAL" then Logger::FATAL
73
+ when "UNKNOWN" then Logger::UNKNOWN
74
+ end
75
+ end
76
+
77
+ # Parse log output destination from environment variable string
78
+ # @param output_str [String, nil] Output destination (stdout, stderr, or file path)
79
+ # @return [IO, nil] Output destination
80
+ def parse_log_output(output_str)
81
+ return nil unless output_str
82
+
83
+ case output_str.to_s.downcase
84
+ when "stdout" then $stdout
85
+ when "stderr" then $stderr
86
+ else
87
+ # Treat as file path
88
+ begin
89
+ File.open(output_str.to_s, "a")
90
+ rescue ArgumentError, IOError => e
91
+ warn "Failed to open log file #{output_str}: #{e.message}, using STDOUT"
92
+ $stdout
93
+ end
94
+ end
95
+ end
96
+
97
+ # Create a disabled logger that outputs nothing
98
+ # @return [Logger] Disabled logger instance
99
+ def create_disabled_logger
100
+ logger = Logger.new(nil)
101
+ logger.level = Logger::UNKNOWN
102
+ logger
103
+ end
104
+
105
+ # Reset all global state (useful for testing and isolation)
106
+ def reset!
107
+ @logger = nil
108
+ end
109
+ end
110
+
111
+ # Ractor-safe logging module.
112
+ #
113
+ # This module provides logging functionality that works correctly
114
+ # within Ractors by using $stderr for unbuffered output.
115
+ #
116
+ # @example Inside a worker or ractor
117
+ # Fractor::RactorLogger.debug("Processing work", ractor_name: "worker-1")
118
+ # Fractor::RactorLogger.info("Worker started", ractor_name: "worker-1")
119
+ # Fractor::RactorLogger.warn("Long processing time", ractor_name: "worker-1")
120
+ # Fractor::RactorLogger.error("Worker failed", ractor_name: "worker-1", exception: e)
121
+ #
122
+ module RactorLogger
123
+ # Log levels in order of severity
124
+ LEVELS = {
125
+ debug: 0,
126
+ info: 1,
127
+ warn: 2,
128
+ error: 3,
129
+ }.freeze
130
+
131
+ class << self
132
+ # Get or set the current log level
133
+ # @return [Symbol] Current log level (:debug, :info, :warn, :error)
134
+ attr_accessor :level
135
+
136
+ # Enable or disable logging
137
+ # @return [Boolean] Whether logging is enabled
138
+ attr_accessor :enabled
139
+
140
+ # Enable debug mode (sets level to :debug)
141
+ def debug!
142
+ self.level = :debug
143
+ self.enabled = true
144
+ end
145
+
146
+ # Disable debug mode (sets level to :warn)
147
+ def nodebug!
148
+ self.level = :warn
149
+ self.enabled = false
150
+ end
151
+
152
+ # Check if a given log level would be logged
153
+ # @param lvl [Symbol] Log level to check
154
+ # @return [Boolean] True if messages at this level would be logged
155
+ def log?(lvl)
156
+ enabled && LEVELS[lvl.to_sym] >= LEVELS[level]
157
+ end
158
+
159
+ # Log a debug message
160
+ # @param message [String] Message to log
161
+ # @param ractor_name [String, nil] Name of the ractor (optional)
162
+ def debug(message, ractor_name: nil)
163
+ return unless log?(:debug)
164
+
165
+ log(:debug, message, ractor_name: ractor_name)
166
+ end
167
+
168
+ # Log an info message
169
+ # @param message [String] Message to log
170
+ # @param ractor_name [String, nil] Name of the ractor (optional)
171
+ def info(message, ractor_name: nil)
172
+ return unless log?(:info)
173
+
174
+ log(:info, message, ractor_name: ractor_name)
175
+ end
176
+
177
+ # Log a warning message
178
+ # @param message [String] Message to log
179
+ # @param ractor_name [String, nil] Name of the ractor (optional)
180
+ def warn(message, ractor_name: nil)
181
+ return unless log?(:warn)
182
+
183
+ log(:warn, message, ractor_name: ractor_name)
184
+ end
185
+
186
+ # Log an error message
187
+ # @param message [String] Message to log
188
+ # @param ractor_name [String, nil] Name of the ractor (optional)
189
+ # @param exception [Exception, nil] Exception object (optional)
190
+ def error(message, ractor_name: nil, exception: nil)
191
+ return unless log?(:error)
192
+
193
+ log(:error, message, ractor_name: ractor_name, exception: exception)
194
+ end
195
+
196
+ private
197
+
198
+ # Internal logging method
199
+ # @param level [Symbol] Log level
200
+ # @param message [String] Message to log
201
+ # @param ractor_name [String, nil] Name of the ractor
202
+ # @param exception [Exception, nil] Exception object
203
+ def log(level, message, ractor_name: nil, exception: nil)
204
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
205
+ level_tag = level.to_s.upcase.ljust(5)
206
+
207
+ # Format: [TIMESTAMP] [LEVEL] [RACTOR] message
208
+ ractor_tag = ractor_name ? "[#{ractor_name}] " : ""
209
+ output = "[#{timestamp}] [#{level_tag}] #{ractor_tag}#{message}"
210
+
211
+ # Always use $stderr for immediate, unbuffered output
212
+ warn(output)
213
+ $stderr.flush
214
+
215
+ # If there's an exception, log the stack trace
216
+ if exception
217
+ warn("[#{timestamp}] [#{level_tag}] #{ractor_tag}#{exception.class}: #{exception.message}")
218
+ exception.backtrace&.each do |line|
219
+ warn("[#{timestamp}] [#{level_tag}] #{ractor_tag} #{line}")
220
+ end
221
+ $stderr.flush
222
+ end
223
+ end
224
+ end
225
+
226
+ # Initialize with defaults - check FRACTOR_DEBUG environment variable
227
+ @enabled = ["1", "true"].include?(ENV["FRACTOR_DEBUG"])
228
+ @level = @enabled ? :debug : :info
229
+ end
230
+ end