Module: Appsignal::Hooks::ActiveJobHook::ActiveJobTraceContext

Defined in:
lib/appsignal/hooks/active_job.rb

Overview

Reads and writes W3C trace context on the ActiveJob enqueue/perform boundary, wire-compatible with OpenTelemetry's ActiveJob instrumentation. All of this no-ops outside collector mode.

Context rides on the job under __otel_headers, the same carrier OTel uses. Stock serialize/deserialize only carry a fixed key set, so -- like OTel -- we patch both plus an accessor to round-trip it. The on-wire value is run through ActiveJob's argument serializer (an array of [key, value] pairs), matching OTel byte-for-byte so an AppSignal- and an OTel-instrumented service read each other's jobs.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#__otel_headersObject



247
248
249
# File 'lib/appsignal/hooks/active_job.rb', line 247

def __otel_headers
  @__otel_headers ||= {}
end

Instance Method Details

#deserialize(job_data) ⇒ Object



240
241
242
243
244
245
# File 'lib/appsignal/hooks/active_job.rb', line 240

def deserialize(job_data)
  super
  serialized = job_data["__otel_headers"]
  @__otel_headers =
    serialized ? ::ActiveJob::Arguments.deserialize(serialized).to_h : {}
end

#enqueueObject

Inject on enqueue from inside a producer event, so the job carries this transaction's context and the perform later links back. Mirrors the Sidekiq client middleware: an AppSignal event (a producer span in collector mode), not a direct SDK span. Appsignal.instrument is a transparent pass-through when there's no active transaction, and inject_context no-ops outside collector mode.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/appsignal/hooks/active_job.rb', line 192

def enqueue(*, **)
  # When enqueue instrumentation is disabled, drop the trace context
  # along with the event. Without an enqueue event there is no producer
  # span, so the context we would write is that of whatever span is
  # current, such as the surrounding web request. The job that performs
  # later would then link back to a span that is not a producer.
  return super if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]

  # Another enqueue integration is already recording this enqueue, so
  # don't record it a second time.
  if Appsignal::Transaction.current? &&
      Appsignal::Transaction.current.job_enqueue_events_suppressed?
    return super
  end

  Appsignal.instrument(
    "enqueue.active_job",
    "enqueue #{self.class.name} job",
    :opentelemetry_kind => :producer,
    :opentelemetry_scope => ["appsignal-ruby/active_job", Appsignal::VERSION]
  ) do
    Appsignal::Transaction.current.add_opentelemetry_attributes(
      Appsignal::OpenTelemetry::Messaging
        .enqueue_attributes("active_job", :destination => queue_name)
    )
    Appsignal::OpenTelemetry.inject_context(__otel_headers)
    # Active Job enqueues through an adapter (Sidekiq, Resque, ...) that
    # has its own enqueue instrumentation. Suppress it so the enqueue is
    # recorded once, as this event, rather than as nested Active Job +
    # adapter events.
    if Appsignal::Transaction.current?
      Appsignal::Transaction.current.suppress_job_enqueue_events { super }
    else
      super
    end
  end
end

#serializeObject



230
231
232
233
234
235
236
237
238
# File 'lib/appsignal/hooks/active_job.rb', line 230

def serialize
  super.tap do |data|
    Appsignal::OpenTelemetry.if_started do
      next if __otel_headers.empty?

      data["__otel_headers"] = ::ActiveJob::Arguments.serialize(__otel_headers)
    end
  end
end