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.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +91 -0
  4. data/CHANGELOG.md +23 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +47 -0
  7. data/Rakefile +55 -0
  8. data/Steepfile +10 -0
  9. data/guides/API_REFERENCE.md +112 -0
  10. data/guides/BEST_PRACTICES.md +113 -0
  11. data/guides/CACHE_STORE_INTEGRATION.md +145 -0
  12. data/guides/CONDITIONAL_EXECUTION.md +66 -0
  13. data/guides/DEPENDENCY_WAIT.md +386 -0
  14. data/guides/DRY_RUN.md +390 -0
  15. data/guides/DSL_BASICS.md +216 -0
  16. data/guides/ERROR_HANDLING.md +187 -0
  17. data/guides/GETTING_STARTED.md +524 -0
  18. data/guides/INSTRUMENTATION.md +131 -0
  19. data/guides/LIFECYCLE_HOOKS.md +415 -0
  20. data/guides/NAMESPACES.md +75 -0
  21. data/guides/OPENTELEMETRY_INTEGRATION.md +86 -0
  22. data/guides/PARALLEL_PROCESSING.md +302 -0
  23. data/guides/PRODUCTION_DEPLOYMENT.md +110 -0
  24. data/guides/QUEUE_MANAGEMENT.md +141 -0
  25. data/guides/README.md +174 -0
  26. data/guides/SCHEDULED_JOBS.md +165 -0
  27. data/guides/STRUCTURED_LOGGING.md +268 -0
  28. data/guides/TASK_OUTPUTS.md +240 -0
  29. data/guides/TESTING_STRATEGY.md +56 -0
  30. data/guides/THROTTLING.md +198 -0
  31. data/guides/TROUBLESHOOTING.md +53 -0
  32. data/guides/WORKFLOW_COMPOSITION.md +675 -0
  33. data/guides/WORKFLOW_STATUS_QUERY.md +288 -0
  34. data/lib/job-workflow.rb +3 -0
  35. data/lib/job_workflow/argument_def.rb +16 -0
  36. data/lib/job_workflow/arguments.rb +40 -0
  37. data/lib/job_workflow/auto_scaling/adapter/aws_adapter.rb +66 -0
  38. data/lib/job_workflow/auto_scaling/adapter.rb +31 -0
  39. data/lib/job_workflow/auto_scaling/configuration.rb +85 -0
  40. data/lib/job_workflow/auto_scaling/executor.rb +43 -0
  41. data/lib/job_workflow/auto_scaling.rb +69 -0
  42. data/lib/job_workflow/cache_store_adapters.rb +46 -0
  43. data/lib/job_workflow/context.rb +352 -0
  44. data/lib/job_workflow/dry_run_config.rb +31 -0
  45. data/lib/job_workflow/dsl.rb +236 -0
  46. data/lib/job_workflow/error_hook.rb +24 -0
  47. data/lib/job_workflow/hook.rb +24 -0
  48. data/lib/job_workflow/hook_registry.rb +66 -0
  49. data/lib/job_workflow/instrumentation/log_subscriber.rb +194 -0
  50. data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +221 -0
  51. data/lib/job_workflow/instrumentation.rb +257 -0
  52. data/lib/job_workflow/job_status.rb +92 -0
  53. data/lib/job_workflow/logger.rb +86 -0
  54. data/lib/job_workflow/namespace.rb +36 -0
  55. data/lib/job_workflow/output.rb +81 -0
  56. data/lib/job_workflow/output_def.rb +14 -0
  57. data/lib/job_workflow/queue.rb +74 -0
  58. data/lib/job_workflow/queue_adapter.rb +38 -0
  59. data/lib/job_workflow/queue_adapters/abstract.rb +87 -0
  60. data/lib/job_workflow/queue_adapters/null_adapter.rb +127 -0
  61. data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +224 -0
  62. data/lib/job_workflow/runner.rb +173 -0
  63. data/lib/job_workflow/schedule.rb +46 -0
  64. data/lib/job_workflow/semaphore.rb +71 -0
  65. data/lib/job_workflow/task.rb +83 -0
  66. data/lib/job_workflow/task_callable.rb +43 -0
  67. data/lib/job_workflow/task_context.rb +70 -0
  68. data/lib/job_workflow/task_dependency_wait.rb +66 -0
  69. data/lib/job_workflow/task_enqueue.rb +50 -0
  70. data/lib/job_workflow/task_graph.rb +43 -0
  71. data/lib/job_workflow/task_job_status.rb +70 -0
  72. data/lib/job_workflow/task_output.rb +51 -0
  73. data/lib/job_workflow/task_retry.rb +64 -0
  74. data/lib/job_workflow/task_throttle.rb +46 -0
  75. data/lib/job_workflow/version.rb +5 -0
  76. data/lib/job_workflow/workflow.rb +87 -0
  77. data/lib/job_workflow/workflow_status.rb +112 -0
  78. data/lib/job_workflow.rb +59 -0
  79. data/rbs_collection.lock.yaml +172 -0
  80. data/rbs_collection.yaml +14 -0
  81. data/sig/generated/job-workflow.rbs +2 -0
  82. data/sig/generated/job_workflow/argument_def.rbs +14 -0
  83. data/sig/generated/job_workflow/arguments.rbs +26 -0
  84. data/sig/generated/job_workflow/auto_scaling/adapter/aws_adapter.rbs +32 -0
  85. data/sig/generated/job_workflow/auto_scaling/adapter.rbs +22 -0
  86. data/sig/generated/job_workflow/auto_scaling/configuration.rbs +50 -0
  87. data/sig/generated/job_workflow/auto_scaling/executor.rbs +29 -0
  88. data/sig/generated/job_workflow/auto_scaling.rbs +47 -0
  89. data/sig/generated/job_workflow/cache_store_adapters.rbs +28 -0
  90. data/sig/generated/job_workflow/context.rbs +155 -0
  91. data/sig/generated/job_workflow/dry_run_config.rbs +16 -0
  92. data/sig/generated/job_workflow/dsl.rbs +117 -0
  93. data/sig/generated/job_workflow/error_hook.rbs +18 -0
  94. data/sig/generated/job_workflow/hook.rbs +18 -0
  95. data/sig/generated/job_workflow/hook_registry.rbs +47 -0
  96. data/sig/generated/job_workflow/instrumentation/log_subscriber.rbs +102 -0
  97. data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +113 -0
  98. data/sig/generated/job_workflow/instrumentation.rbs +138 -0
  99. data/sig/generated/job_workflow/job_status.rbs +46 -0
  100. data/sig/generated/job_workflow/logger.rbs +56 -0
  101. data/sig/generated/job_workflow/namespace.rbs +24 -0
  102. data/sig/generated/job_workflow/output.rbs +39 -0
  103. data/sig/generated/job_workflow/output_def.rbs +12 -0
  104. data/sig/generated/job_workflow/queue.rbs +49 -0
  105. data/sig/generated/job_workflow/queue_adapter.rbs +18 -0
  106. data/sig/generated/job_workflow/queue_adapters/abstract.rbs +56 -0
  107. data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +73 -0
  108. data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +111 -0
  109. data/sig/generated/job_workflow/runner.rbs +66 -0
  110. data/sig/generated/job_workflow/schedule.rbs +34 -0
  111. data/sig/generated/job_workflow/semaphore.rbs +37 -0
  112. data/sig/generated/job_workflow/task.rbs +60 -0
  113. data/sig/generated/job_workflow/task_callable.rbs +30 -0
  114. data/sig/generated/job_workflow/task_context.rbs +52 -0
  115. data/sig/generated/job_workflow/task_dependency_wait.rbs +42 -0
  116. data/sig/generated/job_workflow/task_enqueue.rbs +27 -0
  117. data/sig/generated/job_workflow/task_graph.rbs +27 -0
  118. data/sig/generated/job_workflow/task_job_status.rbs +42 -0
  119. data/sig/generated/job_workflow/task_output.rbs +29 -0
  120. data/sig/generated/job_workflow/task_retry.rbs +30 -0
  121. data/sig/generated/job_workflow/task_throttle.rbs +20 -0
  122. data/sig/generated/job_workflow/version.rbs +5 -0
  123. data/sig/generated/job_workflow/workflow.rbs +48 -0
  124. data/sig/generated/job_workflow/workflow_status.rbs +55 -0
  125. data/sig/generated/job_workflow.rbs +8 -0
  126. data/sig-private/activejob.rbs +35 -0
  127. data/sig-private/activesupport.rbs +23 -0
  128. data/sig-private/aws.rbs +32 -0
  129. data/sig-private/opentelemetry.rbs +40 -0
  130. data/sig-private/solid_queue.rbs +108 -0
  131. data/tmp/.keep +0 -0
  132. 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