fractor 0.1.6 → 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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +227 -102
- data/README.adoc +113 -1940
- data/docs/.lycheeignore +16 -0
- data/docs/Gemfile +24 -0
- data/docs/README.md +157 -0
- data/docs/_config.yml +151 -0
- data/docs/_features/error-handling.adoc +1192 -0
- data/docs/_features/index.adoc +80 -0
- data/docs/_features/monitoring.adoc +589 -0
- data/docs/_features/signal-handling.adoc +202 -0
- data/docs/_features/workflows.adoc +1235 -0
- data/docs/_guides/continuous-mode.adoc +736 -0
- data/docs/_guides/cookbook.adoc +1133 -0
- data/docs/_guides/index.adoc +55 -0
- data/docs/_guides/pipeline-mode.adoc +730 -0
- data/docs/_guides/troubleshooting.adoc +358 -0
- data/docs/_pages/architecture.adoc +1390 -0
- data/docs/_pages/core-concepts.adoc +1392 -0
- data/docs/_pages/design-principles.adoc +862 -0
- data/docs/_pages/getting-started.adoc +290 -0
- data/docs/_pages/installation.adoc +143 -0
- data/docs/_reference/api.adoc +1080 -0
- data/docs/_reference/error-reporting.adoc +670 -0
- data/docs/_reference/examples.adoc +181 -0
- data/docs/_reference/index.adoc +96 -0
- data/docs/_reference/troubleshooting.adoc +862 -0
- data/docs/_tutorials/complex-workflows.adoc +1022 -0
- data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
- data/docs/_tutorials/first-application.adoc +384 -0
- data/docs/_tutorials/index.adoc +48 -0
- data/docs/_tutorials/long-running-services.adoc +931 -0
- data/docs/assets/images/favicon-16.png +0 -0
- data/docs/assets/images/favicon-32.png +0 -0
- data/docs/assets/images/favicon-48.png +0 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/images/favicon.svg +45 -0
- data/docs/assets/images/fractor-icon.svg +49 -0
- data/docs/assets/images/fractor-logo.svg +61 -0
- data/docs/index.adoc +131 -0
- data/docs/lychee.toml +39 -0
- data/examples/api_aggregator/README.adoc +627 -0
- data/examples/api_aggregator/api_aggregator.rb +376 -0
- data/examples/auto_detection/README.adoc +407 -29
- data/examples/continuous_chat_common/message_protocol.rb +1 -1
- data/examples/error_reporting.rb +207 -0
- data/examples/file_processor/README.adoc +170 -0
- data/examples/file_processor/file_processor.rb +615 -0
- data/examples/file_processor/sample_files/invalid.csv +1 -0
- data/examples/file_processor/sample_files/orders.xml +24 -0
- data/examples/file_processor/sample_files/products.json +23 -0
- data/examples/file_processor/sample_files/users.csv +6 -0
- data/examples/hierarchical_hasher/README.adoc +629 -41
- data/examples/image_processor/README.adoc +610 -0
- data/examples/image_processor/image_processor.rb +349 -0
- data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
- data/examples/image_processor/test_images/sample_1.png +1 -0
- data/examples/image_processor/test_images/sample_10.png +1 -0
- data/examples/image_processor/test_images/sample_2.png +1 -0
- data/examples/image_processor/test_images/sample_3.png +1 -0
- data/examples/image_processor/test_images/sample_4.png +1 -0
- data/examples/image_processor/test_images/sample_5.png +1 -0
- data/examples/image_processor/test_images/sample_6.png +1 -0
- data/examples/image_processor/test_images/sample_7.png +1 -0
- data/examples/image_processor/test_images/sample_8.png +1 -0
- data/examples/image_processor/test_images/sample_9.png +1 -0
- data/examples/log_analyzer/README.adoc +662 -0
- data/examples/log_analyzer/log_analyzer.rb +579 -0
- data/examples/log_analyzer/sample_logs/apache.log +20 -0
- data/examples/log_analyzer/sample_logs/json.log +15 -0
- data/examples/log_analyzer/sample_logs/nginx.log +15 -0
- data/examples/log_analyzer/sample_logs/rails.log +29 -0
- data/examples/multi_work_type/README.adoc +576 -26
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +2 -2
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/simple/README.adoc +347 -0
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +44 -8
- data/examples/stream_processor/README.adoc +206 -0
- data/examples/stream_processor/stream_processor.rb +284 -0
- data/examples/web_scraper/README.adoc +625 -0
- data/examples/web_scraper/web_scraper.rb +285 -0
- data/examples/workflow/README.adoc +406 -0
- data/examples/workflow/circuit_breaker/README.adoc +360 -0
- data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
- data/examples/workflow/conditional/README.adoc +483 -0
- data/examples/workflow/conditional/conditional_workflow.rb +215 -0
- data/examples/workflow/dead_letter_queue/README.adoc +374 -0
- data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
- data/examples/workflow/fan_out/README.adoc +381 -0
- data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
- data/examples/workflow/retry/README.adoc +248 -0
- data/examples/workflow/retry/retry_workflow.rb +195 -0
- data/examples/workflow/simple_linear/README.adoc +267 -0
- data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
- data/examples/workflow/simplified/README.adoc +329 -0
- data/examples/workflow/simplified/simplified_workflow.rb +222 -0
- data/exe/fractor +10 -0
- data/lib/fractor/cli.rb +288 -0
- data/lib/fractor/configuration.rb +307 -0
- data/lib/fractor/continuous_server.rb +60 -65
- data/lib/fractor/error_formatter.rb +72 -0
- data/lib/fractor/error_report_generator.rb +152 -0
- data/lib/fractor/error_reporter.rb +244 -0
- data/lib/fractor/error_statistics.rb +147 -0
- data/lib/fractor/execution_tracer.rb +162 -0
- data/lib/fractor/logger.rb +230 -0
- data/lib/fractor/main_loop_handler.rb +406 -0
- data/lib/fractor/main_loop_handler3.rb +135 -0
- data/lib/fractor/main_loop_handler4.rb +299 -0
- data/lib/fractor/performance_metrics_collector.rb +181 -0
- data/lib/fractor/performance_monitor.rb +215 -0
- data/lib/fractor/performance_report_generator.rb +202 -0
- data/lib/fractor/priority_work.rb +93 -0
- data/lib/fractor/priority_work_queue.rb +189 -0
- data/lib/fractor/result_aggregator.rb +32 -0
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +382 -269
- data/lib/fractor/supervisor_logger.rb +88 -0
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work.rb +12 -0
- data/lib/fractor/work_distribution_manager.rb +151 -0
- data/lib/fractor/work_queue.rb +20 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +73 -0
- data/lib/fractor/workflow/builder.rb +210 -0
- data/lib/fractor/workflow/chain_builder.rb +169 -0
- data/lib/fractor/workflow/circuit_breaker.rb +183 -0
- data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
- data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
- data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
- data/lib/fractor/workflow/execution_hooks.rb +39 -0
- data/lib/fractor/workflow/execution_strategy.rb +225 -0
- data/lib/fractor/workflow/execution_trace.rb +134 -0
- data/lib/fractor/workflow/helpers.rb +191 -0
- data/lib/fractor/workflow/job.rb +290 -0
- data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
- data/lib/fractor/workflow/logger.rb +110 -0
- data/lib/fractor/workflow/pre_execution_context.rb +193 -0
- data/lib/fractor/workflow/retry_config.rb +156 -0
- data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
- data/lib/fractor/workflow/retry_strategy.rb +93 -0
- data/lib/fractor/workflow/structured_logger.rb +30 -0
- data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
- data/lib/fractor/workflow/visualizer.rb +211 -0
- data/lib/fractor/workflow/workflow_context.rb +132 -0
- data/lib/fractor/workflow/workflow_executor.rb +669 -0
- data/lib/fractor/workflow/workflow_result.rb +55 -0
- data/lib/fractor/workflow/workflow_validator.rb +295 -0
- data/lib/fractor/workflow.rb +333 -0
- data/lib/fractor/wrapped_ractor.rb +66 -101
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +92 -4
- metadata +179 -6
- data/tests/sample.rb.bak +0 -309
- 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
|