Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Instrumentation/Symfony/_register.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

declare(strict_types=1);

use OpenTelemetry\Contrib\Instrumentation\Symfony\ConsoleInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Symfony\HttpClientInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Symfony\MessengerInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Symfony\SymfonyInstrumentation;
Expand All @@ -20,3 +21,4 @@
SymfonyInstrumentation::register();
MessengerInstrumentation::register();
HttpClientInstrumentation::register();
ConsoleInstrumentation::register();
3 changes: 2 additions & 1 deletion src/Instrumentation/Symfony/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"phpunit/phpunit": "^9.5",
"vimeo/psalm": "6.4.0",
"symfony/http-client": "^5.4||^6.0",
"symfony/messenger": "^5.4||^6.0"
"symfony/messenger": "^5.4||^6.0",
"symfony/console": "^5.4||^6.0"
},
"autoload": {
"psr-4": {
Expand Down
95 changes: 95 additions & 0 deletions src/Instrumentation/Symfony/src/ConsoleInstrumentation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Symfony;

use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use function OpenTelemetry\Instrumentation\hook;
use OpenTelemetry\SemConv\TraceAttributes;
use OpenTelemetry\SemConv\Version;
use Symfony\Component\Console\Command\Command;

/**
* Creates a default root span for console command execution, mirroring the
* root span created by the HttpKernel instrumentation for HTTP requests.
*/
final class ConsoleInstrumentation
{
const ATTRIBUTE_CONSOLE_EXIT_CODE = 'symfony.console.exit_code';

/** @psalm-suppress PossiblyUnusedMethod */
public static function register(): void
{
$instrumentation = new CachedInstrumentation(
'io.opentelemetry.contrib.php.symfony_console',
null,
Version::VERSION_1_32_0->url(),
);

/** @psalm-suppress UnusedFunctionCall */
hook(
Command::class,
'run',
Comment on lines +36 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Capture the final console exit code

When a Symfony app has a ConsoleEvents::TERMINATE listener that changes the exit code, this span is already finishing in the Command::run() post hook, before Symfony dispatches that terminate event. Symfony documents that terminate listeners run after the command and may change the exit code, so a command returning 0 can later exit non-zero while this instrumentation exports symfony.console.exit_code=0 and leaves the span status unset; hook/defer to the application-level final exit code instead.

Useful? React with 👍 / 👎.

pre: static function (
Command $command,
array $params,
string $class,
string $function,
?string $filename,
?int $lineno,
) use ($instrumentation): array {
$name = $command->getName() ?? 'command';

/** @psalm-suppress ArgumentTypeCoercion */
$builder = $instrumentation
->tracer()
->spanBuilder($name)
->setSpanKind(SpanKind::KIND_INTERNAL)
->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, sprintf('%s::%s', $class, $function))
->setAttribute(TraceAttributes::CODE_FILE_PATH, $filename)
->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno);

$parent = Context::getCurrent();
$span = $builder
->setParent($parent)
->startSpan();

Context::storage()->attach($span->storeInContext($parent));

return $params;
},
post: static function (
Command $command,
array $params,
?int $returnValue,
?\Throwable $exception
): void {
$scope = Context::storage()->scope();
if (null === $scope) {
return;
}

$scope->detach();
$span = Span::fromContext($scope->context());

if (null !== $exception) {
$span->recordException($exception);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
} elseif (null !== $returnValue && $returnValue !== Command::SUCCESS) {
$span->setStatus(StatusCode::STATUS_ERROR);
}

if (null !== $returnValue) {
$span->setAttribute(self::ATTRIBUTE_CONSOLE_EXIT_CODE, $returnValue);
}

$span->end();
}
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Tests\Instrumentation\Symfony\tests\Integration;

use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Contrib\Instrumentation\Symfony\ConsoleInstrumentation;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;

final class ConsoleInstrumentationTest extends AbstractTest
{
public function test_command_run_creates_root_span(): void
{
$command = new class() extends Command {
protected static $defaultName = 'app:test-command';

protected function configure(): void
{
$this->setName('app:test-command');
}

protected function execute($input, $output): int
{
return Command::SUCCESS;
}
};

$this->assertCount(0, $this->storage);

$exitCode = $command->run(new ArrayInput([]), new NullOutput());

$this->assertSame(Command::SUCCESS, $exitCode);
$this->assertCount(1, $this->storage);

$span = $this->storage[0];
$this->assertSame('app:test-command', $span->getName());
$this->assertSame(SpanKind::KIND_INTERNAL, $span->getKind());
$this->assertSame(Command::SUCCESS, $span->getAttributes()->get(ConsoleInstrumentation::ATTRIBUTE_CONSOLE_EXIT_CODE));
$this->assertNotSame(StatusCode::STATUS_ERROR, $span->getStatus()->getCode());
}

public function test_command_run_with_non_zero_exit_code_is_error(): void
{
$command = new class() extends Command {
protected static $defaultName = 'app:failing-command';

protected function configure(): void
{
$this->setName('app:failing-command');
}

protected function execute($input, $output): int
{
return Command::FAILURE;
}
};

$exitCode = $command->run(new ArrayInput([]), new NullOutput());

$this->assertSame(Command::FAILURE, $exitCode);
$this->assertCount(1, $this->storage);

$span = $this->storage[0];
$this->assertSame(StatusCode::STATUS_ERROR, $span->getStatus()->getCode());
$this->assertSame(Command::FAILURE, $span->getAttributes()->get(ConsoleInstrumentation::ATTRIBUTE_CONSOLE_EXIT_CODE));
}

public function test_command_run_records_exception(): void
{
$command = new class() extends Command {
protected static $defaultName = 'app:throwing-command';

protected function configure(): void
{
$this->setName('app:throwing-command');
}

protected function execute($input, $output): int
{
throw new \RuntimeException('something went wrong');
}
};

try {
$command->run(new ArrayInput([]), new NullOutput());
$this->fail('Expected exception was not thrown');
} catch (\RuntimeException $e) {
$this->assertSame('something went wrong', $e->getMessage());
}

$this->assertCount(1, $this->storage);
$span = $this->storage[0];
$this->assertSame(StatusCode::STATUS_ERROR, $span->getStatus()->getCode());
$this->assertSame('something went wrong', $span->getStatus()->getDescription());
}
}
Loading