activejob-temporal 0.1.0

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +130 -0
  3. data/LICENSE +21 -0
  4. data/README.md +198 -0
  5. data/activejob-temporal.gemspec +58 -0
  6. data/api/job_payload_schema.json +318 -0
  7. data/bin/temporal-worker +295 -0
  8. data/lib/activejob/temporal/active_job_handler_source.rb +84 -0
  9. data/lib/activejob/temporal/activities/aj_runner_activity.rb +454 -0
  10. data/lib/activejob/temporal/activities/best_effort_side_effects.rb +49 -0
  11. data/lib/activejob/temporal/activities/dependency_status_activity.rb +160 -0
  12. data/lib/activejob/temporal/activities/rate_limit_activity.rb +41 -0
  13. data/lib/activejob/temporal/adapter.rb +257 -0
  14. data/lib/activejob/temporal/audit_log.rb +118 -0
  15. data/lib/activejob/temporal/batch_enqueue_result.rb +110 -0
  16. data/lib/activejob/temporal/batch_enqueuer.rb +141 -0
  17. data/lib/activejob/temporal/bind_policy.rb +44 -0
  18. data/lib/activejob/temporal/cancel/batch_canceller.rb +154 -0
  19. data/lib/activejob/temporal/cancel/batch_summary.rb +45 -0
  20. data/lib/activejob/temporal/cancel.rb +236 -0
  21. data/lib/activejob/temporal/certificate_watcher.rb +76 -0
  22. data/lib/activejob/temporal/chain_options.rb +83 -0
  23. data/lib/activejob/temporal/child_workflow_options.rb +102 -0
  24. data/lib/activejob/temporal/client.rb +215 -0
  25. data/lib/activejob/temporal/conditional_enqueue.rb +56 -0
  26. data/lib/activejob/temporal/configurable.rb +55 -0
  27. data/lib/activejob/temporal/configuration.rb +981 -0
  28. data/lib/activejob/temporal/configured_job_compatibility.rb +44 -0
  29. data/lib/activejob/temporal/connection_worker_pool.rb +88 -0
  30. data/lib/activejob/temporal/dead_letter_payload_validation.rb +34 -0
  31. data/lib/activejob/temporal/dead_letter_queue.rb +163 -0
  32. data/lib/activejob/temporal/dependency_options.rb +134 -0
  33. data/lib/activejob/temporal/external_operation.rb +193 -0
  34. data/lib/activejob/temporal/health_check_server.rb +159 -0
  35. data/lib/activejob/temporal/http_line_reader.rb +36 -0
  36. data/lib/activejob/temporal/inspect.rb +184 -0
  37. data/lib/activejob/temporal/job_descriptor.rb +37 -0
  38. data/lib/activejob/temporal/job_payload_builder.rb +209 -0
  39. data/lib/activejob/temporal/job_payload_chain_builder.rb +106 -0
  40. data/lib/activejob/temporal/job_payload_child_workflows.rb +127 -0
  41. data/lib/activejob/temporal/job_payload_dependencies.rb +40 -0
  42. data/lib/activejob/temporal/job_payload_rate_limits.rb +53 -0
  43. data/lib/activejob/temporal/job_payload_workflow_interactions.rb +31 -0
  44. data/lib/activejob/temporal/job_tags.rb +40 -0
  45. data/lib/activejob/temporal/locales/en.yml +126 -0
  46. data/lib/activejob/temporal/logger.rb +214 -0
  47. data/lib/activejob/temporal/metrics_server.rb +150 -0
  48. data/lib/activejob/temporal/middleware/chain.rb +106 -0
  49. data/lib/activejob/temporal/middleware.rb +11 -0
  50. data/lib/activejob/temporal/observability/datadog.rb +167 -0
  51. data/lib/activejob/temporal/observability/opentelemetry.rb +107 -0
  52. data/lib/activejob/temporal/observability/prometheus.rb +271 -0
  53. data/lib/activejob/temporal/observability.rb +260 -0
  54. data/lib/activejob/temporal/payload.rb +415 -0
  55. data/lib/activejob/temporal/payload_encryption.rb +215 -0
  56. data/lib/activejob/temporal/payload_serializers/json.rb +23 -0
  57. data/lib/activejob/temporal/payload_serializers/marshal.rb +53 -0
  58. data/lib/activejob/temporal/payload_serializers/message_pack.rb +59 -0
  59. data/lib/activejob/temporal/payload_serializers.rb +37 -0
  60. data/lib/activejob/temporal/payload_storage.rb +103 -0
  61. data/lib/activejob/temporal/rails_environment_loader.rb +143 -0
  62. data/lib/activejob/temporal/rate_limit_options.rb +94 -0
  63. data/lib/activejob/temporal/rate_limiters/memory.rb +198 -0
  64. data/lib/activejob/temporal/reload_signal_queue.rb +40 -0
  65. data/lib/activejob/temporal/retry_handler_extractor.rb +361 -0
  66. data/lib/activejob/temporal/retry_mapper.rb +264 -0
  67. data/lib/activejob/temporal/schedulable.rb +60 -0
  68. data/lib/activejob/temporal/schedule.rb +181 -0
  69. data/lib/activejob/temporal/schedule_options.rb +105 -0
  70. data/lib/activejob/temporal/search_attributes.rb +173 -0
  71. data/lib/activejob/temporal/signal_query.rb +161 -0
  72. data/lib/activejob/temporal/signal_query_options.rb +106 -0
  73. data/lib/activejob/temporal/temporal_options.rb +114 -0
  74. data/lib/activejob/temporal/tls_file.rb +45 -0
  75. data/lib/activejob/temporal/transaction_safety.rb +39 -0
  76. data/lib/activejob/temporal/version.rb +7 -0
  77. data/lib/activejob/temporal/visibility_query.rb +13 -0
  78. data/lib/activejob/temporal/worker_client_reloader.rb +34 -0
  79. data/lib/activejob/temporal/worker_health.rb +117 -0
  80. data/lib/activejob/temporal/worker_pool.rb +408 -0
  81. data/lib/activejob/temporal/workflow_enqueuer.rb +271 -0
  82. data/lib/activejob/temporal/workflow_enqueuer_batch.rb +17 -0
  83. data/lib/activejob/temporal/workflow_id_builder.rb +155 -0
  84. data/lib/activejob/temporal/workflow_identity.rb +62 -0
  85. data/lib/activejob/temporal/workflows/aj_workflow.rb +282 -0
  86. data/lib/activejob/temporal/workflows/dead_letter_support.rb +134 -0
  87. data/lib/activejob/temporal/workflows/dead_letter_workflow.rb +114 -0
  88. data/lib/activejob/temporal/workflows/workflow_chaining.rb +194 -0
  89. data/lib/activejob/temporal/workflows/workflow_child_workflows.rb +140 -0
  90. data/lib/activejob/temporal/workflows/workflow_continue_as_new.rb +44 -0
  91. data/lib/activejob/temporal/workflows/workflow_dependencies.rb +115 -0
  92. data/lib/activejob/temporal/workflows/workflow_execution_steps.rb +22 -0
  93. data/lib/activejob/temporal/workflows/workflow_interactions.rb +215 -0
  94. data/lib/activejob/temporal/workflows/workflow_local_activities.rb +29 -0
  95. data/lib/activejob/temporal/workflows/workflow_nexus.rb +15 -0
  96. data/lib/activejob/temporal/workflows/workflow_versioning.rb +21 -0
  97. data/lib/activejob/temporal.rb +297 -0
  98. data/lib/activejob-temporal.rb +3 -0
  99. metadata +423 -0
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../observability"
4
+ require_relative "../version"
5
+
6
+ module ActiveJob
7
+ module Temporal
8
+ module Observability
9
+ class OpenTelemetry < Adapter
10
+ SPAN_EVENTS = %i[enqueue perform retry].freeze
11
+
12
+ attr_writer :tracer, :propagation
13
+
14
+ def initialize(tracer: nil, propagation: nil)
15
+ super(:opentelemetry)
16
+ @tracer = tracer
17
+ @propagation = propagation
18
+ end
19
+
20
+ def trace_context_for_enqueue(_payload)
21
+ carrier = {}
22
+ propagation.inject(carrier)
23
+ carrier
24
+ end
25
+
26
+ def record(event_name, payload)
27
+ return unless event_name == :enqueue
28
+
29
+ trace(:enqueue, payload) { nil }
30
+ end
31
+
32
+ def instrument(event_name, payload, &block)
33
+ return block.call unless SPAN_EVENTS.include?(event_name)
34
+
35
+ trace(event_name, payload, &block)
36
+ end
37
+
38
+ def validate_dependencies!
39
+ require_dependency("opentelemetry-sdk", "opentelemetry/sdk", "OpenTelemetry")
40
+ self
41
+ end
42
+
43
+ private
44
+
45
+ def trace(event_name, payload, &)
46
+ context = extracted_context(payload)
47
+ if context
48
+ ::OpenTelemetry::Context.with_current(context) do
49
+ trace_span(event_name, payload, &)
50
+ end
51
+ else
52
+ trace_span(event_name, payload, &)
53
+ end
54
+ end
55
+
56
+ def trace_span(event_name, payload)
57
+ tracer.in_span(span_name(event_name), attributes: span_attributes(payload)) do |span|
58
+ yield
59
+ rescue StandardError => e
60
+ span.record_exception(e) if span.respond_to?(:record_exception)
61
+ raise
62
+ end
63
+ end
64
+
65
+ def extracted_context(payload)
66
+ carrier = Observability.trace_context_from_payload(payload).fetch("opentelemetry", nil)
67
+ return if carrier.nil? || carrier.empty?
68
+
69
+ propagation.extract(carrier)
70
+ end
71
+
72
+ def tracer
73
+ @tracer ||= ::OpenTelemetry.tracer_provider.tracer(
74
+ "activejob-temporal",
75
+ ActiveJob::Temporal::VERSION
76
+ )
77
+ end
78
+
79
+ def propagation
80
+ @propagation ||= ::OpenTelemetry.propagation
81
+ end
82
+
83
+ def span_name(event_name)
84
+ "activejob_temporal.#{event_name}"
85
+ end
86
+
87
+ def span_attributes(payload)
88
+ {
89
+ "activejob_temporal.job_class" => payload[:job_class],
90
+ "activejob_temporal.job_id" => payload[:job_id],
91
+ "activejob_temporal.queue" => payload[:queue],
92
+ "activejob_temporal.workflow_id" => payload[:workflow_id],
93
+ "activejob_temporal.run_id" => payload[:run_id],
94
+ "activejob_temporal.namespace" => payload[:namespace],
95
+ "activejob_temporal.task_queue" => payload[:task_queue],
96
+ "activejob_temporal.attempt" => payload[:attempt]
97
+ }.compact
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ ActiveJob::Temporal::Observability.register_adapter(
105
+ :opentelemetry,
106
+ ActiveJob::Temporal::Observability::OpenTelemetry
107
+ )
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../metrics_server"
4
+ require_relative "../observability"
5
+
6
+ module ActiveJob
7
+ module Temporal
8
+ module Observability
9
+ class MetricsServerConfiguration
10
+ attr_accessor :port, :bind, :allow_public_bind
11
+
12
+ def initialize
13
+ @port = nil
14
+ @bind = MetricsServer::DEFAULT_BIND_ADDRESS
15
+ @allow_public_bind = false
16
+ end
17
+ end
18
+
19
+ module PrometheusErrorLabels
20
+ LABEL_CLASSES = [
21
+ ActiveJob::DeserializationError,
22
+ ActiveJob::SerializationError,
23
+ NoMethodError,
24
+ NameError,
25
+ ArgumentError,
26
+ TypeError,
27
+ LoadError,
28
+ SystemCallError,
29
+ IOError,
30
+ RuntimeError,
31
+ StandardError,
32
+ ScriptError,
33
+ Exception
34
+ ].freeze
35
+
36
+ module_function
37
+
38
+ def for(error)
39
+ error_class = error_class_for(error)
40
+ label_class = LABEL_CLASSES.find { |klass| error_class <= klass }
41
+
42
+ (label_class&.name || "Unknown").to_s
43
+ end
44
+
45
+ def error_class_for(error)
46
+ return error if error.is_a?(Class)
47
+ return error.class if error.is_a?(Exception)
48
+
49
+ LABEL_CLASSES.find { |klass| klass.name == error.to_s } || StandardError
50
+ end
51
+ end
52
+
53
+ # rubocop:disable Metrics/ClassLength
54
+ class Prometheus < Adapter
55
+ DURATION_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120].freeze
56
+ PAYLOAD_SIZE_BUCKETS = [512, 1024, 2_048, 4_096, 8_192, 16_384, 32_768, 65_536, 131_072, 262_144,
57
+ 524_288, 1_048_576].freeze
58
+
59
+ attr_reader :registry, :metrics_server
60
+
61
+ def initialize(registry: nil, monotonic_clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
62
+ super(:prometheus)
63
+ @registry = registry
64
+ @monotonic_clock = monotonic_clock
65
+ @metrics_server = MetricsServerConfiguration.new
66
+ @server = nil
67
+ @registered = false
68
+ end
69
+
70
+ def start!
71
+ super
72
+ ensure_metrics_registered
73
+ self
74
+ end
75
+
76
+ def stop!
77
+ stop_metrics_server
78
+ super
79
+ end
80
+
81
+ def record(event_name, payload)
82
+ ensure_metrics_registered
83
+
84
+ case event_name
85
+ when :enqueue then record_enqueue(payload)
86
+ when :payload_serialize then observe_payload_size(payload)
87
+ when :retry then record_retry(payload)
88
+ when :worker_start then record_worker_started
89
+ when :worker_stop then record_worker_stopped
90
+ when :active_tasks then record_active_tasks(payload[:count])
91
+ end
92
+ end
93
+
94
+ def instrument(event_name, payload)
95
+ return yield unless event_name == :perform
96
+
97
+ ensure_metrics_registered
98
+ started_at = monotonic_time
99
+
100
+ result = yield
101
+ @jobs_completed.increment(labels: { class: label(payload[:job_class]), queue: label(payload[:queue]) })
102
+ result
103
+ rescue StandardError => e
104
+ @jobs_failed.increment(labels: {
105
+ class: label(payload[:job_class]),
106
+ queue: label(payload[:queue]),
107
+ error: PrometheusErrorLabels.for(e)
108
+ })
109
+ raise
110
+ ensure
111
+ if started_at
112
+ @job_duration.observe(monotonic_time - started_at, labels: { class: label(payload[:job_class]) })
113
+ end
114
+ end
115
+
116
+ def render
117
+ ensure_metrics_registered
118
+ ::Prometheus::Client::Formats::Text.marshal(registry)
119
+ end
120
+
121
+ def start_metrics_server(port: metrics_server.port,
122
+ bind_address: metrics_server.bind,
123
+ allow_public_bind: metrics_server.allow_public_bind)
124
+ raise ArgumentError, "Prometheus metrics server port is required" unless port
125
+
126
+ @server = MetricsServer.new(
127
+ port: port,
128
+ bind_address: bind_address,
129
+ allow_public_bind: allow_public_bind,
130
+ provider: self
131
+ ).start
132
+ end
133
+
134
+ def stop_metrics_server
135
+ @server&.stop
136
+ @server = nil
137
+ end
138
+
139
+ def validate_dependencies!
140
+ require_dependency("prometheus-client", "prometheus/client", "Prometheus")
141
+ require_dependency("prometheus-client", "prometheus/client/formats/text", "Prometheus")
142
+ @registry ||= ::Prometheus::Client::Registry.new
143
+ self
144
+ end
145
+
146
+ private
147
+
148
+ def record_enqueue(payload)
149
+ return if payload[:duplicate]
150
+
151
+ @jobs_enqueued.increment(labels: { class: label(payload[:job_class]), queue: label(payload[:queue]) })
152
+ end
153
+
154
+ def observe_payload_size(payload)
155
+ @payload_size.observe(payload[:bytes], labels: { class: label(payload[:job_class]) })
156
+ end
157
+
158
+ def record_retry(payload)
159
+ @retries.increment(labels: {
160
+ class: label(payload[:job_class]),
161
+ error: PrometheusErrorLabels.for(payload[:error])
162
+ })
163
+ end
164
+
165
+ def record_worker_started
166
+ @active_workers.set(1)
167
+ end
168
+
169
+ def record_worker_stopped
170
+ @active_workers.set(0)
171
+ record_active_tasks(0)
172
+ end
173
+
174
+ def record_active_tasks(count)
175
+ @active_tasks.set(count.to_i)
176
+ end
177
+
178
+ def ensure_metrics_registered
179
+ validate_dependencies!
180
+ return if @registered
181
+
182
+ register_metrics
183
+ @registered = true
184
+ end
185
+
186
+ def register_metrics
187
+ register_counters
188
+ register_histograms
189
+ register_gauges
190
+ end
191
+
192
+ def register_counters
193
+ @jobs_enqueued = register_counter(
194
+ :activejob_temporal_jobs_enqueued_total,
195
+ "ActiveJob jobs successfully enqueued as Temporal workflows.",
196
+ labels: %i[class queue]
197
+ )
198
+ @jobs_completed = register_counter(
199
+ :activejob_temporal_jobs_completed_total,
200
+ "ActiveJob jobs completed by Temporal activities.",
201
+ labels: %i[class queue]
202
+ )
203
+ @jobs_failed = register_counter(
204
+ :activejob_temporal_jobs_failed_total,
205
+ "ActiveJob jobs failed during Temporal activity execution.",
206
+ labels: %i[class queue error]
207
+ )
208
+ @retries = register_counter(
209
+ :activejob_temporal_retries_total,
210
+ "ActiveJob Temporal retry attempts that failed.",
211
+ labels: %i[class error]
212
+ )
213
+ end
214
+
215
+ def register_histograms
216
+ @job_duration = register_histogram(
217
+ :activejob_temporal_job_duration_seconds,
218
+ "ActiveJob Temporal activity runner duration in seconds.",
219
+ labels: %i[class],
220
+ buckets: DURATION_BUCKETS
221
+ )
222
+ @payload_size = register_histogram(
223
+ :activejob_temporal_payload_size_bytes,
224
+ "Serialized ActiveJob payload size in bytes.",
225
+ labels: %i[class],
226
+ buckets: PAYLOAD_SIZE_BUCKETS
227
+ )
228
+ end
229
+
230
+ def register_gauges
231
+ @active_workers = register_gauge(
232
+ :activejob_temporal_active_workers,
233
+ "Active Temporal worker process state for this scrape target."
234
+ )
235
+ @active_tasks = register_gauge(
236
+ :activejob_temporal_active_tasks,
237
+ "Active Temporal activity tasks in this worker process."
238
+ )
239
+ end
240
+
241
+ def register_counter(name, docstring, labels: [])
242
+ registry.register(::Prometheus::Client::Counter.new(name, docstring: docstring, labels: labels))
243
+ end
244
+
245
+ def register_histogram(name, docstring, labels:, buckets:)
246
+ registry.register(
247
+ ::Prometheus::Client::Histogram.new(name, docstring: docstring, labels: labels, buckets: buckets)
248
+ )
249
+ end
250
+
251
+ def register_gauge(name, docstring)
252
+ registry.register(::Prometheus::Client::Gauge.new(name, docstring: docstring))
253
+ end
254
+
255
+ def monotonic_time
256
+ @monotonic_clock.call
257
+ end
258
+
259
+ def label(value)
260
+ (value || "unknown").to_s
261
+ end
262
+ end
263
+ # rubocop:enable Metrics/ClassLength
264
+ end
265
+ end
266
+ end
267
+
268
+ ActiveJob::Temporal::Observability.register_adapter(
269
+ :prometheus,
270
+ ActiveJob::Temporal::Observability::Prometheus
271
+ )
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+
5
+ module ActiveJob
6
+ module Temporal
7
+ class Error < StandardError; end unless const_defined?(:Error, false)
8
+
9
+ module Observability
10
+ EVENT_NAMESPACE = "activejob_temporal"
11
+ TRACE_CONTEXT_KEY = :observability
12
+
13
+ class Error < ActiveJob::Temporal::Error; end
14
+ class MissingDependency < Error; end
15
+ class UnknownAdapter < Error; end
16
+
17
+ class << self
18
+ def register_adapter(name, adapter_class)
19
+ adapter_registry[name.to_sym] = adapter_class
20
+ end
21
+
22
+ def adapter_class(name)
23
+ adapter_registry.fetch(name.to_sym) do
24
+ raise UnknownAdapter, "Unknown observability adapter: #{name.inspect}"
25
+ end
26
+ end
27
+
28
+ def emit(name, payload = {})
29
+ event_payload = normalize_payload(payload)
30
+ ActiveSupport::Notifications.instrument(event_name(name), event_payload)
31
+ active_adapters.each { |adapter| adapter.record(name.to_sym, event_payload) }
32
+ nil
33
+ end
34
+
35
+ def instrument(name, payload = {}, &)
36
+ event_payload = normalize_payload(payload)
37
+
38
+ ActiveSupport::Notifications.instrument(event_name(name), event_payload) do
39
+ instrument_adapters(name.to_sym, event_payload, &)
40
+ end
41
+ end
42
+
43
+ def trace_context_for_enqueue(payload = {})
44
+ active_adapters.each_with_object({}) do |adapter, context|
45
+ next unless adapter.respond_to?(:trace_context_for_enqueue)
46
+
47
+ adapter_context = adapter.trace_context_for_enqueue(payload)
48
+ context[adapter.name.to_s] = adapter_context if adapter_context && !adapter_context.empty?
49
+ end
50
+ end
51
+
52
+ def inject_trace_context(payload, attributes = {})
53
+ trace_context = trace_context_for_enqueue(attributes)
54
+ return payload if trace_context.empty?
55
+
56
+ observability = payload[:observability] || payload["observability"] || {}
57
+ observability = observability.merge("trace_context" => trace_context)
58
+ payload[:observability] = observability
59
+ payload
60
+ end
61
+
62
+ def trace_context_from_payload(payload)
63
+ observability = payload[:observability] || payload["observability"] || {}
64
+ observability[:trace_context] || observability["trace_context"] || {}
65
+ end
66
+
67
+ def attributes_from_job(job, **attributes)
68
+ normalize_payload(
69
+ {
70
+ job_class: job.class.name,
71
+ job_id: job.job_id,
72
+ queue: job.queue_name
73
+ }.merge(attributes)
74
+ )
75
+ end
76
+
77
+ def attributes_from_payload(payload, **attributes)
78
+ normalize_payload(
79
+ {
80
+ job_class: payload_value(payload, :job_class),
81
+ job_id: payload_value(payload, :job_id),
82
+ queue: payload_value(payload, :queue_name) || payload_value(payload, :queue),
83
+ task_queue: payload_value(payload, :activity_task_queue)
84
+ }.merge(activity_context_attributes).merge(attributes)
85
+ )
86
+ end
87
+
88
+ def retry_attempt?
89
+ attempt = activity_context_attributes[:attempt]
90
+ attempt && attempt.to_i > 1
91
+ end
92
+
93
+ def reset!
94
+ configuration.reset!
95
+ end
96
+
97
+ def configuration
98
+ ActiveJob::Temporal.config.observability
99
+ end
100
+
101
+ def event_name(name)
102
+ event = name.to_s
103
+ return event if event.end_with?(".#{EVENT_NAMESPACE}")
104
+
105
+ "#{event}.#{EVENT_NAMESPACE}"
106
+ end
107
+
108
+ private
109
+
110
+ def adapter_registry
111
+ @adapter_registry ||= {}
112
+ end
113
+
114
+ def active_adapters
115
+ return [] unless ActiveJob::Temporal.respond_to?(:config)
116
+
117
+ configuration.adapters
118
+ rescue StandardError
119
+ []
120
+ end
121
+
122
+ def instrument_adapters(name, payload, &block)
123
+ active_adapters.reverse.reduce(block) do |inner, adapter|
124
+ proc { adapter.instrument(name, payload, &inner) }
125
+ end.call
126
+ end
127
+
128
+ def normalize_payload(payload)
129
+ payload.each_with_object({}) do |(key, value), normalized|
130
+ normalized[key.to_sym] = value unless value.nil?
131
+ end
132
+ end
133
+
134
+ def payload_value(payload, key)
135
+ payload[key] || payload[key.to_s]
136
+ end
137
+
138
+ def activity_context_attributes
139
+ return {} unless defined?(Temporalio::Activity::Context)
140
+ return {} unless Temporalio::Activity::Context.exist?
141
+
142
+ info = Temporalio::Activity::Context.current.info
143
+ normalize_payload(
144
+ workflow_id: context_value(info, :workflow_id),
145
+ run_id: context_value(info, :workflow_run_id, :run_id),
146
+ namespace: context_value(info, :workflow_namespace),
147
+ attempt: context_value(info, :attempt)
148
+ )
149
+ rescue StandardError
150
+ {}
151
+ end
152
+
153
+ def context_value(object, *methods)
154
+ methods.each do |method_name|
155
+ return object.public_send(method_name) if object.respond_to?(method_name)
156
+ end
157
+
158
+ nil
159
+ end
160
+ end
161
+
162
+ class Configuration
163
+ attr_reader :adapters
164
+
165
+ def initialize
166
+ @adapters = []
167
+ end
168
+
169
+ def use(name, **)
170
+ adapter = Observability.adapter_class(name).new(**)
171
+ yield adapter if block_given?
172
+ adapter.start!
173
+ replace_adapter(adapter)
174
+ adapter
175
+ end
176
+
177
+ def adapter(name)
178
+ adapters.find { |registered_adapter| registered_adapter.name == name.to_sym }
179
+ end
180
+
181
+ def enabled?(name = nil)
182
+ return adapters.any? unless name
183
+
184
+ !adapter(name).nil?
185
+ end
186
+
187
+ def reset!
188
+ adapters.each(&:stop!)
189
+ adapters.clear
190
+ end
191
+
192
+ def validate!
193
+ adapters.each(&:validate!)
194
+ end
195
+
196
+ private
197
+
198
+ def replace_adapter(adapter)
199
+ previous_adapter = self.adapter(adapter.name)
200
+ previous_adapter&.stop!
201
+ adapters.delete(previous_adapter)
202
+ adapters << adapter
203
+ end
204
+ end
205
+
206
+ class Adapter
207
+ attr_reader :name
208
+
209
+ def initialize(name)
210
+ @name = name.to_sym
211
+ @started = false
212
+ end
213
+
214
+ def start!
215
+ validate!
216
+ @started = true
217
+ self
218
+ end
219
+
220
+ def stop!
221
+ @started = false
222
+ self
223
+ end
224
+
225
+ def started?
226
+ @started
227
+ end
228
+
229
+ def validate!
230
+ validate_dependencies!
231
+ self
232
+ end
233
+
234
+ def validate_dependencies!
235
+ self
236
+ end
237
+
238
+ def record(_event_name, _payload)
239
+ nil
240
+ end
241
+
242
+ def instrument(_event_name, _payload)
243
+ yield
244
+ end
245
+
246
+ private
247
+
248
+ def require_dependency(gem_name, require_path, adapter_name)
249
+ require require_path
250
+ rescue LoadError => e
251
+ raise unless e.path == require_path || e.message.include?(require_path)
252
+
253
+ raise MissingDependency,
254
+ "#{adapter_name} observability requires the `#{gem_name}` gem. " \
255
+ "Add `gem \"#{gem_name}\"` and require the adapter before enabling it."
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end