<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aryan</title>
    <description>The latest articles on DEV Community by Aryan (@aryanba50cdaec38a2).</description>
    <link>https://dev.to/aryanba50cdaec38a2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036178%2Fa43be184-cb5b-426e-9080-9a919d44073f.png</url>
      <title>DEV Community: Aryan</title>
      <link>https://dev.to/aryanba50cdaec38a2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aryanba50cdaec38a2"/>
    <language>en</language>
    <item>
      <title>The Job Finished. The Trace Didn’t.</title>
      <dc:creator>Aryan</dc:creator>
      <pubDate>Sun, 19 Jul 2026 07:18:39 +0000</pubDate>
      <link>https://dev.to/aryanba50cdaec38a2/the-job-finished-the-trace-didnt-509i</link>
      <guid>https://dev.to/aryanba50cdaec38a2/the-job-finished-the-trace-didnt-509i</guid>
      <description>&lt;p&gt;&lt;em&gt;How I repaired broken causality across FastAPI, Redis Streams, retries, and a background worker with OpenTelemetry and SigNoz&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The API returned &lt;code&gt;202 Accepted&lt;/code&gt;. The worker finished the job. Redis had nothing left to process. Then I searched for &lt;code&gt;job-before-001&lt;/code&gt; in SigNoz and found two unrelated trace IDs.&lt;/p&gt;

&lt;p&gt;Nothing in the application had visibly failed. The FastAPI service accepted the synthetic job, the Redis Stream delivered it, and the worker ran its steps. But the trace could not explain how the worker got there. The API span ended after publishing to Redis; the worker appeared in a new root trace.&lt;/p&gt;

&lt;p&gt;That was the point of this local experiment: a system can look healthy while its causal story is broken.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1u8sum07jeaqam7pdx8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1u8sum07jeaqam7pdx8.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One &lt;code&gt;app.job_id&lt;/code&gt; returned API and worker spans, but the trace IDs differed. The Redis boundary split one logical operation into separate traces.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The small system I used to reproduce it
&lt;/h2&gt;

&lt;p&gt;I kept the setup deliberately small: a client calls &lt;code&gt;POST /agent-jobs&lt;/code&gt;, FastAPI publishes a message to the &lt;code&gt;agent-jobs&lt;/code&gt; Redis Stream, and a background worker consumes it. Inside the worker, I created &lt;code&gt;agent.plan&lt;/code&gt;, &lt;code&gt;tool.retrieve&lt;/code&gt;, &lt;code&gt;agent.evaluate&lt;/code&gt;, and &lt;code&gt;result.persist&lt;/code&gt; spans. The services are named &lt;code&gt;async-agent-api&lt;/code&gt; and &lt;code&gt;async-agent-worker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All job IDs, payloads, and failures were synthetic. I did not need a large workload to see the problem; one queue boundary and a few child spans were enough. The project also includes &lt;code&gt;casting.yaml&lt;/code&gt; and &lt;code&gt;casting.yaml.lock&lt;/code&gt; so the local SigNoz setup is reproducible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4qdrri437pn1m7k31qn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4qdrri437pn1m7k31qn.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Conceptual diagram, not telemetry evidence: the same workflow before and after W3C trace context crosses the Redis message boundary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The broken version
&lt;/h2&gt;

&lt;p&gt;The broken experiment was intentional. I started the API and worker with &lt;code&gt;PROPAGATE_TRACE=false&lt;/code&gt; and sent &lt;code&gt;job-before-001&lt;/code&gt; using the tested command below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROPAGATE_TRACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false &lt;/span&gt;&lt;span class="nv"&gt;APP_EXPERIMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;before docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--force-recreate&lt;/span&gt; async-agent-api async-agent-worker
python scripts/send_jobs.py &lt;span class="nt"&gt;--job-id&lt;/span&gt; job-before-001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer still created a &lt;code&gt;redis.publish&lt;/code&gt; span and wrote a normal Redis Stream message. The key difference was that its &lt;code&gt;trace_context&lt;/code&gt; field stayed empty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;propagate_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;experiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the consumer side, an empty carrier meant &lt;code&gt;parent_context&lt;/code&gt; was &lt;code&gt;None&lt;/code&gt;. Starting &lt;code&gt;redis.consume&lt;/code&gt; with that context made it a new root. The useful &lt;code&gt;app.job_id&lt;/code&gt; attribute still linked the records semantically, which is why the SigNoz filter &lt;code&gt;app.job_id = 'job-before-001'&lt;/code&gt; found both sides. It did not connect them causally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that was missing
&lt;/h2&gt;

&lt;p&gt;Redis is a manual propagation boundary in this setup. FastAPI instrumentation can create the server-side HTTP span, but a custom message body does not automatically know which field should carry the W3C context. &lt;a href="https://opentelemetry.io/docs/specs/otel/context/api-propagators/" rel="noopener noreferrer"&gt;OpenTelemetry’s propagation model&lt;/a&gt; calls that message field a carrier: a mutable string map that is written on the sending side and read on the receiving side.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Telemetry can be complete and still be causally wrong.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The repair was simple in concept: inject the current context, transport the carrier inside the Redis message, then extract it before the worker creates its consumer span. The exact placement mattered more than the number of lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-part fix: inject, transport, extract
&lt;/h2&gt;

&lt;p&gt;For the repaired run, I recreated the services with &lt;code&gt;PROPAGATE_TRACE=true&lt;/code&gt; and sent &lt;code&gt;job-after-001&lt;/code&gt;. The API injects while the producer span is current, then serializes the carrier because Redis Stream fields must be strings or bytes in this client setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis.publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workflow_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PRODUCER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;propagate_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;experiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker does the inverse before it creates &lt;code&gt;redis.consume&lt;/code&gt;. It also records whether a carrier existed and keeps the same &lt;code&gt;app.job_id&lt;/code&gt; on every span.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;context_present&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;parent_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context_present&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis.consume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;parent_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CONSUMER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attributes&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nf"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_present&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messaging.system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messaging.destination.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows the &lt;a href="https://opentelemetry.io/docs/languages/python/propagation/" rel="noopener noreferrer"&gt;OpenTelemetry Python propagation guidance&lt;/a&gt;: deserialize the carrier, extract a context, and start the next span with it. After that change, &lt;code&gt;POST /agent-jobs&lt;/code&gt;, &lt;code&gt;redis.publish&lt;/code&gt;, &lt;code&gt;redis.consume&lt;/code&gt;, &lt;code&gt;agent.plan&lt;/code&gt;, &lt;code&gt;tool.retrieve&lt;/code&gt;, &lt;code&gt;agent.evaluate&lt;/code&gt;, and &lt;code&gt;result.persist&lt;/code&gt; appeared in one connected trace for the repaired job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzj21d4xc4z1cxtrbrzjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzj21d4xc4z1cxtrbrzjx.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After injection in the API and extraction in the worker, the repaired job appears as one connected API-to-worker trace.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Retries were a second propagation boundary
&lt;/h2&gt;

&lt;p&gt;Fixing the first publish was not enough. A retry can accidentally create a fresh carrier and quietly split the workflow again. I tested this with &lt;code&gt;job-after-retry-001&lt;/code&gt; and the tested script flag &lt;code&gt;--fail-first-attempt&lt;/code&gt;. Attempt one raises the controlled &lt;code&gt;RuntimeError("controlled retrieval failure")&lt;/code&gt; inside &lt;code&gt;tool.retrieve&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The worker records a &lt;code&gt;retry.schedule&lt;/code&gt; span, preserves the existing carrier, changes only &lt;code&gt;attempt&lt;/code&gt; from &lt;code&gt;"1"&lt;/code&gt; to &lt;code&gt;"2"&lt;/code&gt;, and sends the retry back to &lt;code&gt;agent-jobs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;retry_job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;retries_total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.experiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;experiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second attempt completed in the local test. There is no artificial backoff in this code, so I do not claim one. The useful result is narrower: the retry reused the original propagated context instead of losing it at the second queue publish.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzhatpubbo81tkpk4hwsj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzhatpubbo81tkpk4hwsj.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A controlled retrieval failure triggers &lt;code&gt;retry.schedule&lt;/code&gt;; the retry preserves the carried context and completes on attempt two.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Logs finally told the same story
&lt;/h2&gt;

&lt;p&gt;I also wanted logs to be navigable with the trace, not merely searchable by job ID. &lt;code&gt;emit_log()&lt;/code&gt; creates an OTLP log record using &lt;code&gt;get_current()&lt;/code&gt; while the active span is still in scope. The API emits &lt;code&gt;Agent job accepted&lt;/code&gt;; the worker emits &lt;code&gt;Worker job started&lt;/code&gt;, &lt;code&gt;Agent job scheduled for retry&lt;/code&gt; when applicable, and &lt;code&gt;Agent job completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Logs Explorer screenshot shows API and worker messages for the same synthetic job. The code intentionally sends no full message payloads, passwords, or API keys to the log attributes. SigNoz documents the same general correlation model: log records with trace context can be opened from Logs Explorer and navigated back to their corresponding trace (&lt;a href="https://signoz.io/docs/traces-management/guides/correlate-traces-and-logs/" rel="noopener noreferrer"&gt;trace-log correlation&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ykq4e1up1vdk1l09zpi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ykq4e1up1vdk1l09zpi.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;API and worker log messages for the repaired workflow, filtered by the shared job attribute.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Turning trace integrity into a signal
&lt;/h2&gt;

&lt;p&gt;Finding one split job was satisfying, but I wanted a way to notice the pattern again. The worker emits three counters: &lt;code&gt;async_jobs_total&lt;/code&gt;, &lt;code&gt;async_jobs_context_missing_total&lt;/code&gt;, and &lt;code&gt;async_jobs_retried_total&lt;/code&gt;. I made the &lt;strong&gt;Async Agent Trace Integrity&lt;/strong&gt; dashboard in SigNoz from those real metrics.&lt;/p&gt;

&lt;p&gt;In the captured 30-minute dashboard view, the panels show &lt;strong&gt;Total Jobs: 2&lt;/strong&gt;, &lt;strong&gt;Missing Context: 0&lt;/strong&gt;, and &lt;strong&gt;Retry Count: 0&lt;/strong&gt;. Those values are only a snapshot of the selected local time range, not a throughput claim. The panel construction used SigNoz Query Builder’s metric aggregation options; SigNoz documents that the same visual builder is available in dashboards and supports filtering and aggregation (&lt;a href="https://signoz.io/docs/userguide/query-builder-v5/" rel="noopener noreferrer"&gt;Query Builder&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I did not configure or test a trace-integrity alert in this experiment, so I am not presenting an alert as evidence. The next useful step would be an alert on nonzero missing-context jobs after deciding what volume and sampling policy make sense.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bz57m68y0lyek5cucls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bz57m68y0lyek5cucls.png" alt=" " width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The local dashboard snapshot tracks total jobs, jobs missing propagated context, and retries for the selected 30-minute window.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The main lesson was not that adding telemetry is enough. Telemetry presence and telemetry correctness are different things. A stable job ID helped me locate the split, but it could not repair causality. The repair came from treating both the async handoff and the retry handoff as boundaries that needed deliberate context handling.&lt;/p&gt;

&lt;p&gt;I also learned to emit correlation-sensitive logs while a span is active, and to start with one workflow I could understand end to end. This remains a local synthetic experiment with limited traffic; it does not establish production-scale behavior, sampling policy, or baggage governance. Next time, I would add an automated integration check that asserts the API and worker share a trace for a known job.&lt;/p&gt;

&lt;p&gt;Before this experiment, I treated a completed worker job as proof that the workflow was healthy. Now I also check whether the trace can explain how that job got there.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
