Skip to content

Observability

The span and events the queue worker emits per job — what you see at debug, info, and warn levels, and the boot lines that precede them.

Every queue event the framework emits targets nest_rs::queue. One target, every worker-side log filterable by RUST_LOG=nest_rs::queue=debug, one span wrapping each job so its events share the same fields. The service-level logs your #[process] method emits keep their own target (e.g. features::audio) — same convention as the rest of the framework.

TargetWhatLevel
nest_rs::queueBoot registration, per-job span, skipped/unversioned warnings, and the reason a job failed.debug on job start, warn on a retryable failure, error on a dead-letter.
nest_rs::operationOne queue.job line per attempt — identity, outcome, duration_ms. Shared with every other edge.info.
nest_rs::appBoot: attached module-contributed transport transport="RedisWorker". Emitted by the composition root, which attaches every transport — so the target is the app’s, not the queue’s.info.
features::<feature>Your service-level logs inside the #[process] method.Your call.
oxana::*The job runtime’s own lines: Job started and Job finished per job on oxana::executor, which restate queue.job; a job requeued from a dead replica on oxana::storage_internal, which nothing else reports. Below info, oxana logs whole job records, payload included — on the producer’s side at trace as well as the worker’s.info, warn on a transient Redis error.

Hot paths respect RUST_LOG=info: job started sits at debug, so a worker at info logs one queue.job line per attempt and, on failure, one event saying why — no per-poll noise.

The worker opens one span per job attempt, target nest_rs::queue, name process job. The handler runs inside it, so any tracing event your service emits — and the framework’s own start/end events — are tagged with the span’s fields:

FieldNotes
queueThe Redis queue name.
processorThe processor host name, e.g. AudioProcessor::transcode.
job_idThe job’s id in Redis — a UUID, the same across its retries.
attemptCounted by the worker: 1 on the first try, higher on retries.
trace_id / span_idThis job’s W3C trace context — always present.
parent_span_idThe span that enqueued the job: the HTTP request, the message, the tick.
continued_tracetrue when the envelope carried a traceparent, false when this job started a trace of its own.
actor_idWho the enqueue was served for. Absent when nobody was authenticated — a job pushed by a scheduled tick has none.
otel.kindconsumer — a job is work delivered to this process.

attempt distinguishes retries of the same job_id. The OTLP appender from nest-rs-opentelemetry exports these as span attributes.

The trace fields cross a process boundary: the producer seals them into the job envelope and the consumer continues from them — see Correlation.

job started at debug, then whatever the handler logs, then one queue.job line on nest_rs::operation — the same line every edge files for a unit of work, carrying the job’s identity, its outcome and its duration_ms:

Terminal window
DEBUG features::audio: transcoded file="01a0978b-938c-718c-bd77-ac49831a8510-e2e-multipart-87068-1789248902027799293.mp3" derived_key="transcoded/01a0978b-938c-718c-bd77-ac49831a8510-e2e-multipart-87068-1789248902027799293.mp3" byte_size=41 trace_id=01a0978b93aa746c8754d7c900ef3597 span_id=8a42eb4688f9bb3d
INFO nest_rs::operation: queue.job queue="audio" processor="AudioProcessor::transcode" job_id="ccb2f3ed-924c-49f5-9370-10970d083b29" attempt=1 outcome="ok" duration_ms=29.803 trace_id=01a0978b93aa746c8754d7c900ef3597 span_id=8a42eb4688f9bb3d

A failure adds one event on nest_rs::queue saying why — at warn when the retry budget will re-run it, at error when the job dead-letters — and the queue.job line reports outcome="error" (or outcome="panic") beside it. The two never restate each other: the detail carries the cause, the line carries the timing and the identity.

attempt is how you correlate a retry: the worker re-runs the same job_id with attempt=2, opening a fresh process job span and filing a fresh line — same trace_id, new span_id. See Retries and failure for the policy behind attempt.

Each active processor logs one attached line (from the transport) and one registered queue processor line carrying its retries budget. A #[process] method that compiled in but whose provider isn’t reachable from this app’s module tree is skipped with a warn:

Terminal window
INFO nest_rs::app: attached module-contributed transport transport="RedisWorker"
INFO nest_rs::queue: registered queue processor processor="AudioProcessor::transcode" queue="audio" retries=3
WARN nest_rs::queue: skipped #[process] method: no instance of the provider in
this app's container processor=ReportJobs::weekly queue=reports
origin="features::reports::queue::processor"
hint="nothing is registered under that exact type. Common causes: its module
is not imported by this app (import it, or delete the methods); it is bound
only as `dyn Trait`; it is imported through a `for_root`; it is registered by
hand under a key or a trait"

The skip line is warn — a linked-but-unimported handler is usually a wiring mistake worth surfacing, and it confirms a shared features crate isn’t accidentally activating handlers this binary doesn’t want. See Wiring the worker for the module-gating rule.

Terminal window
RUST_LOG=info,oxana::executor=warn,nest_rs::queue=debug nestrs run dev worker

info everywhere, debug on the queue path: adds the job started line per poll on top of the queue.job line and any failure detail, while the runtime’s own per-job lines stay quiet. Silence oxana::executor rather than oxana: the latter also hides the line saying a dead replica’s job was put back on its queue, and never lower oxana below info in production, where it logs payloads.

Terminal window
RUST_LOG=nest_rs::queue=warn nestrs run dev worker

Strictly failures, skips, and unversioned warnings — useful for tailing a healthy worker where the only thing you care about is “did anything go wrong.”