docs

BullMQ

Trace BullMQ 5 queues, flows and workers with @unhandled/instrumentation-bullmq, including job sampling through the AdonisJS agent.

@unhandled/instrumentation-bullmq traces BullMQ 5: jobs added through queues and flow producers, and jobs run by workers. Dispatch and execution spans feed the jobs views. It needs bullmq 5.0.0 or later.

Install

Terminal
npm install @unhandled/instrumentation-bullmq

Register with the AdonisJS agent

Add the integration to config/unhandled.ts. Nothing is traced until it is listed.

ts
import env from '#start/env'
import { defineConfig, Sample } from '@unhandled/agent-adonisjs'
import { bullmq } from '@unhandled/instrumentation-bullmq/integration'

export default defineConfig({
  dsn: env.get('UNHANDLED_DSN'),
  integrations: [
    bullmq({
      useProducerSpanAsConsumerParent: true,
      sampling: (job) => (job.name === 'SyncInventory' ? Sample.rate(0.1) : Sample.default()),
    }),
  ],
})

By default the integration uses the agent's sampling callback for every worker execution. bullmq({ sampling }) uses a different callback for BullMQ jobs only, and bullmq({ enabled: false }) turns the integration off. The callback runs before the worker body, so a dropped job creates no spans at all. See job sampling for the rules.

Register standalone

Without the agent, create and enable the instrumentation before bullmq is imported:

ts
import { BullMQInstrumentation } from '@unhandled/instrumentation-bullmq'

new BullMQInstrumentation({ requireParentSpanForPublish: true }).enable()

Both require and import are hooked; ESM needs the import-in-the-middle loader registered before bullmq loads. When a bundler or load order defeats the hooks, patch the module yourself with instrumentation.patch(bullmq) and undo it with instrumentation.unpatch(bullmq). Standalone use does no job sampling.

Options

OptionDefaultEffect
emitCreateSpansForBulktrueOne Job.addJob span per job in Queue.addBulk.
emitCreateSpansForFlowtrueOne Job.addJob span per job in FlowProducer.add, and per flow in FlowProducer.addBulk.
requireParentSpanForPublishfalseProducer spans are created only when a span is already active.
useProducerSpanAsConsumerParentfalseThe worker span joins the producer's trace as its child instead of starting a new trace with a link.
enabledtrueIntegration only. false registers nothing.
samplingagent callbackIntegration only. Sampling callback for BullMQ jobs.

Spans

OperationKindName
Queue.addPRODUCERQueue.add {queue}
Queue.addBulkINTERNALQueue.addBulk {queue}
Each job of a bulk or flowPRODUCERJob.addJob {queue}
FlowProducer.addINTERNALFlowProducer.add {queue}
FlowProducer.addBulkINTERNALFlowProducer.addBulk
Worker job executionCONSUMERWorker.run {queue}

The producer's trace context is written to the job's opts.telemetry.metadata, the field BullMQ itself uses for propagation. Jobs added with telemetry.omitContext: true carry no context. By default the worker span starts a new trace with a link to the producer span; with useProducerSpanAsConsumerParent it becomes the producer span's child.

A failed execution records the exception and sets ERROR status. The rescheduling errors DelayedError, WaitingError, WaitingChildrenError and RateLimitError are not failures.

Attributes

Every span has messaging.system=bullmq and messaging.operation.type: send on Queue.add and the bulk and flow parents, create on Job.addJob, process on Worker.run. All spans except FlowProducer.addBulk carry messaging.destination.name.

AttributeSpans
messaging.job.name, messaging.bullmq.job.nameSingle-job spans
messaging.message.idSingle-job spans
messaging.bullmq.job.timestamp, .delay, .attempts, .repeatJobKeySingle-job spans
messaging.batch.message_count, messaging.bullmq.job.bulk.namesBulk parents
messaging.message.retry.count, messaging.job.queue_time_msWorker.run
messaging.bullmq.job.processedOn, .finishedOn, .failedReasonWorker.run; failedReason only after a failure
messaging.bullmq.worker.concurrency, .lockDuration, .lockRenewTime, .rateLimiter.max, .rateLimiter.durationWorker.run, when set on the worker

messaging.job.queue_time_ms is processedOn - timestamp - delay, so a delayed job's wait does not count as queue time.

Events on Worker.run

EventAttributes
bullmq.job.lock_extendedmessaging.message.id, messaging.bullmq.lock.duration
bullmq.job.removedmessaging.message.id
bullmq.job.retrymessaging.message.id; for automatic retries also messaging.message.retry.count and messaging.bullmq.retry.delay

The attribute and event names are exported as BULLMQ_ATTR and BULLMQ_EVENTS.