Skip to content

Commit 53bf6b8

Browse files
committed
feat: trace queued job execution and propagate trace context via the payload
- QueueJobListener listens to JobProcessing / JobProcessed / JobExceptionOccurred. On JobProcessing it restores a TraceContext from the `laravel-trace` key in the job payload (when present), opens a `queue.job` span (SpanType::Job) with queue.connection / queue.name / queue.job attributes, and closes or fails it when the job finishes. - Context restored from the payload is a temporary execution boundary: it is cleared after both successful and failed processing so it does not leak into whatever runs next on the worker. The `queue.job` SpanScope still restores its own previous context normally; only the injected root context is torn down. - TraceContext gains toArray()/fromArray() for payload (de)serialization. - The service provider registers a Queue::createPayloadUsing() hook that writes the active context into every dispatched job's payload, binds QueueJobListener as a singleton, and wires the three queue events. - New `laravel-trace.queue.enabled` config key (default true), checked live like the database listener. Covered by QueueTracingTest (8 cases): span recording, failure, the enabled switch, no-op without an active trace, payload round-trip, and no context leak after success or failure.
1 parent ba52c96 commit 53bf6b8

7 files changed

Lines changed: 400 additions & 0 deletions

File tree

config/laravel-trace.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@
99
'enabled' => true,
1010
],
1111

12+
'queue' => [
13+
'enabled' => true,
14+
],
15+
1216
];

src/Context/TraceContext.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,30 @@ public function withSpan(SpanId $spanId): self
2121
spanId: $spanId,
2222
);
2323
}
24+
25+
/**
26+
* @return array{trace_id: string, span_id: string|null}
27+
*/
28+
public function toArray(): array
29+
{
30+
return [
31+
'trace_id' => (string) $this->traceId,
32+
'span_id' => $this->spanId !== null
33+
? (string) $this->spanId
34+
: null,
35+
];
36+
}
37+
38+
/**
39+
* @param array{trace_id: string, span_id: string|null} $context
40+
*/
41+
public static function fromArray(array $context): self
42+
{
43+
return new self(
44+
traceId: new TraceId($context['trace_id']),
45+
spanId: isset($context['span_id'])
46+
? new SpanId($context['span_id'])
47+
: null,
48+
);
49+
}
2450
}

src/LaravelTraceServiceProvider.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;
1010
use Illuminate\Contracts\Queue\Queue;
1111
use Illuminate\Database\Events\QueryExecuted;
12+
use Illuminate\Queue\Events\JobExceptionOccurred;
13+
use Illuminate\Queue\Events\JobProcessed;
14+
use Illuminate\Queue\Events\JobProcessing;
1215
use Illuminate\Support\Facades\Event;
16+
use Illuminate\Support\Facades\Queue as QueueFacade;
1317
use Illuminate\Support\ServiceProvider;
1418
use LaravelTrace\LaravelTrace\Console\Commands\LaravelTraceCommand;
1519
use LaravelTrace\LaravelTrace\Context\InMemoryTraceContextStore;
@@ -21,6 +25,7 @@
2125
use LaravelTrace\LaravelTrace\Tracing\EventListenerTracer;
2226
use LaravelTrace\LaravelTrace\Tracing\InMemorySpanRecorder;
2327
use LaravelTrace\LaravelTrace\Tracing\InMemoryTraceRecorder;
28+
use LaravelTrace\LaravelTrace\Tracing\QueueJobListener;
2429
use LaravelTrace\LaravelTrace\Tracing\Tracer;
2530
use LaravelTrace\LaravelTrace\Tracing\TracingEventDispatcher;
2631

@@ -73,6 +78,18 @@ function (Application $app): Tracer {
7378
},
7479
);
7580

81+
$this->app->singleton(EventListenerTracer::class);
82+
83+
$this->app->singleton(
84+
QueueJobListener::class,
85+
function (Application $app): QueueJobListener {
86+
return new QueueJobListener(
87+
tracer: $app->make(TracerContract::class),
88+
config: $app->make(ConfigRepository::class),
89+
);
90+
},
91+
);
92+
7693
$this->app->singleton(
7794
'events',
7895
function (Application $app): TracingEventDispatcher {
@@ -116,6 +133,22 @@ public function boot(): void
116133

117134
$this->loadTranslationsFrom(__DIR__.'/../lang', 'laravel-trace');
118135

136+
QueueFacade::createPayloadUsing(
137+
function (): array {
138+
$context = $this->app->make(
139+
TracerContract::class,
140+
)->context();
141+
142+
if ($context === null) {
143+
return [];
144+
}
145+
146+
return [
147+
'laravel-trace' => $context->toArray(),
148+
];
149+
},
150+
);
151+
119152
if (! $this->app->runningInConsole()) {
120153
return;
121154
}
@@ -124,6 +157,22 @@ public function boot(): void
124157
QueryExecuted::class,
125158
DatabaseQueryListener::class,
126159
);
160+
161+
Event::listen(
162+
JobProcessing::class,
163+
[QueueJobListener::class, 'handleProcessing'],
164+
);
165+
166+
Event::listen(
167+
JobProcessed::class,
168+
[QueueJobListener::class, 'handleProcessed'],
169+
);
170+
171+
Event::listen(
172+
JobExceptionOccurred::class,
173+
[QueueJobListener::class, 'handleException'],
174+
);
175+
127176
$this->publishes([
128177
__DIR__.'/../config/laravel-trace.php' => config_path('laravel-trace.php'),
129178
], ['laravel-trace', 'laravel-trace-config']);

src/Tracing/QueueJobListener.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaravelTrace\LaravelTrace\Tracing;
6+
7+
use Illuminate\Contracts\Config\Repository as ConfigRepository;
8+
use Illuminate\Queue\Events\JobExceptionOccurred;
9+
use Illuminate\Queue\Events\JobProcessed;
10+
use Illuminate\Queue\Events\JobProcessing;
11+
use LaravelTrace\LaravelTrace\Context\TraceContext;
12+
use LaravelTrace\LaravelTrace\Contracts\Tracer;
13+
use LaravelTrace\LaravelTrace\Span\SpanType;
14+
15+
final class QueueJobListener
16+
{
17+
/**
18+
* @var array<int, SpanScope>
19+
*/
20+
private array $scopes = [];
21+
22+
/**
23+
* @var array<int, bool>
24+
*/
25+
private array $injectedContexts = [];
26+
27+
public function __construct(
28+
private readonly Tracer $tracer,
29+
private readonly ConfigRepository $config,
30+
) {}
31+
32+
public function handleProcessed(JobProcessed $event): void
33+
{
34+
$key = $this->jobKey($event->job);
35+
36+
$scope = $this->scopes[$key] ?? null;
37+
38+
unset($this->scopes[$key]);
39+
40+
$scope?->close();
41+
42+
$this->clearInjectedContext($key);
43+
}
44+
45+
public function handleException(JobExceptionOccurred $event): void
46+
{
47+
$key = $this->jobKey($event->job);
48+
49+
$scope = $this->scopes[$key] ?? null;
50+
51+
unset($this->scopes[$key]);
52+
53+
$scope?->fail($event->exception);
54+
55+
$this->clearInjectedContext($key);
56+
}
57+
58+
private function clearInjectedContext(int $key): void
59+
{
60+
if (($this->injectedContexts[$key] ?? false) !== true) {
61+
return;
62+
}
63+
64+
$this->tracer->clearContext();
65+
66+
unset($this->injectedContexts[$key]);
67+
}
68+
69+
public function handleProcessing(JobProcessing $event): void
70+
{
71+
if (! $this->isEnabled()) {
72+
return;
73+
}
74+
75+
$key = $this->jobKey($event->job);
76+
77+
$context = $event->job->payload()['laravel-trace'] ?? null;
78+
79+
if ($context !== null) {
80+
$this->tracer->setContext(
81+
TraceContext::fromArray($context),
82+
);
83+
84+
$this->injectedContexts[$key] = true;
85+
}
86+
87+
if ($this->tracer->context() === null) {
88+
return;
89+
}
90+
91+
$this->scopes[$key] = $this->tracer->span(
92+
name: 'queue.job',
93+
type: SpanType::Job,
94+
attributes: [
95+
'queue.connection' => $event->connectionName,
96+
'queue.name' => $event->job->getQueue(),
97+
'queue.job' => $event->job->resolveName(),
98+
],
99+
);
100+
}
101+
102+
private function isEnabled(): bool
103+
{
104+
return (bool) $this->config->get(
105+
'laravel-trace.queue.enabled',
106+
true,
107+
);
108+
}
109+
110+
private function jobKey(object $job): int
111+
{
112+
return spl_object_id($job);
113+
}
114+
}

0 commit comments

Comments
 (0)