diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 9be661d00..2c1b19f45 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -92,6 +92,7 @@ updates:
- "/src/Instrumentation/Yii"
- "/src/Logs/Monolog"
- "/src/MetaPackages/opentelemetry"
+ - "/src/Propagation/CloudTrace"
- "/src/Propagation/Instana"
- "/src/Propagation/ServerTiming"
- "/src/Propagation/TraceResponse"
diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index 022d20add..e833c354f 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -54,6 +54,7 @@ jobs:
'Instrumentation/Symfony',
'Instrumentation/Yii',
'Logs/Monolog',
+ 'Propagation/CloudTrace',
'Propagation/Instana',
'Propagation/ServerTiming',
'Propagation/TraceResponse',
diff --git a/.gitsplit.yml b/.gitsplit.yml
index 66cac9025..7830c9b64 100644
--- a/.gitsplit.yml
+++ b/.gitsplit.yml
@@ -72,6 +72,8 @@ splits:
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-logger-monolog.git"
- prefix: "src/MetaPackages/opentelemetry"
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/opentelemetry-meta.git"
+ - prefix: "src/Propagation/CloudTrace"
+ target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-propagator-cloudtrace.git"
- prefix: "src/Propagation/Instana"
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-propagator-instana.git"
- prefix: "src/Propagation/ServerTiming"
diff --git a/composer.json b/composer.json
index 56945bd4b..3c64f2e40 100644
--- a/composer.json
+++ b/composer.json
@@ -45,6 +45,7 @@
"OpenTelemetry\\Contrib\\Instrumentation\\Wordpress\\": "src/Instrumentation/Wordpress/src",
"OpenTelemetry\\Contrib\\Instrumentation\\Yii\\": "src/Instrumentation/Yii/src",
"OpenTelemetry\\Contrib\\Logs\\Monolog\\": "src/Logs/Monolog/src",
+ "OpenTelemetry\\Contrib\\Propagation\\CloudTrace\\": "src/Propagation/CloudTrace/src",
"OpenTelemetry\\Contrib\\Propagation\\Instana\\": "src/Propagation/Instana/src",
"OpenTelemetry\\Contrib\\Propagation\\ServerTiming\\": "src/Propagation/ServerTiming/src",
"OpenTelemetry\\Contrib\\Propagation\\TraceResponse\\": "src/Propagation/TraceResponse/src",
@@ -84,6 +85,7 @@
"src/Instrumentation/Symfony/_register.php",
"src/Instrumentation/Wordpress/_register.php",
"src/Instrumentation/Yii/_register.php",
+ "src/Propagation/CloudTrace/_register.php",
"src/Propagation/Instana/_register.php",
"src/ResourceDetectors/Azure/_register.php",
"src/ResourceDetectors/Container/_register.php",
@@ -117,6 +119,7 @@
"OpenTelemetry\\Tests\\Instrumentation\\Symfony\\tests\\": "src/Instrumentation/Symfony/tests",
"OpenTelemetry\\Tests\\Instrumentation\\Wordpress\\": "src/Instrumentation/Wordpress/tests",
"OpenTelemetry\\Tests\\Instrumentation\\Yii\\": "src/Instrumentation/Yii/tests",
+ "OpenTelemetry\\Tests\\Propagation\\CloudTrace\\": "src/Propagation/CloudTrace/tests",
"OpenTelemetry\\Tests\\Resource\\Detector\\Azure\\": "src/ResourceDetectors/Azure/tests",
"OpenTelemetry\\Contrib\\Resource\\Detector\\DigitalOcean\\": "src/ResourceDetectors/DigitalOcean/tests",
"OpenTelemetry\\Tests\\Contrib\\Symfony\\": "src/Symfony/tests",
@@ -160,6 +163,7 @@
"open-telemetry/opentelemetry-exporter-instana": "self.version",
"open-telemetry/opentelemetry-instrumentation-installer": "self.version",
"open-telemetry/opentelemetry-logger-monolog": "self.version",
+ "open-telemetry/opentelemetry-propagation-cloudtrace": "self.version",
"open-telemetry/opentelemetry-propagation-instana": "self.version",
"open-telemetry/opentelemetry-propagation-server-timing": "self.version",
"open-telemetry/opentelemetry-propagation-traceresponse": "self.version",
diff --git a/src/Propagation/CloudTrace/.gitattributes b/src/Propagation/CloudTrace/.gitattributes
new file mode 100644
index 000000000..1676cf825
--- /dev/null
+++ b/src/Propagation/CloudTrace/.gitattributes
@@ -0,0 +1,12 @@
+* text=auto
+
+*.md diff=markdown
+*.php diff=php
+
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.php-cs-fixer.php export-ignore
+/phpstan.neon.dist export-ignore
+/phpunit.xml.dist export-ignore
+/psalm.xml.dist export-ignore
+/tests export-ignore
diff --git a/src/Propagation/CloudTrace/.gitignore b/src/Propagation/CloudTrace/.gitignore
new file mode 100644
index 000000000..57872d0f1
--- /dev/null
+++ b/src/Propagation/CloudTrace/.gitignore
@@ -0,0 +1 @@
+/vendor/
diff --git a/src/Propagation/CloudTrace/.php-cs-fixer.php b/src/Propagation/CloudTrace/.php-cs-fixer.php
new file mode 100644
index 000000000..e35fa078c
--- /dev/null
+++ b/src/Propagation/CloudTrace/.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,
+ 'type_declaration_spaces' => true,
+ 'include' => true,
+ 'lowercase_cast' => true,
+ 'new_with_parentheses' => 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,
+ 'blank_lines_before_namespace' => true,
+ 'single_quote' => true,
+ 'trailing_comma_in_multiline' => true,
+ ])
+ ->setRiskyAllowed(true)
+ ->setFinder($finder);
+
diff --git a/src/Propagation/CloudTrace/README.md b/src/Propagation/CloudTrace/README.md
new file mode 100644
index 000000000..f1e048aba
--- /dev/null
+++ b/src/Propagation/CloudTrace/README.md
@@ -0,0 +1,35 @@
+[](https://github.com/opentelemetry-php/extension-propagator-cloudtrace/releases)
+[](https://github.com/open-telemetry/opentelemetry-php/issues)
+[](https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Propagation/CloudTrace)
+[](https://github.com/opentelemetry-php/extension-propagator-cloudtrace)
+[](https://packagist.org/packages/open-telemetry/extension-propagator-cloudtrace/)
+[](https://packagist.org/packages/open-telemetry/extension-propagator-cloudtrace/)
+
+This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib.
+
+# OpenTelemetry CloudTrace Propagator
+
+CloudTrace is a propagator that supports the specification for the header "x-cloud-trace-context" used for trace context propagation across
+service boundaries. (https://cloud.google.com/trace/docs/setup#force-trace). OpenTelemetry PHP CloudTrace Propagator Extension provides
+option to use it bi-directionally or one-way. One-way does not inject the header for downstream consumption, it only processes the incoming headers
+and returns the correct span context. It only attaches to existing X-Cloud-Trace-Context traces and does not create downstream ones.
+
+## Installation
+
+```sh
+composer require open-telemetry/extension-propagator-cloudtrace
+```
+
+## Usage
+
+For one-way CloudTrace:
+
+```
+$propagator = CloudTracePropagator::getOneWayInstance();
+```
+
+For bi-directional CloudTrace:
+
+```
+$propagator = CloudTracePropagator::getInstance();
+```
diff --git a/src/Propagation/CloudTrace/_register.php b/src/Propagation/CloudTrace/_register.php
new file mode 100644
index 000000000..5244f8fea
--- /dev/null
+++ b/src/Propagation/CloudTrace/_register.php
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/Unit
+
+
+
+
diff --git a/src/Propagation/CloudTrace/psalm.xml.dist b/src/Propagation/CloudTrace/psalm.xml.dist
new file mode 100644
index 000000000..155711712
--- /dev/null
+++ b/src/Propagation/CloudTrace/psalm.xml.dist
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Propagation/CloudTrace/src/CloudTraceFormatter.php b/src/Propagation/CloudTrace/src/CloudTraceFormatter.php
new file mode 100644
index 000000000..816ab9ad3
--- /dev/null
+++ b/src/Propagation/CloudTrace/src/CloudTraceFormatter.php
@@ -0,0 +1,57 @@
+[/][;o=]`.
+ * The options are a bitmask of options. Currently the only option is the
+ * least significant bit which signals whether the request was traced or not
+ * (1 = traced, 0 = not traced).
+ */
+final class CloudTraceFormatter
+{
+ const CONTEXT_HEADER_FORMAT = '/([0-9a-fA-F]{32})(?:\/(\d+))?(?:;o=(\d+))?/';
+
+ /**
+ * Generate a SpanContext object from the Trace Context header
+ */
+ public static function deserialize(string $header) : SpanContextInterface
+ {
+ $matched = preg_match(self::CONTEXT_HEADER_FORMAT, $header, $matches);
+
+ if (!$matched) {
+ return SpanContext::getInvalid();
+ }
+ if (!array_key_exists(2, $matches) || empty($matches[2])) {
+ return SpanContext::getInvalid();
+ }
+ if (!array_key_exists(3, $matches)) {
+ return SpanContext::getInvalid();
+ }
+
+ return SpanContext::createFromRemoteParent(
+ strtolower($matches[1]),
+ Utils::leftZeroPad(Utils::decToHex($matches[2])),
+ (int) ($matches[3] === '1')
+ );
+ }
+
+ /**
+ * Convert a SpanContextInterface to header string
+ */
+ public static function serialize(SpanContextInterface $context) : string
+ {
+ $ret = $context->getTraceId();
+ if ($context->getSpanId()) {
+ $ret .= '/' . Utils::hexToDec($context->getSpanId());
+ }
+
+ return $ret . (';o=' . ($context->isSampled() ? '1' : '0'));
+ }
+}
diff --git a/src/Propagation/CloudTrace/src/CloudTracePropagator.php b/src/Propagation/CloudTrace/src/CloudTracePropagator.php
new file mode 100644
index 000000000..90e56ee52
--- /dev/null
+++ b/src/Propagation/CloudTrace/src/CloudTracePropagator.php
@@ -0,0 +1,99 @@
+oneWay) {
+ return;
+ }
+
+ $setter ??= ArrayAccessGetterSetter::getInstance();
+ $context ??= Context::getCurrent();
+ $spanContext = Span::fromContext($context)->getContext();
+
+ if (!$spanContext->isValid()) {
+ return;
+ }
+
+ $headerValue = CloudTraceFormatter::serialize($spanContext);
+ $setter->set($carrier, self::XCLOUD, $headerValue);
+ }
+
+ /** {@inheritdoc} */
+ #[\Override]
+ public function extract($carrier, ?PropagationGetterInterface $getter = null, ?ContextInterface $context = null): ContextInterface
+ {
+ $getter ??= ArrayAccessGetterSetter::getInstance();
+ $context ??= Context::getCurrent();
+
+ $headerValue = $getter->get($carrier, self::XCLOUD);
+ if ($headerValue === null) {
+ return $context;
+ }
+
+ $spanContext = CloudTraceFormatter::deserialize($headerValue);
+ if (!$spanContext->isValid()) {
+ return $context;
+ }
+
+ return $context->withContextValue(Span::wrap($spanContext));
+ }
+}
diff --git a/src/Propagation/CloudTrace/src/Utils.php b/src/Propagation/CloudTrace/src/Utils.php
new file mode 100644
index 000000000..700bf7095
--- /dev/null
+++ b/src/Propagation/CloudTrace/src/Utils.php
@@ -0,0 +1,122 @@
+= and no >, because this function is used in context of what method to use
+ * to convert to some base (in our case hex to octal and vice versa).
+ * So it's ok if we use >=, because it means that only for MAX_INT we will use the slower baseConvert
+ * method.
+ *
+ * @param int|float $number The number to test.
+ * @return bool Whether it was bigger or not than the max.
+ */
+ public static function isBigNum($number) : bool
+ {
+ return $number >= PHP_INT_MAX;
+ }
+
+ /**
+ * Custom function to convert a number in string format from one base to another.
+ * Built-in functions, specifically for hex, do not work well in PHP under
+ * all versions (32/64-bit) or if the number only fits into an unsigned long.
+ * PHP does not have unsigned longs, so this function is necessary.
+ *
+ * @param string $num The number to convert (in some base).
+ * @param int $fromBase The base to convert from.
+ * @param int $toBase The base to convert to.
+ * @return string Converted number in the new base.
+ */
+ public static function baseConvert(string $num, int $fromBase, int $toBase) : string
+ {
+ $num = strtolower($num);
+ $chars = '0123456789abcdefghijklmnopqrstuvwxyz';
+ $newstring = substr($chars, 0, $toBase);
+
+ $length = strlen($num);
+ $result = '';
+
+ $number = [];
+ for ($i = 0; $i < $length; $i++) {
+ $number[$i] = strpos($chars, $num[$i]);
+ }
+
+ do {
+ $divide = 0;
+ $newlen = 0;
+ for ($i = 0; $i < $length; $i++) {
+ if (!isset($number[$i]) || $number[$i] === false) {
+ return '';
+ }
+ $divide = $divide * $fromBase + $number[$i];
+ if ($divide >= $toBase) {
+ $number[$newlen++] = (int) ($divide / $toBase);
+ $divide %= $toBase;
+ } elseif ($newlen > 0) {
+ $number[$newlen++] = 0;
+ }
+ }
+ $length = $newlen;
+ $result = $newstring[$divide] . $result;
+ } while ($newlen != 0);
+
+ return $result;
+ }
+}
diff --git a/src/Propagation/CloudTrace/tests/Unit/CloudTraceFormatterTest.php b/src/Propagation/CloudTrace/tests/Unit/CloudTraceFormatterTest.php
new file mode 100644
index 000000000..0a7af692e
--- /dev/null
+++ b/src/Propagation/CloudTrace/tests/Unit/CloudTraceFormatterTest.php
@@ -0,0 +1,51 @@
+assertEquals($result->getTraceId(), $traceId, "Given deserialize($header), traceId != $traceId (result={$result->getTraceId()}");
+ $this->assertEquals($result->getSpanId(), $spanId, "Given deserialize($header), spanId != $spanId (result={$result->getSpanId()}");
+ $this->assertEquals($result->getTraceFlags(), $sample, "Given deserialize($header), traceFlags != $sample (result={$result->getTraceFlags()}");
+ }
+
+ public static function for_test_deserialize() : array
+ {
+ return [
+ ['00000000000000000000000000000001/1;o=0', '00000000000000000000000000000001', '0000000000000001', 0],
+ ['10000000000000000000000000000001/10;o=1', '10000000000000000000000000000001', '000000000000000a', 1],
+ ];
+ }
+
+ /**
+ * @dataProvider for_test_serialize
+ */
+ public function test_serialize(SpanContextInterface $span, string $header) : void
+ {
+ $result = CloudTraceFormatter::serialize($span);
+ $this->assertEquals($result, $header, "Given serialize(header), result != $header (result=$result");
+ }
+
+ public static function for_test_serialize() : array
+ {
+ return [
+ [SpanContext::createFromRemoteParent('00000000000000000000000000000001', '0000000000000001', TraceFlags::DEFAULT), '00000000000000000000000000000001/1;o=0'],
+ [SpanContext::createFromRemoteParent('00000000000000000000000000000001', '000000000000000a', TraceFlags::SAMPLED), '00000000000000000000000000000001/10;o=1'],
+ ];
+ }
+}
diff --git a/src/Propagation/CloudTrace/tests/Unit/CloudTracePropagatorOneWayTest.php b/src/Propagation/CloudTrace/tests/Unit/CloudTracePropagatorOneWayTest.php
new file mode 100644
index 000000000..0282a4524
--- /dev/null
+++ b/src/Propagation/CloudTrace/tests/Unit/CloudTracePropagatorOneWayTest.php
@@ -0,0 +1,177 @@
+cloudTracePropagator = CloudTracePropagator::getOneWayInstance();
+ [$this->xcloud] = $this->cloudTracePropagator->fields();
+ }
+
+ public function test_fields(): void
+ {
+ $this->assertSame(
+ ['x-cloud-trace-context'],
+ $this->cloudTracePropagator->fields()
+ );
+ }
+
+ public function test_inject_invalid_context(): void
+ {
+ $carrier = [];
+ $this
+ ->cloudTracePropagator
+ ->inject(
+ $carrier,
+ null,
+ $this->withSpanContext(
+ SpanContext::create(
+ SpanContextValidator::INVALID_TRACE,
+ SpanContextValidator::INVALID_SPAN,
+ TraceFlags::SAMPLED
+ ),
+ Context::getCurrent()
+ )
+ );
+ $this->assertEmpty($carrier);
+ }
+
+ public function test_inject_sampled_context(): void
+ {
+ $carrier = [];
+ $this
+ ->cloudTracePropagator
+ ->inject(
+ $carrier,
+ null,
+ $this->withSpanContext(
+ SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, TraceFlags::SAMPLED),
+ Context::getCurrent()
+ )
+ );
+ $this->assertEmpty($carrier);
+ }
+
+ public function test_inject_non_sampled_context(): void
+ {
+ $carrier = [];
+ $this
+ ->cloudTracePropagator
+ ->inject(
+ $carrier,
+ null,
+ $this->withSpanContext(
+ SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, TraceFlags::DEFAULT),
+ Context::getCurrent()
+ )
+ );
+ $this->assertEmpty($carrier);
+ }
+
+ public function test_inject_non_sampled_default_context(): void
+ {
+ $carrier = [];
+ $this
+ ->cloudTracePropagator
+ ->inject(
+ $carrier,
+ null,
+ $this->withSpanContext(
+ SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16),
+ Context::getCurrent()
+ )
+ );
+ $this->assertEmpty($carrier);
+ }
+
+ public function test_extract_nothing(): void
+ {
+ $this->assertSame(
+ Context::getCurrent(),
+ $this->cloudTracePropagator->extract([])
+ );
+ }
+
+ public function test_extract_sampled_context(): void
+ {
+ $carrier = [
+ $this->xcloud => $this->generateTraceIdHeaderValue(
+ self::TRACE_ID_BASE16,
+ self::SPAN_ID_BASE10,
+ self::TRACE_ENABLED
+ ),
+ ];
+
+ $context = $this->cloudTracePropagator->extract($carrier);
+
+ $this->assertEquals(
+ SpanContext::createFromRemoteParent(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, TraceFlags::SAMPLED),
+ $this->getSpanContext($context)
+ );
+ }
+
+ public function test_extract_non_sampled_context(): void
+ {
+ $carrier = [
+ $this->xcloud => $this->generateTraceIdHeaderValue(
+ self::TRACE_ID_BASE16,
+ self::SPAN_ID_BASE10,
+ self::TRACE_DISABLED
+ ),
+ ];
+
+ $context = $this->cloudTracePropagator->extract($carrier);
+
+ $this->assertEquals(
+ SpanContext::createFromRemoteParent(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, TraceFlags::DEFAULT),
+ $this->getSpanContext($context)
+ );
+ }
+
+ private function getSpanContext(ContextInterface $context): SpanContextInterface
+ {
+ return Span::fromContext($context)->getContext();
+ }
+
+ private function withSpanContext(SpanContextInterface $spanContext, ContextInterface $context): ContextInterface
+ {
+ return $context->withContextValue(Span::wrap($spanContext));
+ }
+
+ private function generateTraceIdHeaderValue(
+ string $traceId,
+ string $spanId,
+ int $traceEnabled
+ ): string {
+ return sprintf('%s/%s;o=%d', $traceId, $spanId, $traceEnabled);
+ }
+}
diff --git a/src/Propagation/CloudTrace/tests/Unit/CloudTracePropagatorTest.php b/src/Propagation/CloudTrace/tests/Unit/CloudTracePropagatorTest.php
new file mode 100644
index 000000000..50d2b7e2b
--- /dev/null
+++ b/src/Propagation/CloudTrace/tests/Unit/CloudTracePropagatorTest.php
@@ -0,0 +1,201 @@
+cloudTracePropagator = CloudTracePropagator::getInstance();
+ [$this->xcloud] = $this->cloudTracePropagator->fields();
+ }
+
+ public function test_fields(): void
+ {
+ $this->assertSame(
+ ['x-cloud-trace-context'],
+ $this->cloudTracePropagator->fields()
+ );
+ }
+
+ public function test_inject_invalid_context(): void
+ {
+ $carrier = [];
+ $this
+ ->cloudTracePropagator
+ ->inject(
+ $carrier,
+ null,
+ $this->withSpanContext(
+ SpanContext::create(
+ SpanContextValidator::INVALID_TRACE,
+ SpanContextValidator::INVALID_SPAN,
+ TraceFlags::SAMPLED
+ ),
+ Context::getCurrent()
+ )
+ );
+ $this->assertEmpty($carrier);
+ }
+
+ public function test_inject_sampled_context(): void
+ {
+ $carrier = [];
+ $this
+ ->cloudTracePropagator
+ ->inject(
+ $carrier,
+ null,
+ $this->withSpanContext(
+ SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, TraceFlags::SAMPLED),
+ Context::getCurrent()
+ )
+ );
+
+ $this->assertSame(
+ [$this->xcloud => $this->generateTraceIdHeaderValue(
+ self::TRACE_ID_BASE16,
+ self::SPAN_ID_BASE10,
+ self::TRACE_ENABLED
+ )],
+ $carrier
+ );
+ }
+
+ public function test_inject_non_sampled_context(): void
+ {
+ $carrier = [];
+ $this
+ ->cloudTracePropagator
+ ->inject(
+ $carrier,
+ null,
+ $this->withSpanContext(
+ SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, TraceFlags::DEFAULT),
+ Context::getCurrent()
+ )
+ );
+
+ $this->assertSame(
+ [$this->xcloud => $this->generateTraceIdHeaderValue(
+ self::TRACE_ID_BASE16,
+ self::SPAN_ID_BASE10,
+ self::TRACE_DISABLED
+ )],
+ $carrier
+ );
+ }
+
+ public function test_inject_non_sampled_default_context(): void
+ {
+ $carrier = [];
+ $this
+ ->cloudTracePropagator
+ ->inject(
+ $carrier,
+ null,
+ $this->withSpanContext(
+ SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16),
+ Context::getCurrent()
+ )
+ );
+
+ $this->assertSame(
+ [$this->xcloud => $this->generateTraceIdHeaderValue(
+ self::TRACE_ID_BASE16,
+ self::SPAN_ID_BASE10,
+ self::TRACE_DISABLED
+ )],
+ $carrier
+ );
+ }
+
+ public function test_extract_nothing(): void
+ {
+ $this->assertSame(
+ Context::getCurrent(),
+ $this->cloudTracePropagator->extract([])
+ );
+ }
+
+ public function test_extract_sampled_context(): void
+ {
+ $carrier = [
+ $this->xcloud => $this->generateTraceIdHeaderValue(
+ self::TRACE_ID_BASE16,
+ self::SPAN_ID_BASE10,
+ self::TRACE_ENABLED
+ ),
+ ];
+
+ $context = $this->cloudTracePropagator->extract($carrier);
+
+ $this->assertEquals(
+ SpanContext::createFromRemoteParent(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, TraceFlags::SAMPLED),
+ $this->getSpanContext($context)
+ );
+ }
+
+ public function test_extract_non_sampled_context(): void
+ {
+ $carrier = [
+ $this->xcloud => $this->generateTraceIdHeaderValue(
+ self::TRACE_ID_BASE16,
+ self::SPAN_ID_BASE10,
+ self::TRACE_DISABLED
+ ),
+ ];
+
+ $context = $this->cloudTracePropagator->extract($carrier);
+
+ $this->assertEquals(
+ SpanContext::createFromRemoteParent(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, TraceFlags::DEFAULT),
+ $this->getSpanContext($context)
+ );
+ }
+
+ private function getSpanContext(ContextInterface $context): SpanContextInterface
+ {
+ return Span::fromContext($context)->getContext();
+ }
+
+ private function withSpanContext(SpanContextInterface $spanContext, ContextInterface $context): ContextInterface
+ {
+ return $context->withContextValue(Span::wrap($spanContext));
+ }
+
+ private function generateTraceIdHeaderValue(
+ string $traceId,
+ string $spanId,
+ int $traceEnabled
+ ): string {
+ return sprintf('%s/%s;o=%d', $traceId, $spanId, $traceEnabled);
+ }
+}
diff --git a/src/Propagation/CloudTrace/tests/Unit/UtilsTest.php b/src/Propagation/CloudTrace/tests/Unit/UtilsTest.php
new file mode 100644
index 000000000..ae00d241e
--- /dev/null
+++ b/src/Propagation/CloudTrace/tests/Unit/UtilsTest.php
@@ -0,0 +1,123 @@
+assertEquals(Utils::leftZeroPad($pad, $howMuch), $equalsTo, "Given leftZeroPad($pad, $howMuch) != $equalsTo");
+ }
+
+ public static function for_test_left_zero_pad() : array
+ {
+ return [
+ ['a', 3, '00a'],
+ ['aaa', 3, 'aaa'],
+ ['aaa', 16, '0000000000000aaa'],
+ ['', 1, '0'],
+ ];
+ }
+
+ /**
+ * @dataProvider for_test_dec_to_hex
+ */
+ public function test_dec_to_hex(string $decNum, string $equalsTo) : void
+ {
+ $this->assertEquals(Utils::decToHex($decNum), $equalsTo, "Given decToHex($decNum) != $equalsTo");
+ }
+
+ public static function for_test_dec_to_hex() : array
+ {
+ return [
+ ['10', 'a'],
+ ['1', '1'],
+ ['9223372036854775807', '7fffffffffffffff'],
+ ['18446744073709551615', 'ffffffffffffffff'],
+ ['28446744073709551615', '18ac7230489e7ffff'],
+ ];
+ }
+
+ /**
+ * @dataProvider for_test_hex_to_dec
+ */
+ public function test_hex_to_dec(string $hexNum, string $equalsTo) : void
+ {
+ $this->assertEquals(Utils::hexToDec($hexNum), $equalsTo, "Given hexToDec($hexNum) != $equalsTo");
+ }
+
+ public static function for_test_hex_to_dec() : array
+ {
+ return [
+ ['a', '10'],
+ ['B', '11'],
+ ['0xc', '12'],
+ ['1', '1'],
+ ['7fffffffffffffff', '9223372036854775807'],
+ ['1ffffffffffffffA', '2305843009213693946'],
+ ['1fffffffffffffff', '2305843009213693951'],
+ ['8fffffffffffffff', '10376293541461622783'],
+ ['7fffffffffffffff', '9223372036854775807'],
+ ['8000000000000000', '9223372036854775808'],
+ ['ffffffffffffffff', '18446744073709551615'],
+ ['18ac7230489e7ffff', '28446744073709551615'],
+ ];
+ }
+
+ /**
+ * @dataProvider for_test_is_big_num
+ */
+ public function test_is_big_num($num, bool $equalsTo) : void
+ {
+ $this->assertEquals(Utils::isBigNum($num), $equalsTo, "Given isBigNum($num) != $equalsTo");
+ }
+
+ public static function for_test_is_big_num() : array
+ {
+ return [
+ [-100.5, false],
+ [-1, false],
+ [1, false],
+ [100.5, false],
+ [9223372036854775806, false],
+ [9223372036854775807, true],
+ [9223372036854775808, true],
+ [18446744073709551615, true],
+ [28446744073709551615, true],
+ ];
+ }
+
+ /**
+ * @dataProvider for_test_base_convert
+ */
+ public function test_base_convert(string $num, int $fromBase, int $toBase, string $equalsTo) : void
+ {
+ $result = Utils::baseConvert($num, $fromBase, $toBase);
+ $this->assertEquals($result, $equalsTo, "Given baseConvert($num, $fromBase, $toBase) != $equalsTo (result=$result)");
+ }
+
+ public static function for_test_base_convert() : array
+ {
+ return [
+ ['b', 16, 10, '11'],
+ ['c', 16, 10, '12'],
+ ['fffffffffffff', 16, 10, '4503599627370495'],
+ ['7fffffff', 16, 10, '2147483647'],
+ ['7ffffffffffffffe', 16, 10, '9223372036854775806'],
+ ['7fffffffffffffff', 16, 10, '9223372036854775807'],
+ ['8000000000000000', 16, 10, '9223372036854775808'], // bigger than signed int max 64 bit
+ ['18ac7230489e7ffff', 16, 10, '28446744073709551615'],
+ ['28446744073709551615', 10, 16, '18ac7230489e7ffff'],
+ ['10', 10, 16, 'a'],
+ ];
+ }
+}