diff --git a/api/Trace/PropagationSetter.php b/api/Trace/PropagationSetter.php index 9f6ded27..5226cd20 100644 --- a/api/Trace/PropagationSetter.php +++ b/api/Trace/PropagationSetter.php @@ -14,5 +14,5 @@ interface PropagationSetter * @param string $value * @return void */ - public function set($carrier, string $key, string $value) : void; + public function set(&$carrier, string $key, string $value) : void; } diff --git a/api/Trace/TextMapFormatPropagator.php b/api/Trace/TextMapFormatPropagator.php index 0f613a3b..247b8449 100644 --- a/api/Trace/TextMapFormatPropagator.php +++ b/api/Trace/TextMapFormatPropagator.php @@ -11,7 +11,7 @@ interface TextMapFormatPropagator * * @return array */ - public function fields() : array; + public static function fields() : array; /** * Encodes the given SpanContext into propagator specific format and injects @@ -22,7 +22,7 @@ interface TextMapFormatPropagator * @param PropagationSetter $setter * @return void */ - public function inject(SpanContext $context, $carrier, PropagationSetter $setter) : void; + public static function inject(SpanContext $context, &$carrier, PropagationSetter $setter) : void; /** * Retrieves encoded SpanContext using Getter from the associated carrier. @@ -31,5 +31,5 @@ interface TextMapFormatPropagator * @param PropagationGetter $getter * @return SpanContext */ - public function extract($carrier, PropagationGetter $getter): SpanContext; + public static function extract($carrier, PropagationGetter $getter): SpanContext; } diff --git a/sdk/Trace/PropagationMap.php b/sdk/Trace/PropagationMap.php new file mode 100644 index 00000000..90bf0fbb --- /dev/null +++ b/sdk/Trace/PropagationMap.php @@ -0,0 +1,68 @@ +offsetExists($lKey) ? $carrier->offsetGet($lKey) : null; + } + + if (\is_array($carrier)) { + if (empty($carrier)) { + return null; + } + + foreach ($carrier as $k => $value) { + if (strtolower($k) === $lKey) { + return $value; + } + } + + return null; + } + + throw new \InvalidArgumentException( + sprintf( + 'Invalid carrier of type %s. Unable to get value associated with key:%s', + \is_object($carrier) ? \get_class($carrier) : \gettype($carrier), + $key + ) + ); + } + + /** + * {@inheritdoc} + */ + public function set(&$carrier, string $key, string $value): void + { + if ($key === '') { + throw new \InvalidArgumentException('Unable to set value with an empty key'); + } + + if ($carrier instanceof ArrayAccess || \is_array($carrier)) { + $carrier[\strtolower($key)] = $value; + + return; + } + + throw new \InvalidArgumentException( + sprintf( + 'Invalid carrier of type %s. Unable to set value associated with key:%s', + \is_object($carrier) ? \get_class($carrier) : \gettype($carrier), + $key + ) + ); + } +} diff --git a/sdk/Trace/TraceContext.php b/sdk/Trace/TraceContext.php new file mode 100644 index 00000000..dec8ae97 --- /dev/null +++ b/sdk/Trace/TraceContext.php @@ -0,0 +1,96 @@ +getTraceId() . '-' . $context->getSpanId() . '-' . ($context->isSampled() ? '01' : '00'); + $setter->set($carrier, self::TRACEPARENT, $traceparent); + } + + /** + * {@inheritdoc} + */ + public static function extract($carrier, API\PropagationGetter $getter): API\SpanContext + { + $traceparent = $getter->get($carrier, self::TRACEPARENT); + if ($traceparent === null) { + throw new \InvalidArgumentException('Traceparent not present'); + } + + // Traceparent = {version}-{trace-id}-{parent-id}-{trace-flags} + $pieces = explode('-', $traceparent); + + $peicesCount = count($pieces); + if ($peicesCount != 4) { + throw new \InvalidArgumentException( + sprintf('Unable to extract traceparent. Expected 4 values, got %d', $peicesCount) + ); + } + + // Parse the traceparent version. Currently only '00' is supported. + $version = $pieces[0]; + if ((preg_match(self::VALID_VERSION, $version) === 0) || ($version !== self::SUPPORTED_VERSION)) { + throw new \InvalidArgumentException( + sprintf('Only version 00 is supported, got %s', $version) + ); + } + + $traceId = $pieces[1]; + if ((preg_match(self::VALID_TRACE, $traceId) === 0) || ($traceId === self::INVALID_TRACE)) { + throw new \InvalidArgumentException( + sprintf('TraceID must be exactly 16 bytes (32 chars) and at least one non-zero byte, got %s', $traceId) + ); + } + + $spanId = $pieces[2]; + if ((preg_match(self::VALID_SPAN, $spanId) === 0) || ($spanId === self::INVALID_SPAN)) { + throw new \InvalidArgumentException( + sprintf('SpanID must be exactly 8 bytes (16 chars) and at least one non-zero byte, got %s', $spanId) + ); + } + + $traceFlags = $pieces[3]; + if (preg_match(self::VALID_TRACEFLAGS, $traceFlags) === 0) { + throw new \InvalidArgumentException( + sprintf('TraceFlags must be exactly 1 bytes (1 char) representing a bit field, got %s', $traceFlags) + ); + } + + // Only the sampled flag is extracted from the traceFlags (00000001) + $convertedTraceFlags = hexdec($traceFlags); + $isSampled = ($convertedTraceFlags & self::SAMPLED_FLAG) === self::SAMPLED_FLAG; + + return SpanContext::restore($traceId, $spanId, $isSampled, true); + } +} diff --git a/tests/Sdk/Unit/Trace/PropagationMapTest.php b/tests/Sdk/Unit/Trace/PropagationMapTest.php new file mode 100644 index 00000000..312f70dd --- /dev/null +++ b/tests/Sdk/Unit/Trace/PropagationMapTest.php @@ -0,0 +1,113 @@ + 'alpha']; + $map = new PropagationMap(); + + $value = $map->get($carrier, 'a'); + $this->assertSame('alpha', $value); + } + + /** + * @test + */ + public function testGetFromMapArrayAccess() + { + $carrier = new ArrayObject(['a' => 'alpha']); + $map = new PropagationMap(); + + $value = $map->get($carrier, 'a'); + $this->assertSame('alpha', $value); + } + + /** + * @test + */ + public function testGetFromUnsupportedCarrier() + { + $carrier = new stdClass(); + $map = new PropagationMap(); + $this->expectException(\InvalidArgumentException::class); + $map->get($carrier, 'a'); + } + + /** + * @test + */ + public function testInvalidGetFromMap() + { + $carrier = ['a' => 'alpha']; + $map = new PropagationMap(); + + $this->assertNull($map->get([], 'a')); + $this->assertNull($map->get($carrier, 'b')); + + $this->expectException(\InvalidArgumentException::class); + $value = $map->get('invalid carrier', 'a'); + } + + /** + * @test + */ + public function testSetMapArray() + { + $carrier = ['a' => 'alpha']; + $map = new PropagationMap(); + + $map->set($carrier, 'b', 'bravo'); + $value = $map->get($carrier, 'b'); + $this->assertSame('bravo', $value); + } + + /** + * @test + */ + public function testSetMapArrayAccess() + { + $carrier = new ArrayObject(['a' => 'alpha']); + $map = new PropagationMap(); + + $map->set($carrier, 'b', 'bravo'); + $value = $map->get($carrier, 'b'); + $this->assertSame('bravo', $value); + } + + /** + * @test + */ + public function testSetUnsupportedCarrier() + { + $carrier = new stdClass(); + $map = new PropagationMap(); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid carrier of type ' . \get_class($carrier) . '. Unable to set value associated with key:a'); + $map->set($carrier, 'a', 'alpha'); + } + + /** + * @test + */ + public function testSetEmptyKey() + { + $carrier = ['a' => 'alpha']; + $map = new PropagationMap(); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to set value with an empty key'); + $map->set($carrier, '', 'alpha'); + } +} diff --git a/tests/Sdk/Unit/Trace/TraceContextTest.php b/tests/Sdk/Unit/Trace/TraceContextTest.php new file mode 100644 index 00000000..c22c50db --- /dev/null +++ b/tests/Sdk/Unit/Trace/TraceContextTest.php @@ -0,0 +1,176 @@ +assertSame($fields[0], TraceContext::TRACEPARENT); + $this->assertSame($fields[1], TraceContext::TRACESTATE); + } + + /** + * @test + */ + public function testExtractValidTraceContext() + { + $traceparentValues = [self::TRACEPARENTVALUE, // sampled == true + self::VERSION . '-' . self::TRACEID . '-' . self::SPANID . '-00', ]; // sampled == false + + foreach ($traceparentValues as $traceparentValue) { + $carrier = [TraceContext::TRACEPARENT => $traceparentValue]; + $map = new PropagationMap(); + $context = TraceContext::extract($carrier, $map); + $extractedTraceparent = '00-' . $context->getTraceId() . '-' . $context->getSpanId() . '-' . ($context->isSampled() ? '01' : '00'); + $this->assertSame($traceparentValue, $extractedTraceparent); + } + } + + /** + * @test + */ + public function testExtractInvalidTraceContext() + { + $carrier = []; + $map = new PropagationMap(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Traceparent not present'); + $context = TraceContext::extract($carrier, $map); + } + + /** + * @test + */ + public function testInjectValidTraceContext() + { + $carrier = []; + $map = new PropagationMap(); + $context = SpanContext::restore(self::TRACEID, self::SPANID, true, false); + TraceContext::inject($context, $carrier, $map); + + $this->assertSame(self::TRACEPARENTVALUE, $map->get($carrier, TraceContext::TRACEPARENT)); + } + + /** + * @test + */ + public function testTraceparentLength() + { + $invalidValues = [self::TRACEPARENTVALUE . '-extra', // Length > 4 values + self::VERSION . '-' . self::SPANID . '-' . self::SAMPLED, ]; // Length < 4 values + + foreach ($invalidValues as $invalidTraceparentValue) { + $carrier = [TraceContext::TRACEPARENT => $invalidTraceparentValue]; + $map = new PropagationMap(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to extract traceparent. Expected 4 values, got ' . 5); + $context = TraceContext::extract($carrier, $map); + } + } + + /** + * @test + */ + public function testInvalidTraceparentVersion() + { + $invalidValues = ['ff', // invalid hex value + '003', // Length > 2 + '1', // Length < 2 + '0j', ]; // Hex character != 'a - f or 0 - 9' + + $buildTraceparent = self::TRACEID . '-' . self::SPANID . '-' . self::SAMPLED; + + foreach ($invalidValues as $invalidVersion) { + $traceparentValue = $invalidVersion . '-' . $buildTraceparent; + $carrier = [TraceContext::TRACEPARENT => $traceparentValue]; + $map = new PropagationMap(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Only version 00 is supported, got ' . $invalidVersion); + $context = TraceContext::extract($carrier, $map); + } + } + + /** + * @test + */ + public function testInvalidTraceparentTraceId() + { + $invalidValues = ['00000000000000000000000000000000', // All zeros + '000000000000000000000000000000033', // Length > 32 + '0000000000000000000000000000031', // Length < 32 + '000000000000000000000g0000000032', ]; // Hex character != 'a - f or 0 - 9' + + foreach ($invalidValues as $invalidTraceId) { + $traceparentValue = self::VERSION . '-' . $invalidTraceId . '-' . self::SPANID . '-' . self::SAMPLED; + $carrier = [TraceContext::TRACEPARENT => $traceparentValue]; + $map = new PropagationMap(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('TraceID must be exactly 16 bytes (32 chars) and at least one non-zero byte, got ' . $invalidTraceId); + $context = TraceContext::extract($carrier, $map); + } + } + + /** + * @test + */ + public function testInvalidTraceparentSpanId() + { + $invalidValues = ['0000000000000000', // All zeros + '00000000000000017', // Length > 16 + '000000000000015', // Length < 16 + '00000000*0000016', ]; // Hex character != 'a - f or 0 - 9' + + foreach ($invalidValues as $invalidSpanId) { + $traceparentValue = self::VERSION . '-' . self::TRACEID . '-' . $invalidSpanId . '-' . self::SAMPLED; + $carrier = [TraceContext::TRACEPARENT => $traceparentValue]; + $map = new PropagationMap(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('SpanID must be exactly 8 bytes (16 chars) and at least one non-zero byte, got ' . $invalidSpanId); + $context = TraceContext::extract($carrier, $map); + } + } + + /** + * @test + */ + public function testInvalidTraceFlags() + { + $invalidValues = ['003', // Length > 2 + '1', // Length < 2 + '0g', ]; // Hex character != 'a - f or 0 - 9' + + $buildTraceperent = self::VERSION . '-' . self::TRACEID . '-' . self::SPANID; + foreach ($invalidValues as $invalidTraceFlag) { + $traceparentValue = $buildTraceperent . '-' . $invalidTraceFlag; + $carrier = [TraceContext::TRACEPARENT => $traceparentValue]; + $map = new PropagationMap(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('TraceFlags must be exactly 1 bytes (1 char) representing a bit field, got ' . $invalidTraceFlag); + $context = TraceContext::extract($carrier, $map); + } + } +}