job-workflow 0.1.3
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/.rspec +3 -0
- data/.rubocop.yml +91 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +55 -0
- data/Steepfile +10 -0
- data/guides/API_REFERENCE.md +112 -0
- data/guides/BEST_PRACTICES.md +113 -0
- data/guides/CACHE_STORE_INTEGRATION.md +145 -0
- data/guides/CONDITIONAL_EXECUTION.md +66 -0
- data/guides/DEPENDENCY_WAIT.md +386 -0
- data/guides/DRY_RUN.md +390 -0
- data/guides/DSL_BASICS.md +216 -0
- data/guides/ERROR_HANDLING.md +187 -0
- data/guides/GETTING_STARTED.md +524 -0
- data/guides/INSTRUMENTATION.md +131 -0
- data/guides/LIFECYCLE_HOOKS.md +415 -0
- data/guides/NAMESPACES.md +75 -0
- data/guides/OPENTELEMETRY_INTEGRATION.md +86 -0
- data/guides/PARALLEL_PROCESSING.md +302 -0
- data/guides/PRODUCTION_DEPLOYMENT.md +110 -0
- data/guides/QUEUE_MANAGEMENT.md +141 -0
- data/guides/README.md +174 -0
- data/guides/SCHEDULED_JOBS.md +165 -0
- data/guides/STRUCTURED_LOGGING.md +268 -0
- data/guides/TASK_OUTPUTS.md +240 -0
- data/guides/TESTING_STRATEGY.md +56 -0
- data/guides/THROTTLING.md +198 -0
- data/guides/TROUBLESHOOTING.md +53 -0
- data/guides/WORKFLOW_COMPOSITION.md +675 -0
- data/guides/WORKFLOW_STATUS_QUERY.md +288 -0
- data/lib/job-workflow.rb +3 -0
- data/lib/job_workflow/argument_def.rb +16 -0
- data/lib/job_workflow/arguments.rb +40 -0
- data/lib/job_workflow/auto_scaling/adapter/aws_adapter.rb +66 -0
- data/lib/job_workflow/auto_scaling/adapter.rb +31 -0
- data/lib/job_workflow/auto_scaling/configuration.rb +85 -0
- data/lib/job_workflow/auto_scaling/executor.rb +43 -0
- data/lib/job_workflow/auto_scaling.rb +69 -0
- data/lib/job_workflow/cache_store_adapters.rb +46 -0
- data/lib/job_workflow/context.rb +352 -0
- data/lib/job_workflow/dry_run_config.rb +31 -0
- data/lib/job_workflow/dsl.rb +236 -0
- data/lib/job_workflow/error_hook.rb +24 -0
- data/lib/job_workflow/hook.rb +24 -0
- data/lib/job_workflow/hook_registry.rb +66 -0
- data/lib/job_workflow/instrumentation/log_subscriber.rb +194 -0
- data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +221 -0
- data/lib/job_workflow/instrumentation.rb +257 -0
- data/lib/job_workflow/job_status.rb +92 -0
- data/lib/job_workflow/logger.rb +86 -0
- data/lib/job_workflow/namespace.rb +36 -0
- data/lib/job_workflow/output.rb +81 -0
- data/lib/job_workflow/output_def.rb +14 -0
- data/lib/job_workflow/queue.rb +74 -0
- data/lib/job_workflow/queue_adapter.rb +38 -0
- data/lib/job_workflow/queue_adapters/abstract.rb +87 -0
- data/lib/job_workflow/queue_adapters/null_adapter.rb +127 -0
- data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +224 -0
- data/lib/job_workflow/runner.rb +173 -0
- data/lib/job_workflow/schedule.rb +46 -0
- data/lib/job_workflow/semaphore.rb +71 -0
- data/lib/job_workflow/task.rb +83 -0
- data/lib/job_workflow/task_callable.rb +43 -0
- data/lib/job_workflow/task_context.rb +70 -0
- data/lib/job_workflow/task_dependency_wait.rb +66 -0
- data/lib/job_workflow/task_enqueue.rb +50 -0
- data/lib/job_workflow/task_graph.rb +43 -0
- data/lib/job_workflow/task_job_status.rb +70 -0
- data/lib/job_workflow/task_output.rb +51 -0
- data/lib/job_workflow/task_retry.rb +64 -0
- data/lib/job_workflow/task_throttle.rb +46 -0
- data/lib/job_workflow/version.rb +5 -0
- data/lib/job_workflow/workflow.rb +87 -0
- data/lib/job_workflow/workflow_status.rb +112 -0
- data/lib/job_workflow.rb +59 -0
- data/rbs_collection.lock.yaml +172 -0
- data/rbs_collection.yaml +14 -0
- data/sig/generated/job-workflow.rbs +2 -0
- data/sig/generated/job_workflow/argument_def.rbs +14 -0
- data/sig/generated/job_workflow/arguments.rbs +26 -0
- data/sig/generated/job_workflow/auto_scaling/adapter/aws_adapter.rbs +32 -0
- data/sig/generated/job_workflow/auto_scaling/adapter.rbs +22 -0
- data/sig/generated/job_workflow/auto_scaling/configuration.rbs +50 -0
- data/sig/generated/job_workflow/auto_scaling/executor.rbs +29 -0
- data/sig/generated/job_workflow/auto_scaling.rbs +47 -0
- data/sig/generated/job_workflow/cache_store_adapters.rbs +28 -0
- data/sig/generated/job_workflow/context.rbs +155 -0
- data/sig/generated/job_workflow/dry_run_config.rbs +16 -0
- data/sig/generated/job_workflow/dsl.rbs +117 -0
- data/sig/generated/job_workflow/error_hook.rbs +18 -0
- data/sig/generated/job_workflow/hook.rbs +18 -0
- data/sig/generated/job_workflow/hook_registry.rbs +47 -0
- data/sig/generated/job_workflow/instrumentation/log_subscriber.rbs +102 -0
- data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +113 -0
- data/sig/generated/job_workflow/instrumentation.rbs +138 -0
- data/sig/generated/job_workflow/job_status.rbs +46 -0
- data/sig/generated/job_workflow/logger.rbs +56 -0
- data/sig/generated/job_workflow/namespace.rbs +24 -0
- data/sig/generated/job_workflow/output.rbs +39 -0
- data/sig/generated/job_workflow/output_def.rbs +12 -0
- data/sig/generated/job_workflow/queue.rbs +49 -0
- data/sig/generated/job_workflow/queue_adapter.rbs +18 -0
- data/sig/generated/job_workflow/queue_adapters/abstract.rbs +56 -0
- data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +73 -0
- data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +111 -0
- data/sig/generated/job_workflow/runner.rbs +66 -0
- data/sig/generated/job_workflow/schedule.rbs +34 -0
- data/sig/generated/job_workflow/semaphore.rbs +37 -0
- data/sig/generated/job_workflow/task.rbs +60 -0
- data/sig/generated/job_workflow/task_callable.rbs +30 -0
- data/sig/generated/job_workflow/task_context.rbs +52 -0
- data/sig/generated/job_workflow/task_dependency_wait.rbs +42 -0
- data/sig/generated/job_workflow/task_enqueue.rbs +27 -0
- data/sig/generated/job_workflow/task_graph.rbs +27 -0
- data/sig/generated/job_workflow/task_job_status.rbs +42 -0
- data/sig/generated/job_workflow/task_output.rbs +29 -0
- data/sig/generated/job_workflow/task_retry.rbs +30 -0
- data/sig/generated/job_workflow/task_throttle.rbs +20 -0
- data/sig/generated/job_workflow/version.rbs +5 -0
- data/sig/generated/job_workflow/workflow.rbs +48 -0
- data/sig/generated/job_workflow/workflow_status.rbs +55 -0
- data/sig/generated/job_workflow.rbs +8 -0
- data/sig-private/activejob.rbs +35 -0
- data/sig-private/activesupport.rbs +23 -0
- data/sig-private/aws.rbs +32 -0
- data/sig-private/opentelemetry.rbs +40 -0
- data/sig-private/solid_queue.rbs +108 -0
- data/tmp/.keep +0 -0
- metadata +190 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobWorkflow
|
|
4
|
+
class HookRegistry
|
|
5
|
+
#: () -> void
|
|
6
|
+
def initialize
|
|
7
|
+
self.before_hooks = [] #: Array[Hook]
|
|
8
|
+
self.after_hooks = [] #: Array[Hook]
|
|
9
|
+
self.around_hooks = [] #: Array[Hook]
|
|
10
|
+
self.error_hooks = [] #: Array[ErrorHook]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
#: (task_names: Array[Symbol], block: ^(Context, ?TaskCallable) -> void) -> void
|
|
14
|
+
def add_before_hook(task_names:, block:)
|
|
15
|
+
before_hooks << Hook.new(task_names:, block:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: (task_names: Array[Symbol], block: ^(Context, ?TaskCallable) -> void) -> void
|
|
19
|
+
def add_after_hook(task_names:, block:)
|
|
20
|
+
after_hooks << Hook.new(task_names:, block:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#: (task_names: Array[Symbol], block: ^(Context, ?TaskCallable) -> void) -> void
|
|
24
|
+
def add_around_hook(task_names:, block:)
|
|
25
|
+
around_hooks << Hook.new(task_names:, block:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: (task_names: Array[Symbol], block: ^(Context, StandardError, Task) -> void) -> void
|
|
29
|
+
def add_error_hook(task_names:, block:)
|
|
30
|
+
error_hooks << ErrorHook.new(task_names:, block:)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: (Symbol) -> Array[Hook]
|
|
34
|
+
def before_hooks_for(task_name)
|
|
35
|
+
hooks_for(before_hooks, task_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#: (Symbol) -> Array[Hook]
|
|
39
|
+
def after_hooks_for(task_name)
|
|
40
|
+
hooks_for(after_hooks, task_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#: (Symbol) -> Array[Hook]
|
|
44
|
+
def around_hooks_for(task_name)
|
|
45
|
+
hooks_for(around_hooks, task_name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#: (Symbol) -> Array[ErrorHook]
|
|
49
|
+
def error_hooks_for(task_name)
|
|
50
|
+
hooks_for(error_hooks, task_name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
attr_accessor :before_hooks #: Array[Hook]
|
|
56
|
+
attr_accessor :after_hooks #: Array[Hook]
|
|
57
|
+
attr_accessor :around_hooks #: Array[Hook]
|
|
58
|
+
attr_accessor :error_hooks #: Array[ErrorHook]
|
|
59
|
+
|
|
60
|
+
#: (Array[Hook], Symbol) -> Array[Hook]
|
|
61
|
+
#: (Array[ErrorHook], Symbol) -> Array[ErrorHook]
|
|
62
|
+
def hooks_for(hooks, task_name)
|
|
63
|
+
hooks.filter { |hook| hook.applies_to?(task_name) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobWorkflow
|
|
4
|
+
module Instrumentation
|
|
5
|
+
# LogSubscriber handles JobWorkflow instrumentation events and produces structured JSON logs.
|
|
6
|
+
# It subscribes to ActiveSupport::Notifications events and formats them for logging.
|
|
7
|
+
#
|
|
8
|
+
# @example Enable log subscriber
|
|
9
|
+
# ```ruby
|
|
10
|
+
# JobWorkflow::Instrumentation::LogSubscriber.attach_to(:job_workflow)
|
|
11
|
+
# ```
|
|
12
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
13
|
+
class << self
|
|
14
|
+
#: () -> void
|
|
15
|
+
def attach!
|
|
16
|
+
attach_to(NAMESPACE.to_sym)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs!
|
|
21
|
+
# type log_level = :debug | :info | :warn | :error
|
|
22
|
+
|
|
23
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
24
|
+
def workflow(event)
|
|
25
|
+
# Tracing only - no log output (start/complete events handle logging)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
29
|
+
def workflow_start(event)
|
|
30
|
+
log_event(event, :info)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
34
|
+
def workflow_complete(event)
|
|
35
|
+
log_event(event, :info)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
39
|
+
def task(event)
|
|
40
|
+
# Tracing only - no log output (start/complete events handle logging)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
44
|
+
def task_start(event)
|
|
45
|
+
log_event(event, :info)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
49
|
+
def task_complete(event)
|
|
50
|
+
log_event(event, :info)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
54
|
+
def task_error(event)
|
|
55
|
+
log_event(event, :error)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
59
|
+
def task_skip(event)
|
|
60
|
+
log_event(event, :info)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
64
|
+
def task_enqueue(event)
|
|
65
|
+
log_event(event, :info)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
69
|
+
def task_retry(event)
|
|
70
|
+
log_event(event, :warn)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
74
|
+
def throttle_acquire(event)
|
|
75
|
+
# Tracing only - no log output (start/complete events handle logging)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
79
|
+
def throttle_acquire_start(event)
|
|
80
|
+
log_event(event, :debug)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
84
|
+
def throttle_acquire_complete(event)
|
|
85
|
+
log_event(event, :debug)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
89
|
+
def throttle_release(event)
|
|
90
|
+
log_event(event, :debug)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
94
|
+
def dependent_wait(event)
|
|
95
|
+
# Tracing only - no log output (start/complete events handle logging)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
99
|
+
def dependent_wait_start(event)
|
|
100
|
+
log_event(event, :debug)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
104
|
+
def dependent_wait_complete(event)
|
|
105
|
+
log_event(event, :debug)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
109
|
+
def queue_pause(event)
|
|
110
|
+
log_event(event, :info)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
114
|
+
def queue_resume(event)
|
|
115
|
+
log_event(event, :info)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
119
|
+
def dry_run(event)
|
|
120
|
+
# Tracing only - no log output (skip/execute events handle logging)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
124
|
+
def dry_run_skip(event)
|
|
125
|
+
log_event(event, :info)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
#: (ActiveSupport::Notifications::Event) -> void
|
|
129
|
+
def dry_run_execute(event)
|
|
130
|
+
log_event(event, :debug)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
#: (ActiveSupport::Notifications::Event, log_level) -> void
|
|
136
|
+
def log_event(event, level)
|
|
137
|
+
payload_hash = build_log_payload(event)
|
|
138
|
+
send_log(level, payload_hash)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
#: (ActiveSupport::Notifications::Event) -> Hash[Symbol, untyped]
|
|
142
|
+
def build_log_payload(event)
|
|
143
|
+
base = { event: event.name, duration_ms: event.duration&.round(3) }
|
|
144
|
+
base.merge(extract_loggable_attributes(event.payload))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
148
|
+
def extract_loggable_attributes(payload)
|
|
149
|
+
payload_keys = %i[
|
|
150
|
+
job_id
|
|
151
|
+
job_name
|
|
152
|
+
task_name
|
|
153
|
+
each_index
|
|
154
|
+
retry_count
|
|
155
|
+
reason
|
|
156
|
+
sub_job_count
|
|
157
|
+
attempt
|
|
158
|
+
max_attempts
|
|
159
|
+
delay_seconds
|
|
160
|
+
concurrency_key
|
|
161
|
+
concurrency_limit
|
|
162
|
+
dependent_task_name
|
|
163
|
+
queue_name
|
|
164
|
+
dry_run_name
|
|
165
|
+
dry_run_index
|
|
166
|
+
dry_run
|
|
167
|
+
]
|
|
168
|
+
result = payload.slice(*payload_keys)
|
|
169
|
+
add_error_attributes(result, payload)
|
|
170
|
+
result
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
#: (Hash[Symbol, untyped], Hash[Symbol, untyped]) -> void
|
|
174
|
+
def add_error_attributes(result, payload)
|
|
175
|
+
return unless payload.key?(:error)
|
|
176
|
+
|
|
177
|
+
error = payload[:error]
|
|
178
|
+
result.merge!(
|
|
179
|
+
error_class: payload[:error_class] || error.class.name,
|
|
180
|
+
error_message: payload[:error_message] || error.message
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
#: (log_level, Hash[Symbol, untyped]) -> void
|
|
185
|
+
def send_log(level, payload)
|
|
186
|
+
return JobWorkflow.logger.debug(payload) if level == :debug
|
|
187
|
+
return JobWorkflow.logger.warn(payload) if level == :warn
|
|
188
|
+
return JobWorkflow.logger.error(payload) if level == :error
|
|
189
|
+
|
|
190
|
+
JobWorkflow.logger.info(payload)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobWorkflow
|
|
4
|
+
module Instrumentation
|
|
5
|
+
# OpenTelemetrySubscriber provides OpenTelemetry tracing integration for JobWorkflow events.
|
|
6
|
+
# It subscribes to ActiveSupport::Notifications and creates OpenTelemetry spans.
|
|
7
|
+
#
|
|
8
|
+
# @example Enable OpenTelemetry integration
|
|
9
|
+
# ```ruby
|
|
10
|
+
# # Ensure OpenTelemetry is configured first
|
|
11
|
+
# OpenTelemetry::SDK.configure do |c|
|
|
12
|
+
# c.service_name = "my-app"
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# # Then subscribe JobWorkflow events
|
|
16
|
+
# JobWorkflow::Instrumentation::OpenTelemetrySubscriber.subscribe!
|
|
17
|
+
# ```
|
|
18
|
+
#
|
|
19
|
+
# @note This subscriber requires the opentelemetry-api gem to be installed.
|
|
20
|
+
# If not available, subscription will be silently skipped.
|
|
21
|
+
class OpenTelemetrySubscriber # rubocop:disable Metrics/ClassLength
|
|
22
|
+
module Attributes
|
|
23
|
+
JOB_NAME = "#{NAMESPACE}.job.name".freeze #: String
|
|
24
|
+
JOB_ID = "#{NAMESPACE}.job.id".freeze #: String
|
|
25
|
+
TASK_NAME = "#{NAMESPACE}.task.name".freeze #: String
|
|
26
|
+
TASK_EACH_INDEX = "#{NAMESPACE}.task.each_index".freeze #: String
|
|
27
|
+
TASK_RETRY_COUNT = "#{NAMESPACE}.task.retry_count".freeze #: String
|
|
28
|
+
WORKFLOW_NAME = "#{NAMESPACE}.workflow.name".freeze #: String
|
|
29
|
+
ERROR_CLASS = "#{NAMESPACE}.error.class".freeze #: String
|
|
30
|
+
ERROR_MESSAGE = "#{NAMESPACE}.error.message".freeze #: String
|
|
31
|
+
CONCURRENCY_KEY = "#{NAMESPACE}.concurrency.key".freeze #: String
|
|
32
|
+
CONCURRENCY_LIMIT = "#{NAMESPACE}.concurrency.limit".freeze #: String
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
SUBSCRIBED_EVENTS = [
|
|
36
|
+
Events::WORKFLOW,
|
|
37
|
+
Events::TASK,
|
|
38
|
+
Events::TASK_SKIP,
|
|
39
|
+
Events::TASK_ENQUEUE,
|
|
40
|
+
Events::TASK_RETRY,
|
|
41
|
+
Events::THROTTLE_ACQUIRE,
|
|
42
|
+
Events::DEPENDENT_WAIT
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
# @rbs!
|
|
46
|
+
# def self.subscriptions: () -> Array[untyped]
|
|
47
|
+
# def self.subscriptions=: (Array[untyped]) -> void
|
|
48
|
+
cattr_accessor :subscriptions, instance_accessor: false, default: []
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
#: () -> Array[untyped]?
|
|
52
|
+
def subscribe!
|
|
53
|
+
return unless opentelemetry_available?
|
|
54
|
+
return subscriptions unless subscriptions.empty?
|
|
55
|
+
|
|
56
|
+
self.subscriptions = SUBSCRIBED_EVENTS.map { |event| ActiveSupport::Notifications.subscribe(event, new) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#: () -> void
|
|
60
|
+
def unsubscribe!
|
|
61
|
+
return if subscriptions.empty?
|
|
62
|
+
|
|
63
|
+
subscriptions.each { |sub| ActiveSupport::Notifications.unsubscribe(sub) }
|
|
64
|
+
self.subscriptions = []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#: () -> void
|
|
68
|
+
def reset!
|
|
69
|
+
unsubscribe!
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#: () -> bool
|
|
73
|
+
def opentelemetry_available?
|
|
74
|
+
!!defined?(::OpenTelemetry::Trace)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
#: (String, String, Hash[Symbol, untyped]) -> void
|
|
79
|
+
def start(name, _id, payload)
|
|
80
|
+
return unless self.class.opentelemetry_available?
|
|
81
|
+
|
|
82
|
+
span = start_span(name, payload)
|
|
83
|
+
token = attach_span_context(span)
|
|
84
|
+
store_span_info(payload, span, token)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
handle_error(e)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
#: (String, String, Hash[Symbol, untyped]) -> void
|
|
90
|
+
def finish(_name, _id, payload)
|
|
91
|
+
return unless self.class.opentelemetry_available?
|
|
92
|
+
|
|
93
|
+
span, token = extract_otel_info(payload)
|
|
94
|
+
return if span.nil? || token.nil?
|
|
95
|
+
|
|
96
|
+
handle_exception(payload, span)
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
handle_error(e)
|
|
99
|
+
ensure
|
|
100
|
+
finish_span(span, token) if span || token
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
#: (Hash[Symbol, untyped]) -> Array[untyped?]
|
|
106
|
+
def extract_otel_info(payload)
|
|
107
|
+
otel = payload.delete(:__otel)
|
|
108
|
+
span = otel&.fetch(:span)
|
|
109
|
+
token = otel&.fetch(:ctx_token)
|
|
110
|
+
[span, token]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def start_span(name, payload)
|
|
114
|
+
span_name = build_span_name(name, payload)
|
|
115
|
+
attributes = build_attributes(payload)
|
|
116
|
+
kind = determine_span_kind(name)
|
|
117
|
+
|
|
118
|
+
tracer.start_span(span_name, kind:, attributes:)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
#: (untyped) -> untyped
|
|
122
|
+
def attach_span_context(span)
|
|
123
|
+
OpenTelemetry::Context.attach(OpenTelemetry::Trace.context_with_span(span))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
#: (Hash[Symbol, untyped], untyped, untyped) -> void
|
|
127
|
+
def store_span_info(payload, span, token)
|
|
128
|
+
payload[:__otel] = { span: span, ctx_token: token }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
#: (Hash[Symbol, untyped], untyped) -> void
|
|
132
|
+
def handle_exception(payload, span)
|
|
133
|
+
error = payload[:error] || payload[:exception_object]
|
|
134
|
+
return unless error
|
|
135
|
+
|
|
136
|
+
span.record_exception(error)
|
|
137
|
+
span.status = OpenTelemetry::Trace::Status.error("Unhandled exception: #{error.class}")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#: (untyped, untyped) -> void
|
|
141
|
+
def finish_span(span, token)
|
|
142
|
+
finish_span_safe(span)
|
|
143
|
+
detach_context_safe(token)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
#: (untyped) -> void
|
|
147
|
+
def finish_span_safe(span)
|
|
148
|
+
return unless span&.recording?
|
|
149
|
+
|
|
150
|
+
span.status = OpenTelemetry::Trace::Status.ok if span.status.code == OpenTelemetry::Trace::Status::UNSET
|
|
151
|
+
span.finish
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
handle_error(e)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
#: (untyped) -> void
|
|
157
|
+
def detach_context_safe(token)
|
|
158
|
+
OpenTelemetry::Context.detach(token) if token
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
handle_error(e)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
#: (String, Hash[Symbol, untyped]) -> String
|
|
164
|
+
def build_span_name(event_name, payload)
|
|
165
|
+
base_name = event_name.delete_suffix(".#{Instrumentation::NAMESPACE}")
|
|
166
|
+
|
|
167
|
+
return "#{payload[:job_name]}.#{payload[:task_name]} #{base_name}" if payload[:task_name]
|
|
168
|
+
return "#{payload[:job_name]} #{base_name}" if payload[:job_name]
|
|
169
|
+
|
|
170
|
+
"JobWorkflow #{base_name}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
#: (Hash[Symbol, untyped]) -> Hash[String, untyped]
|
|
174
|
+
def build_attributes(payload)
|
|
175
|
+
attrs = {
|
|
176
|
+
Attributes::JOB_NAME => payload[:job_name],
|
|
177
|
+
Attributes::JOB_ID => payload[:job_id],
|
|
178
|
+
Attributes::TASK_NAME => payload[:task_name],
|
|
179
|
+
Attributes::TASK_EACH_INDEX => payload[:each_index],
|
|
180
|
+
Attributes::TASK_RETRY_COUNT => payload[:retry_count],
|
|
181
|
+
Attributes::CONCURRENCY_KEY => payload[:concurrency_key],
|
|
182
|
+
Attributes::CONCURRENCY_LIMIT => payload[:concurrency_limit]
|
|
183
|
+
}.compact
|
|
184
|
+
add_error_attributes(attrs, payload)
|
|
185
|
+
attrs
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
#: (Hash[String, untyped], Hash[Symbol, untyped]) -> void
|
|
189
|
+
def add_error_attributes(attrs, payload)
|
|
190
|
+
return unless payload[:error]
|
|
191
|
+
|
|
192
|
+
attrs.merge!(
|
|
193
|
+
Attributes::ERROR_CLASS => payload[:error_class] || payload[:error].class.name,
|
|
194
|
+
Attributes::ERROR_MESSAGE => payload[:error_message] || payload[:error].message
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
#: (String) -> Symbol
|
|
199
|
+
def determine_span_kind(event_name)
|
|
200
|
+
case event_name
|
|
201
|
+
when Events::TASK_ENQUEUE
|
|
202
|
+
:producer
|
|
203
|
+
else
|
|
204
|
+
:internal
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
#: () -> untyped
|
|
209
|
+
def tracer
|
|
210
|
+
OpenTelemetry.tracer_provider.tracer(NAMESPACE, JobWorkflow::VERSION)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
#: (StandardError) -> void
|
|
214
|
+
def handle_error(error)
|
|
215
|
+
return unless defined?(OpenTelemetry) && OpenTelemetry.respond_to?(:handle_error)
|
|
216
|
+
|
|
217
|
+
OpenTelemetry.handle_error(exception: error, message: "JobWorkflow OpenTelemetry subscriber error")
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|