Skip to content

OpenTelemetry

NexJob emits distributed traces and metrics out of the box using the standard .NET ActivitySource and Meter APIs. The NexJob.OpenTelemetry package provides opt-in extension methods to wire those signals into the OpenTelemetry SDK, so you can export them to Jaeger, Zipkin, Prometheus, Azure Application Insights, or any OTLP-compatible backend without writing any custom instrumentation.


Installation

dotnet add package NexJob.OpenTelemetry

Registering Instrumentation

Call AddNexJobInstrumentation() inside both WithTracing and WithMetrics in your Program.cs:

using NexJob.OpenTelemetry;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddNexJobInstrumentation()        // registers NexJob spans
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter())
    .WithMetrics(metrics => metrics
        .AddNexJobInstrumentation()        // registers NexJob counters and histograms
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());

You can combine any exporters supported by the OpenTelemetry .NET SDK — OTLP, Jaeger, Zipkin, Prometheus, Application Insights, CloudWatch, and more. NexJob uses the ActivitySource named "NexJob" and the Meter named "NexJob".


Available Spans

NexJob produces three span types during normal operation.

nexjob.enqueue — Producer

Fired whenever a job enters storage via EnqueueAsync, ScheduleAsync, ScheduleAtAsync, or ContinueWithAsync.

Attribute Description
nexjob.job_type Assembly-qualified name of the job type.
nexjob.queue Target queue name.
nexjob.job_id Unique identifier of the job.
nexjob.delay_seconds (scheduled jobs) Seconds until the job becomes eligible.
nexjob.scheduled_at (scheduled jobs) ISO 8601 timestamp of the scheduled execution time.
nexjob.parent_job_id (continuations) ID of the parent job.

nexjob.execute — Consumer

Fired for each job execution attempt. Its parent span is the trace context stored on the job at enqueue time, enabling end-to-end traces from an HTTP request all the way through job completion.

Attribute Description
nexjob.job_type Job type name.
nexjob.queue Queue name.
nexjob.job_id Job ID.
nexjob.attempt Current attempt number (1-based).
nexjob.foreign_job true when the job type belongs to another service and was deferred.
nexjob.interrupted true when the job was interrupted by a host shutdown and requeued.

The span status is set to Ok when the job succeeds, or Error (with an attached exception event) when it fails.

nexjob.recurring.register

Fired when a recurring job is registered via RecurringAsync.

Attribute Description
nexjob.job_type Job type name.
nexjob.recurring_job_id The recurring job ID.
nexjob.queue Target queue name.
nexjob.cron Cron expression used to schedule executions.
nexjob.next_execution ISO 8601 timestamp of the next scheduled execution.

Trace Propagation

When you call EnqueueAsync, NexJob captures the current W3C traceparent from the ambient Activity and stores it on the job record. When the dispatcher executes the job it restores that context and creates a child span, so the full trace — from the originating HTTP request through the background execution — is available in your APM tool.

The same propagation applies to broker triggers (Kafka, RabbitMQ, Azure Service Bus, AWS SQS, Google Pub/Sub). Each trigger extracts traceparent from the broker message headers or attributes before enqueuing the job, connecting the producing service's trace to the NexJob execution span.

HTTP Request (span A)
  └─ nexjob.enqueue (span B, child of A)
       └─ nexjob.execute (span C, child of B — runs asynchronously on a worker)

Available Metrics

NexJob exposes the following metrics under the "NexJob" meter:

Metric Type Description
nexjob.jobs.enqueued Counter Total jobs enqueued. Tagged with nexjob.job_type and nexjob.queue.
nexjob.jobs.succeeded Counter Total jobs that completed successfully. Tagged with nexjob.job_type.
nexjob.jobs.failed Counter Failed execution attempts — one increment per failed attempt, including attempts that will be retried. Tagged with nexjob.job_type.
nexjob.jobs.expired Counter Total jobs that exceeded their deadline and were expired. Tagged with nexjob.job_type.
nexjob.dead_letter.forwarded Counter Dead-lettered jobs that an IDeadLetterForwarder handed over to its destination. Tagged with nexjob.forwarder. See dead-letter forwarding.
nexjob.dead_letter.forward_failed Counter Forwards that failed; the exception is logged and swallowed, so this counter is the only signal. Tagged with nexjob.forwarder.
nexjob.jobs.cancellation_ignored Counter Jobs still running 10 seconds after their execution timeout cancelled their token, so they still hold a worker slot. Tagged with nexjob.job_type.
nexjob.jobs.throttle_deferred Counter Jobs returned to the queue because a slot of their [Throttle] resource was not free. Tagged with nexjob.job_type and nexjob.resource.
nexjob.recurring.id_collisions Counter Recurring job registrations that overwrote a job of a different type stored under the same id (two applications sharing a database). Tagged with recurring_job_id. See recurring job ids.
nexjob.job.duration Histogram Job execution time in milliseconds. Tagged with nexjob.job_type and nexjob.status.
nexjob.queue.depth ObservableGauge Current number of enqueued jobs waiting per queue. Tagged with nexjob.queue.
nexjob.workers.active ObservableGauge Number of workers currently executing jobs on this node.
nexjob.workers.total ObservableGauge Total number of worker slots configured on this node (0 on a host with Workers = 0).

The nexjob.queue tag carries the stored queue name. Since v6.0 the default queue of an application is {prefix}.default, so alerts and dashboards that filter on default need the new name (see the v6.0.0 migration).


Kubernetes HPA with Queue Depth

The nexjob.queue.depth gauge lets you autoscale worker pods directly from queue backlog using Prometheus and KEDA:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: nexjob-worker-scaler
spec:
  scaleTargetRef:
    name: nexjob-worker-deployment
  minReplicaCount: 2
  maxReplicaCount: 20
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://prometheus-k8s.monitoring.svc:9090
        metricName: nexjob_queue_depth
        query: sum(nexjob_queue_depth{nexjob_queue="critical"})
        threshold: '50'

KEDA scales the deployment up when the critical queue depth exceeds 50 waiting jobs, and back down when it drains.


Structured Logging — Ambient Job Context

NexJob automatically enriches every log entry emitted during job execution using ILogger.BeginScope — no additional packages required. This works with the built-in .NET logging pipeline, Serilog, NLog, and the OpenTelemetry logging bridge.

Scope keys

Key Type Description
NexJob.JobId Guid Unique job identifier.
NexJob.JobType string Assembly-qualified job type name.
NexJob.Queue string Target queue name.
NexJob.Attempt int Current attempt number (1-based).
NexJob.TraceParent string W3C traceparent header (empty string if not propagated).

Every log line emitted inside IJob.ExecuteAsync — or internally by the job executor — automatically inherits all five keys as structured fields.

Example structured log output

{
  "Timestamp": "2026-09-26T08:45:10.123Z",
  "Level": "Information",
  "Message": "Payment approved for Order #45210",
  "NexJob.JobId": "8e3b1c77-4a11-4e92-91cd-32aa812f8112",
  "NexJob.JobType": "Acme.Billing.ProcessPaymentJob",
  "NexJob.Queue": "payments",
  "NexJob.Attempt": 1,
  "NexJob.TraceParent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}

Enabling scopes with the built-in JSON console logger

{
  "Logging": {
    "Console": {
      "FormatterName": "json",
      "FormatterOptions": {
        "IncludeScopes": true
      }
    }
  }
}

Enabling scopes with Serilog

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()        // captures NexJob scope keys
    .WriteTo.Console(new JsonFormatter())
    .WriteTo.Seq("http://localhost:5341")
    .CreateLogger();

Querying structured logs

index=prod NexJob.JobId="8e3b1c77-4a11-4e92-91cd-32aa812f8112"
index=prod NexJob.Queue="payments" NexJob.Attempt>1 | stats count by NexJob.JobType
{app="my-worker"} | json | NexJob_Queue="payments" | line_format "{{.NexJob_JobId}} {{.Message}}"
{app="my-worker"} | json | NexJob_Attempt > 1 | count_over_time[5m]
@NexJob.Queue:payments @NexJob.Attempt:>1

Tip

Combine structured log correlation with distributed traces: filter by NexJob.TraceParent to jump from a Loki or Splunk log line directly to the corresponding Tempo or Jaeger span.


Compatible Exporters

NexJob.OpenTelemetry works with any OpenTelemetry exporter:

  • OTLP — Collector, Honeycomb, Lightstep, and any OTLP-compatible backend
  • Jaeger and Zipkin
  • Prometheus (scrape endpoint via AddPrometheusExporter)
  • Azure Application Insights
  • AWS CloudWatch
  • Google Cloud Monitoring

Tip

To turn these metrics into alerts (jobs that exhausted their attempts, expired jobs, a queue nobody drains), see the Alerts guide.

Try it

The NexJob.Sample.Storage sample exports NexJob traces and metrics with OpenTelemetry next to a read replica and a distributed throttle.