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/docker-compose.yaml b/docker-compose.yaml
index b7dc2965..c4d7c810 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,6 +1,7 @@
version: '3.7'
services:
php:
+ image: ghcr.io/open-telemetry/opentelemetry-php/opentelemetry-php-base:${PHP_VERSION:-7.4}
build:
context: ./docker
dockerfile: Dockerfile
diff --git a/src/Instrumentation/Laravel/src/LaravelInstrumentation.php b/src/Instrumentation/Laravel/src/LaravelInstrumentation.php
index 492efd78..eb4c35d6 100644
--- a/src/Instrumentation/Laravel/src/LaravelInstrumentation.php
+++ b/src/Instrumentation/Laravel/src/LaravelInstrumentation.php
@@ -7,7 +7,6 @@
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Http\Kernel;
use Illuminate\Http\Request;
-use Illuminate\Http\Response;
use Illuminate\Support\ServiceProvider;
use OpenTelemetry\API\Common\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Common\Instrumentation\Globals;
@@ -18,6 +17,7 @@
use OpenTelemetry\Context\Context;
use function OpenTelemetry\Instrumentation\hook;
use OpenTelemetry\SemConv\TraceAttributes;
+use Symfony\Component\HttpFoundation\Response;
use Throwable;
class LaravelInstrumentation
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));
+ }
+}