diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 510a58af..6d1cbf15 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -26,6 +26,7 @@ jobs: 'Instrumentation/PDO', 'Instrumentation/Symfony', 'Instrumentation/Laravel', + 'Propagation/TraceResponse', 'Symfony' ] exclude: diff --git a/.gitsplit.yml b/.gitsplit.yml index d8582b4c..7545c5e7 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -34,6 +34,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/context-swoole.git" - prefix: "src/AutoInstrumentationInstaller" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-instrumentation-installer.git" + - prefix: "src/Propagation/TraceResponse" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-propagator-traceresponse.git" # List of references to split (defined as regexp) origins: diff --git a/composer.json b/composer.json index 3b71e375..6f328c55 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "OpenTelemetry\\Contrib\\Symfony\\": "src/Symfony/src", "OpenTelemetry\\Contrib\\Instrumentation\\Psr15\\": "src/Instrumentation/Psr15/src", "OpenTelemetry\\Contrib\\Instrumentation\\Slim\\": "src/Instrumentation/Slim/src", - "OpenTelemetry\\Contrib\\Instrumentation\\Wordpress\\": "src/Instrumentation/Wordpress/src" + "OpenTelemetry\\Contrib\\Instrumentation\\Wordpress\\": "src/Instrumentation/Wordpress/src", + "OpenTelemetry\\Contrib\\Propagation\\TraceResponse\\": "src/Propagation/TraceResponse/src" } }, "config": { diff --git a/src/Propagation/TraceResponse/.php-cs-fixer.php b/src/Propagation/TraceResponse/.php-cs-fixer.php new file mode 100644 index 00000000..248b4b9a --- /dev/null +++ b/src/Propagation/TraceResponse/.php-cs-fixer.php @@ -0,0 +1,43 @@ +exclude('vendor') + ->exclude('var/cache') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +return $config->setRules([ + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'none'], + 'is_null' => true, + 'modernize_types_casting' => true, + 'ordered_imports' => true, + 'php_unit_construct' => true, + 'single_line_comment_style' => true, + 'yoda_style' => false, + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => true, + 'cast_spaces' => true, + 'declare_strict_types' => true, + 'function_typehint_space' => true, + 'include' => true, + 'lowercase_cast' => true, + 'new_with_braces' => true, + 'no_extra_blank_lines' => true, + 'no_leading_import_slash' => true, + 'echo_tag_syntax' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'phpdoc_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_types' => true, + 'short_scalar_cast' => true, + 'single_blank_line_before_namespace' => true, + 'single_quote' => true, + 'trailing_comma_in_multiline' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder($finder); + diff --git a/src/Propagation/TraceResponse/README.md b/src/Propagation/TraceResponse/README.md new file mode 100644 index 00000000..4e09a09a --- /dev/null +++ b/src/Propagation/TraceResponse/README.md @@ -0,0 +1,56 @@ +# OpenTelemetry TraceResponse Propagator + +**Note:** This package is experimental as `traceresponse` is currently an editors' draft. + +This package provides a [Trace Context HTTP Response Headers Format](https://w3c.github.io/trace-context/#trace-context-http-response-headers-format) +propagator to inject the current span context into Response datastructures. + +The main goal is to allow client-side technology (Real User Monitoring, HTTP Clients) to record +the server side context in order to allow referencing it. + +## Requirements + +* OpenTelemetry SDK and exporters (required to actually export traces) + +Optional: +* OpenTelemetry extension (Some instrumentations can automatically use the `TraceResponsePropagator`) + +## Usage + +Assuming there is an active `SpanContext`, you can inject it into your response as follows: + +```php +// your framework probably provides a datastructure to model HTTP responses +// and allows you to hook into the end of a request / listen to a matching event. +$response = new Response(); + +// get the current scope, bail out if none +$scope = Context::storage()->scope(); +if (null === $scope) { + return; +} + +// create a PropagationSetterInterface that knows how to inject response headers +$propagationSetter = new class implements OpenTelemetry\Context\Propagation\PropagationSetterInterface { + public function set(&$carrier, string $key, string $value) : void { + $carrier->headers->set($key, $value); + } +}; +$propagator = new TraceResponseProgator(); +$propagator->inject($response, $propagationSetter, $scope->context()); +``` + +## Installation via composer + +```bash +$ composer require open-telemetry/opentelemetry-propagation-traceresponse +``` + +## Installing dependencies and executing tests + +From TraceResponse subdirectory: + +```bash +$ composer install +$ ./vendor/bin/phpunit tests +``` diff --git a/src/Propagation/TraceResponse/composer.json b/src/Propagation/TraceResponse/composer.json new file mode 100644 index 00000000..54464e2a --- /dev/null +++ b/src/Propagation/TraceResponse/composer.json @@ -0,0 +1,39 @@ +{ + "name": "open-telemetry/opentelemetry-propagation-traceresponse", + "description": "OpenTelemetry traceresponse propagator.", + "keywords": ["opentelemetry", "otel", "open-telemetry", "propagator", "traceresponse"], + "type": "library", + "homepage": "https://opentelemetry.io/docs/php", + "readme": "./README.md", + "license": "Apache-2.0", + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "php": "^7.0|^8.0", + "open-telemetry/context": "^1.0" + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\Contrib\\Propagation\\TraceResponse\\": "src/" + } + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "phan/phan": "^5.0", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "psalm/plugin-phpunit": "^0.16", + "open-telemetry/sdk": "^1.0", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.0", + "symfony/http-client": "^5.4|^6.0", + "guzzlehttp/promises": "^1.5", + "php-http/message-factory": "^1.0", + "nyholm/psr7": "^1.5" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } + } +} diff --git a/src/Propagation/TraceResponse/phpstan.neon.dist b/src/Propagation/TraceResponse/phpstan.neon.dist new file mode 100644 index 00000000..ed94c13d --- /dev/null +++ b/src/Propagation/TraceResponse/phpstan.neon.dist @@ -0,0 +1,9 @@ +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon + +parameters: + tmpDir: var/cache/phpstan + level: 5 + paths: + - src + - tests diff --git a/src/Propagation/TraceResponse/phpunit.xml.dist b/src/Propagation/TraceResponse/phpunit.xml.dist new file mode 100644 index 00000000..44d976f6 --- /dev/null +++ b/src/Propagation/TraceResponse/phpunit.xml.dist @@ -0,0 +1,44 @@ + + + + + + + src + + + + + + + + + + + + + tests/Unit + + + + diff --git a/src/Propagation/TraceResponse/psalm.xml.dist b/src/Propagation/TraceResponse/psalm.xml.dist new file mode 100644 index 00000000..15571171 --- /dev/null +++ b/src/Propagation/TraceResponse/psalm.xml.dist @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/Propagation/TraceResponse/src/ResponsePropagator.php b/src/Propagation/TraceResponse/src/ResponsePropagator.php new file mode 100644 index 00000000..aedd8697 --- /dev/null +++ b/src/Propagation/TraceResponse/src/ResponsePropagator.php @@ -0,0 +1,24 @@ +getContext(); + + if (!$spanContext->isValid()) { + return; + } + + $traceId = $spanContext->getTraceId(); + $spanId = $spanContext->getSpanId(); + + $samplingFlag = $spanContext->isSampled() ? self::IS_SAMPLED : self::NOT_SAMPLED; + + $header = self::SUPPORTED_VERSION . '-' . $traceId . '-' . $spanId . '-' . $samplingFlag; + $setter->set($carrier, self::TRACERESPONSE, $header); + } +} diff --git a/src/Propagation/TraceResponse/tests/Unit/PropagatorTest.php b/src/Propagation/TraceResponse/tests/Unit/PropagatorTest.php new file mode 100644 index 00000000..6ec2337d --- /dev/null +++ b/src/Propagation/TraceResponse/tests/Unit/PropagatorTest.php @@ -0,0 +1,124 @@ +assertSame($propagator->fields(), [Propagator::TRACERESPONSE]); + } + + /** + * @test + * Injects with a valid traceId, spanId, and is sampled + * restore(string $traceId, string $spanId, bool $sampled = false, bool $isRemote = false, ?API\TraceState $traceState = null): SpanContext + */ + public function test_inject_valid_sampled_trace_id() + { + $carrier = []; + (new Propagator())->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create(self::TRACE_ID, self::SPAN_ID, SpanContextInterface::TRACE_FLAG_SAMPLED), + Context::getCurrent() + ) + ); + + $this->assertSame( + [Propagator::TRACERESPONSE => self::TRACERESPONSE_HEADER_SAMPLED], + $carrier + ); + } + + /** + * @test + * Injects with a valid traceId, spanId, and is not sampled + */ + public function test_inject_valid_not_sampled_trace_id() + { + $carrier = []; + (new Propagator())->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create(self::TRACE_ID, self::SPAN_ID, SpanContextInterface::TRACE_FLAG_DEFAULT), + Context::getCurrent() + ) + ); + + $this->assertSame( + [Propagator::TRACERESPONSE => self::TRACERESPONSE_HEADER_NOT_SAMPLED], + $carrier + ); + } + + /** + * @test + * Test inject with tracestate - note: tracestate is not a part of traceresponse + */ + public function test_inject_trace_id_with_trace_state() + { + $carrier = []; + (new Propagator())->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create(self::TRACE_ID, self::SPAN_ID, SpanContextInterface::TRACE_FLAG_SAMPLED, new TraceState('vendor1=opaqueValue1')), + Context::getCurrent() + ) + ); + + $this->assertSame( + [Propagator::TRACERESPONSE => self::TRACERESPONSE_HEADER_SAMPLED], + $carrier + ); + } + + /** + * @test + * Test with an invalid spanContext, should return null + */ + public function test_inject_trace_id_with_invalid_span_context() + { + $carrier = []; + (new Propagator())->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create(SpanContextValidator::INVALID_TRACE, SpanContextValidator::INVALID_SPAN, SpanContextInterface::TRACE_FLAG_SAMPLED, new TraceState('vendor1=opaqueValue1')), + Context::getCurrent() + ) + ); + + $this->assertEmpty($carrier); + } + + private function withSpanContext(SpanContextInterface $spanContext, ContextInterface $context): ContextInterface + { + return $context->withContextValue(Span::wrap($spanContext)); + } +}