• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FriendsOfOpenTelemetry / opentelemetry-bundle / 25035624412

28 Apr 2026 05:27AM UTC coverage: 92.409% (+0.7%) from 91.751%
25035624412

push

github

web-flow
feat(Instrumentation/Messenger): implement worker and middleware instrumentation (#173)

* chore(messenger): add trace parent in dispatched messages (AMQP)

* chore(messenger): use Messenger events to start/end span for instrumentation

* remove strict type, merge start/end span into 1 subscriber, close span on error and on message handled

* propagation not related to AMQP + read incoming trace (async context)

* rename event subscriber

* chore(messenger): use Messenger events to start/end span for instrumentation

* chore(messenger): add trace parent in dispatched messages (AMQP)

* remove strict type, merge start/end span into 1 subscriber, close span on error and on message handled

* propagation not related to AMQP + read incoming trace (async context)

* rename event subscriber

* fix(messenger): clean up worker subscriber and add functional tests

Fix several issues in the WorkerMessageEventSubscriber introduced by PR #173:
- Replace SDK Span import with API Span to respect API/SDK separation
- Implement InstrumentationTypeInterface for consistency with other subscribers
- Add event priorities (10000/-10000) to wrap all other processing
- Add messaging semantic convention attributes (operation.type, destination.name)
- Include message class name in span name for better trace readability
- Remove stale imports and duplicate propagation middleware service definition
- Clean up propagation middleware when messenger tracing is disabled
- Add PHPStan baseline entries for untyped $carrier interface params
- Add functional tests for worker message handled, failed, and attribute mode
- Reorganize messenger tests into Messenger/ subdirectory
- Disable retry on test transport to isolate worker span assertions

* test(messenger): add tests for transport tracing and propagation middleware

Cover TraceableMessengerTransport (get/ack/reject spans + TransportException
error recording) and AddStampForPropagationMiddleware (stamp skip, no-scope
passthrough, ac... (continued)

187 of 191 new or added lines in 14 files covered. (97.91%)

12 existing lines in 10 files now uncovered.

2459 of 2661 relevant lines covered (92.41%)

15.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.59
/src/Instrumentation/Symfony/Console/TraceableConsoleEventSubscriber.php
1
<?php
2

3
namespace FriendsOfOpenTelemetry\OpenTelemetryBundle\Instrumentation\Symfony\Console;
4

5
use FriendsOfOpenTelemetry\OpenTelemetryBundle\Instrumentation\Attribute\Traceable;
6
use FriendsOfOpenTelemetry\OpenTelemetryBundle\Instrumentation\InstrumentationTypeEnum;
7
use FriendsOfOpenTelemetry\OpenTelemetryBundle\Instrumentation\InstrumentationTypeInterface;
8
use FriendsOfOpenTelemetry\OpenTelemetryBundle\OpenTelemetry\Context\Attribute\ConsoleTraceAttributeEnum;
9
use OpenTelemetry\API\Trace\Span;
10
use OpenTelemetry\API\Trace\StatusCode;
11
use OpenTelemetry\API\Trace\TracerInterface;
12
use OpenTelemetry\Context\Context;
13
use OpenTelemetry\SemConv\Attributes\CodeAttributes;
14
use Psr\Log\LoggerInterface;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\ConsoleEvents;
17
use Symfony\Component\Console\Event\ConsoleCommandEvent;
18
use Symfony\Component\Console\Event\ConsoleErrorEvent;
19
use Symfony\Component\Console\Event\ConsoleSignalEvent;
20
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
21
use Symfony\Component\DependencyInjection\ServiceLocator;
22
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
23
use Symfony\Contracts\Service\ServiceSubscriberInterface;
24

25
final class TraceableConsoleEventSubscriber implements EventSubscriberInterface, ServiceSubscriberInterface, InstrumentationTypeInterface
26
{
27
    private InstrumentationTypeEnum $instrumentationType = InstrumentationTypeEnum::Auto;
28
    /**
29
     * @var string[]
30
     */
31
    private array $excludeCommands = [];
32

33
    public function __construct(
34
        private readonly TracerInterface $tracer,
35
        /** @var ServiceLocator<TracerInterface> */
36
        private readonly ServiceLocator $tracerLocator,
37
        private readonly ?LoggerInterface $logger = null,
38
    ) {
39
    }
8✔
40

41
    public static function getSubscribedEvents(): array
42
    {
43
        return [
7✔
44
            ConsoleEvents::COMMAND => [
7✔
45
                ['startSpan', 10000],
7✔
46
            ],
7✔
47
            ConsoleEvents::ERROR => [
7✔
48
                ['handleError', -10000],
7✔
49
            ],
7✔
50
            ConsoleEvents::TERMINATE => [
7✔
51
                ['endSpan', -10000],
7✔
52
            ],
7✔
53
            ConsoleEvents::SIGNAL => [
7✔
54
                ['handleSignal', -10000],
7✔
55
            ],
7✔
56
        ];
7✔
57
    }
58

59
    /**
60
     * @return class-string[]
61
     */
62
    public static function getSubscribedServices(): array
63
    {
64
        return [TracerInterface::class];
7✔
65
    }
66

67
    public function startSpan(ConsoleCommandEvent $event): void
68
    {
69
        $command = $event->getCommand();
8✔
70

UNCOV
71
        assert($command instanceof Command);
×
72

73
        if (false === $this->isAutoTraceable($command) && false === $this->isAttributeTraceable($command)) {
8✔
74
            return;
1✔
75
        }
76

77
        $tracer = $this->getTracer($command);
7✔
78

79
        $name = $command->getName();
7✔
80
        $class = get_class($command);
7✔
81

82
        $spanBuilder = $tracer
7✔
83
            ->spanBuilder($name)
7✔
84
            ->setAttributes([
7✔
85
                CodeAttributes::CODE_FUNCTION_NAME => $class.'::execute',
7✔
86
            ]);
7✔
87

88
        $parent = Context::getCurrent();
7✔
89

90
        $span = $spanBuilder->setParent($parent)->startSpan();
7✔
91

92
        $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId()));
7✔
93

94
        Context::storage()->attach($span->storeInContext($parent));
7✔
95

96
        $this->logger?->debug(sprintf('Activating new scope "%s"', spl_object_id(Context::storage()->scope())));
7✔
97
    }
98

99
    public function handleError(ConsoleErrorEvent $event): void
100
    {
101
        $span = Span::getCurrent();
1✔
102
        $span->setStatus(StatusCode::STATUS_ERROR);
1✔
103
        $span->recordException($event->getError(), [
1✔
104
            ConsoleTraceAttributeEnum::ExitCode->toString() => $event->getExitCode(),
1✔
105
        ]);
1✔
106
    }
107

108
    public function endSpan(ConsoleTerminateEvent $event): void
109
    {
110
        $scope = Context::storage()->scope();
8✔
111
        if (null === $scope) {
8✔
112
            $this->logger?->debug('No active scope');
×
113

114
            return;
×
115
        }
116
        $this->logger?->debug(sprintf('Detaching scope "%s"', spl_object_id($scope)));
8✔
117
        $scope->detach();
8✔
118

119
        $span = Span::fromContext($scope->context());
8✔
120
        $span->setAttribute(
8✔
121
            ConsoleTraceAttributeEnum::ExitCode->value,
8✔
122
            $event->getExitCode()
8✔
123
        );
8✔
124

125
        $statusCode = match ($event->getExitCode()) {
8✔
126
            Command::SUCCESS => StatusCode::STATUS_OK,
6✔
127
            default => StatusCode::STATUS_ERROR,
2✔
128
        };
8✔
129
        $span->setStatus($statusCode);
8✔
130

131
        $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId()));
8✔
132
        $span->end();
8✔
133
    }
134

135
    public function handleSignal(ConsoleSignalEvent $event): void
136
    {
137
        $span = Span::getCurrent();
×
138
        $span->setAttribute(ConsoleTraceAttributeEnum::SignalCode->toString(), $event->getHandlingSignal());
×
139
    }
140

141
    private function parseAttribute(Command $command): ?Traceable
142
    {
143
        $reflection = new \ReflectionClass($command);
8✔
144
        $attribute = $reflection->getAttributes(Traceable::class)[0] ?? null;
8✔
145

146
        return $attribute?->newInstance();
8✔
147
    }
148

149
    private function getTracer(Command $command): TracerInterface
150
    {
151
        $traceable = $this->parseAttribute($command);
7✔
152

153
        if (null !== $traceable?->tracer) {
7✔
154
            return $this->tracerLocator->get($traceable->tracer);
1✔
155
        }
156

157
        return $this->tracer;
6✔
158
    }
159

160
    private function isAutoTraceable(Command $command): bool
161
    {
162
        if (InstrumentationTypeEnum::Auto !== $this->instrumentationType) {
8✔
163
            return false;
4✔
164
        }
165

166
        if (0 === count($this->excludeCommands)) {
4✔
167
            return true;
1✔
168
        }
169

170
        $combinedExcludeCommands = implode('|', $this->excludeCommands);
3✔
171
        if (preg_match("#{$combinedExcludeCommands}#", $command->getName())) {
3✔
172
            return false;
1✔
173
        }
174

175
        return true;
2✔
176
    }
177

178
    private function isAttributeTraceable(Command $command): bool
179
    {
180
        $traceable = $this->parseAttribute($command);
5✔
181

182
        return InstrumentationTypeEnum::Attribute === $this->instrumentationType
5✔
183
            && true === $traceable instanceof Traceable;
5✔
184
    }
185

186
    public function setInstrumentationType(InstrumentationTypeEnum $type): void
187
    {
188
        $this->instrumentationType = $type;
8✔
189
    }
190

191
    /**
192
     * @param string[] $excludeCommands
193
     */
194
    public function setExcludeCommands(array $excludeCommands): void
195
    {
196
        $this->excludeCommands = $excludeCommands;
7✔
197
    }
198
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc