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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +130 -0
- data/LICENSE +21 -0
- data/README.md +198 -0
- data/activejob-temporal.gemspec +58 -0
- data/api/job_payload_schema.json +318 -0
- data/bin/temporal-worker +295 -0
- data/lib/activejob/temporal/active_job_handler_source.rb +84 -0
- data/lib/activejob/temporal/activities/aj_runner_activity.rb +454 -0
- data/lib/activejob/temporal/activities/best_effort_side_effects.rb +49 -0
- data/lib/activejob/temporal/activities/dependency_status_activity.rb +160 -0
- data/lib/activejob/temporal/activities/rate_limit_activity.rb +41 -0
- data/lib/activejob/temporal/adapter.rb +257 -0
- data/lib/activejob/temporal/audit_log.rb +118 -0
- data/lib/activejob/temporal/batch_enqueue_result.rb +110 -0
- data/lib/activejob/temporal/batch_enqueuer.rb +141 -0
- data/lib/activejob/temporal/bind_policy.rb +44 -0
- data/lib/activejob/temporal/cancel/batch_canceller.rb +154 -0
- data/lib/activejob/temporal/cancel/batch_summary.rb +45 -0
- data/lib/activejob/temporal/cancel.rb +236 -0
- data/lib/activejob/temporal/certificate_watcher.rb +76 -0
- data/lib/activejob/temporal/chain_options.rb +83 -0
- data/lib/activejob/temporal/child_workflow_options.rb +102 -0
- data/lib/activejob/temporal/client.rb +215 -0
- data/lib/activejob/temporal/conditional_enqueue.rb +56 -0
- data/lib/activejob/temporal/configurable.rb +55 -0
- data/lib/activejob/temporal/configuration.rb +981 -0
- data/lib/activejob/temporal/configured_job_compatibility.rb +44 -0
- data/lib/activejob/temporal/connection_worker_pool.rb +88 -0
- data/lib/activejob/temporal/dead_letter_payload_validation.rb +34 -0
- data/lib/activejob/temporal/dead_letter_queue.rb +163 -0
- data/lib/activejob/temporal/dependency_options.rb +134 -0
- data/lib/activejob/temporal/external_operation.rb +193 -0
- data/lib/activejob/temporal/health_check_server.rb +159 -0
- data/lib/activejob/temporal/http_line_reader.rb +36 -0
- data/lib/activejob/temporal/inspect.rb +184 -0
- data/lib/activejob/temporal/job_descriptor.rb +37 -0
- data/lib/activejob/temporal/job_payload_builder.rb +209 -0
- data/lib/activejob/temporal/job_payload_chain_builder.rb +106 -0
- data/lib/activejob/temporal/job_payload_child_workflows.rb +127 -0
- data/lib/activejob/temporal/job_payload_dependencies.rb +40 -0
- data/lib/activejob/temporal/job_payload_rate_limits.rb +53 -0
- data/lib/activejob/temporal/job_payload_workflow_interactions.rb +31 -0
- data/lib/activejob/temporal/job_tags.rb +40 -0
- data/lib/activejob/temporal/locales/en.yml +126 -0
- data/lib/activejob/temporal/logger.rb +214 -0
- data/lib/activejob/temporal/metrics_server.rb +150 -0
- data/lib/activejob/temporal/middleware/chain.rb +106 -0
- data/lib/activejob/temporal/middleware.rb +11 -0
- data/lib/activejob/temporal/observability/datadog.rb +167 -0
- data/lib/activejob/temporal/observability/opentelemetry.rb +107 -0
- data/lib/activejob/temporal/observability/prometheus.rb +271 -0
- data/lib/activejob/temporal/observability.rb +260 -0
- data/lib/activejob/temporal/payload.rb +415 -0
- data/lib/activejob/temporal/payload_encryption.rb +215 -0
- data/lib/activejob/temporal/payload_serializers/json.rb +23 -0
- data/lib/activejob/temporal/payload_serializers/marshal.rb +53 -0
- data/lib/activejob/temporal/payload_serializers/message_pack.rb +59 -0
- data/lib/activejob/temporal/payload_serializers.rb +37 -0
- data/lib/activejob/temporal/payload_storage.rb +103 -0
- data/lib/activejob/temporal/rails_environment_loader.rb +143 -0
- data/lib/activejob/temporal/rate_limit_options.rb +94 -0
- data/lib/activejob/temporal/rate_limiters/memory.rb +198 -0
- data/lib/activejob/temporal/reload_signal_queue.rb +40 -0
- data/lib/activejob/temporal/retry_handler_extractor.rb +361 -0
- data/lib/activejob/temporal/retry_mapper.rb +264 -0
- data/lib/activejob/temporal/schedulable.rb +60 -0
- data/lib/activejob/temporal/schedule.rb +181 -0
- data/lib/activejob/temporal/schedule_options.rb +105 -0
- data/lib/activejob/temporal/search_attributes.rb +173 -0
- data/lib/activejob/temporal/signal_query.rb +161 -0
- data/lib/activejob/temporal/signal_query_options.rb +106 -0
- data/lib/activejob/temporal/temporal_options.rb +114 -0
- data/lib/activejob/temporal/tls_file.rb +45 -0
- data/lib/activejob/temporal/transaction_safety.rb +39 -0
- data/lib/activejob/temporal/version.rb +7 -0
- data/lib/activejob/temporal/visibility_query.rb +13 -0
- data/lib/activejob/temporal/worker_client_reloader.rb +34 -0
- data/lib/activejob/temporal/worker_health.rb +117 -0
- data/lib/activejob/temporal/worker_pool.rb +408 -0
- data/lib/activejob/temporal/workflow_enqueuer.rb +271 -0
- data/lib/activejob/temporal/workflow_enqueuer_batch.rb +17 -0
- data/lib/activejob/temporal/workflow_id_builder.rb +155 -0
- data/lib/activejob/temporal/workflow_identity.rb +62 -0
- data/lib/activejob/temporal/workflows/aj_workflow.rb +282 -0
- data/lib/activejob/temporal/workflows/dead_letter_support.rb +134 -0
- data/lib/activejob/temporal/workflows/dead_letter_workflow.rb +114 -0
- data/lib/activejob/temporal/workflows/workflow_chaining.rb +194 -0
- data/lib/activejob/temporal/workflows/workflow_child_workflows.rb +140 -0
- data/lib/activejob/temporal/workflows/workflow_continue_as_new.rb +44 -0
- data/lib/activejob/temporal/workflows/workflow_dependencies.rb +115 -0
- data/lib/activejob/temporal/workflows/workflow_execution_steps.rb +22 -0
- data/lib/activejob/temporal/workflows/workflow_interactions.rb +215 -0
- data/lib/activejob/temporal/workflows/workflow_local_activities.rb +29 -0
- data/lib/activejob/temporal/workflows/workflow_nexus.rb +15 -0
- data/lib/activejob/temporal/workflows/workflow_versioning.rb +21 -0
- data/lib/activejob/temporal.rb +297 -0
- data/lib/activejob-temporal.rb +3 -0
- 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
|