diff --git a/src/API/Trace/Propagation/B3MultiPropagator.php b/src/API/Trace/Propagation/B3MultiPropagator.php new file mode 100644 index 000000000..1a2e9610c --- /dev/null +++ b/src/API/Trace/Propagation/B3MultiPropagator.php @@ -0,0 +1,175 @@ +getContext(); + + if (!$spanContext->isValid()) { + return; + } + + $setter->set($carrier, self::TRACE_ID, $spanContext->getTraceId()); + $setter->set($carrier, self::SPAN_ID, $spanContext->getSpanId()); + $setter->set($carrier, self::SAMPLED, $spanContext->isSampled() ? self::IS_SAMPLED : self::IS_NOT_SAMPLED); + } + + public function extract($carrier, PropagationGetterInterface $getter = null, Context $context = null): Context + { + $getter = $getter ?? ArrayAccessGetterSetter::getInstance(); + $context = $context ?? Context::getCurrent(); + + $spanContext = self::extractImpl($carrier, $getter); + if (!$spanContext->isValid()) { + return $context; + } + + return $context->withContextValue(AbstractSpan::wrap($spanContext)); + } + + private static function getSampledValue($carrier, PropagationGetterInterface $getter): ?int + { + $value = $getter->get($carrier, self::SAMPLED); + + if ($value === null) { + return null; + } + + if ($value === '0' || $value === '1') { + return (int) $value; + } + + if (strtolower($value) === 'true') { + return 1; + } + if (strtolower($value) === 'false') { + return 0; + } + + return null; + } + + private static function extractImpl($carrier, PropagationGetterInterface $getter): SpanContextInterface + { + $traceId = $getter->get($carrier, self::TRACE_ID); + $spanId = $getter->get($carrier, self::SPAN_ID); + $sampled = self::getSampledValue($carrier, $getter); + + if ($traceId === null || $spanId === null) { + return SpanContext::getInvalid(); + } + + // Validates the traceId, spanId and sampled + // Returns an invalid spanContext if any of the checks fail + if (!SpanContext::isValidTraceId($traceId) || !SpanContext::isValidSpanId($spanId)) { + return SpanContext::getInvalid(); + } + + $isSampled = ($sampled === SpanContext::SAMPLED_FLAG); + + // Only traceparent header is extracted. No tracestate. + return SpanContext::createFromRemoteParent( + $traceId, + $spanId, + $isSampled ? SpanContextInterface::TRACE_FLAG_SAMPLED : SpanContextInterface::TRACE_FLAG_DEFAULT + ); + } +} diff --git a/tests/Unit/API/Trace/Propagation/B3MultiPropagatorTest.php b/tests/Unit/API/Trace/Propagation/B3MultiPropagatorTest.php new file mode 100644 index 000000000..d08a70894 --- /dev/null +++ b/tests/Unit/API/Trace/Propagation/B3MultiPropagatorTest.php @@ -0,0 +1,312 @@ +b3MultiPropagator = B3MultiPropagator::getInstance(); + } + + public function test_fields(): void + { + $this->assertSame( + ['X-B3-TraceId', 'X-B3-SpanId', 'X-B3-ParentSpanId', 'X-B3-Sampled', 'X-B3-Flags'], + $this->b3MultiPropagator->fields() + ); + } + + public function test_inject_empty(): void + { + $carrier = []; + $this->b3MultiPropagator->inject($carrier); + $this->assertEmpty($carrier); + } + + public function test_inject_invalid_context(): void + { + $carrier = []; + $this + ->b3MultiPropagator + ->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create( + SpanContext::INVALID_TRACE, + SpanContext::INVALID_SPAN, + SpanContext::SAMPLED_FLAG + ), + Context::getCurrent() + ) + ); + $this->assertEmpty($carrier); + } + + public function test_inject_sampled_context(): void + { + $carrier = []; + $this + ->b3MultiPropagator + ->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, SpanContextInterface::TRACE_FLAG_SAMPLED), + Context::getCurrent() + ) + ); + + $this->compareKeyCaseInsensitive( + [ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => self::IS_SAMPLED, + ], + $carrier + ); + } + + public function test_inject_non_sampled_context(): void + { + $carrier = []; + $this + ->b3MultiPropagator + ->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16), + Context::getCurrent() + ) + ); + + $this->compareKeyCaseInsensitive( + [ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => self::IS_NOT_SAMPLED, + ], + $carrier + ); + } + + public function test_extract_nothing(): void + { + $this->assertSame( + Context::getCurrent(), + $this->b3MultiPropagator->extract([]) + ); + } + + /** + * @dataProvider sampledValueProvider + */ + public function test_extract_sampled_context($sampledValue): void + { + $carrier = $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => $sampledValue, + ]); + + $this->assertEquals( + SpanContext::createFromRemoteParent(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, SpanContextInterface::TRACE_FLAG_SAMPLED), + $this->getSpanContext($this->b3MultiPropagator->extract($carrier)) + ); + } + + public function sampledValueProvider() + { + return [ + 'String sampled value' => ['1'], + 'Boolean(lower string) sampled value' => ['true'], + 'Boolean(upper string) sampled value' => ['TRUE'], + 'Boolean(camel string) sampled value' => ['True'], + ]; + } + + /** + * @dataProvider notSampledValueProvider + */ + public function test_extract_non_sampled_context($sampledValue): void + { + $carrier = $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => $sampledValue, + ]); + + $this->assertEquals( + SpanContext::createFromRemoteParent(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16), + $this->getSpanContext($this->b3MultiPropagator->extract($carrier)) + ); + } + + public function notSampledValueProvider() + { + return [ + 'String sampled value' => ['0'], + 'Boolean(lower string) sampled value' => ['false'], + 'Boolean(upper string) sampled value' => ['FALSE'], + 'Boolean(camel string) sampled value' => ['False'], + ]; + } + + /** + * @dataProvider invalidSampledValueProvider + */ + public function test_extract_invalid_sampled_context($sampledValue): void + { + $carrier = $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => $sampledValue, + ]); + + $this->assertEquals( + SpanContext::createFromRemoteParent(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16), + $this->getSpanContext($this->b3MultiPropagator->extract($carrier)) + ); + } + + public function invalidSampledValueProvider() + { + return [ + 'wrong sampled value' => ['wrong'], + 'null sampled value' => [null], + 'empty sampled value' => [[]], + ]; + } + + public function test_extract_and_inject(): void + { + $extractCarrier = $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => self::IS_SAMPLED, + ]); + $context = $this->b3MultiPropagator->extract($extractCarrier); + $injectCarrier = []; + $this->b3MultiPropagator->inject($injectCarrier, null, $context); + $this->assertSame($injectCarrier, $extractCarrier); + } + + public function test_extract_empty_trace_id(): void + { + $this->assertInvalid( + $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => '', + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => self::IS_SAMPLED, + ]) + ); + } + + public function test_extract_empty_span_id(): void + { + $this->assertInvalid( + $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => '', + B3MultiPropagator::SAMPLED => self::IS_SAMPLED, + ]) + ); + } + + public function test_invalid_trace_id(): void + { + $this->assertInvalid( + $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => 'abcdefghijklmnopabcdefghijklmnop', + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => self::IS_SAMPLED, + ]) + ); + } + + public function test_invalid_trace_id_size(): void + { + $this->assertInvalid( + $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16 . '00', + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16, + B3MultiPropagator::SAMPLED => self::IS_SAMPLED, + ]) + ); + } + + public function test_invalid_span_id(): void + { + $this->assertInvalid( + $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => 'abcdefghijklmnop', + B3MultiPropagator::SAMPLED => self::IS_SAMPLED, + ]) + ); + } + + public function test_invalid_span_id_size(): void + { + $this->assertInvalid( + $this->getLowerCaseKeys([ + B3MultiPropagator::TRACE_ID => self::TRACE_ID_BASE16, + B3MultiPropagator::SPAN_ID => self::SPAN_ID_BASE16 . '00', + B3MultiPropagator::SAMPLED => self::IS_SAMPLED, + ]) + ); + } + + private function assertInvalid(array $carrier): void + { + $this->assertSame( + Context::getCurrent(), + $this->b3MultiPropagator->extract($carrier), + ); + } + + private function getSpanContext(Context $context): SpanContextInterface + { + return Span::fromContext($context)->getContext(); + } + + private function withSpanContext(SpanContextInterface $spanContext, Context $context): Context + { + return $context->withContextValue(Span::wrap($spanContext)); + } + + private function compareKeyCaseInsensitive(array $expected, array $actual): void + { + $expectedLower = $this->getLowerCaseKeys($expected); + $this->assertSame( + $expectedLower, + $actual + ); + } + + private function getLowerCaseKeys(array $carrier): array + { + return array_change_key_case($carrier, CASE_LOWER); + } +}