docs

AdonisJS queue

How the AdonisJS agent traces @adonisjs/queue and @boringnode/queue jobs, the queue option, span linking and job sampling.

The AdonisJS agent traces jobs from @adonisjs/queue and the @boringnode/queue package it builds on. There is no separate Unhandled package to install: the queue ships its own OpenTelemetry instrumentation, and the agent loads it for you. Dispatches and executions then appear in jobs.

How it is loaded

When the agent starts, and the queue option is enabled, it imports @adonisjs/queue/otel, then falls back to @boringnode/queue/otel. It creates the QueueInstrumentation exported there with two settings:

  • messagingSystem: 'boringqueue', so every queue span carries messaging.system=boringqueue.
  • executionSpanLinkMode from your config, link by default.

If neither module can be resolved, the agent skips queue tracing without an error. Any other failure while importing the module is thrown. Queue tracing needs the agent's startup hook from @unhandled/agent-adonisjs/init, covered in the AdonisJS agent setup.

Configure

Queue tracing is on by default. Set queue in config/unhandled.ts to change it:

ts
import env from '#start/env'
import { defineConfig } from '@unhandled/agent-adonisjs'

export default defineConfig({
  dsn: env.get('UNHANDLED_DSN'),
  queue: { executionSpanLinkMode: 'parent' },
})
ValueEffect
omitted, true or { enabled: true }Traces dispatch and execution, executions linked to the dispatch
{ executionSpanLinkMode: 'link' }The execution starts its own trace with a link to the dispatch span
{ executionSpanLinkMode: 'parent' }The execution is a child of the dispatch span, in the same trace
false or { enabled: false }No queue spans

link is the default because a job can run long after the request that dispatched it, and retries add more executions. In parent mode every execution lands in the dispatching request's trace, however much later it runs. Use parent when jobs run within seconds and you want one trace end to end.

What Unhandled reads

The span names and attributes come from the queue package's instrumentation. Unhandled classifies any span with messaging.system as a job and reads the standard messaging attributes:

AttributeUse
messaging.systemboringqueue, set by the agent
messaging.operation.typesend or create for a dispatch, process for an execution
messaging.job.nameJob name, used for grouping, for example ProcessOrder
messaging.destination.nameQueue
messaging.message.idJob ID
messaging.message.retry.countAttempts so far
messaging.job.queue_time_msTime waiting in the queue

A PRODUCER span counts as a dispatch and a CONSUMER span as an execution. A job dispatched outside any request, for example by a scheduler, starts its trace with the PRODUCER span and shows a Queue entry point. The full list of job attributes is on semantic conventions.

Job sampling

The agent's sampling callback applies to queue jobs. It recognizes an execution from its CONSUMER span with messaging.system, and passes { kind: 'job', system, name, queue?, jobId?, attempt? } built from the attributes above, using the span name when messaging.job.name is missing.

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

export default defineConfig({
  dsn: env.get('UNHANDLED_DSN'),
  sampling: (job) => {
    if (job.name === 'RefreshExchangeRates') return Sample.never()
    if (job.name === 'SendNewsletter') return Sample.rate(0).keepErrors({ maxSpans: 500 })
    return Sample.default()
  },
})

Because the decision is made when the execution span starts, a dropped queue job still runs its instrumentation but none of its spans are exported. keepErrors() holds the job's spans until it ends and exports them only if something failed. See job sampling for every rule.

Worker commands

The agent traces Ace commands, but queue:work and queue:listen are excluded by default, along with make:* and generate:*. A worker runs for hours, so a span for the whole command would be useless; each job gets its own spans instead.