diff --git a/.gitsplit.yml b/.gitsplit.yml index 4d235b5c6..f21429504 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -26,6 +26,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/extension-propagator-b3.git" - prefix: "src/Extension/Propagator/CloudTrace" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/extension-propagator-cloudtrace.git" + - prefix: "src/Extension/Propagator/Jaeger" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/extension-propagator-jaeger.git" # List of references to split (defined as regexp) origins: diff --git a/composer.json b/composer.json index 2bd2b597d..b86c11037 100644 --- a/composer.json +++ b/composer.json @@ -57,6 +57,7 @@ "src/Contrib/Zipkin/_register.php", "src/Extension/Propagator/B3/_register.php", "src/Extension/Propagator/CloudTrace/_register.php", + "src/Extension/Propagator/Jaeger/_register.php", "src/SDK/Logs/Exporter/_register.php", "src/SDK/Metrics/MetricExporter/_register.php", "src/SDK/Propagation/_register.php", diff --git a/src/Extension/Propagator/Jaeger/JaegerBaggagePropagator.php b/src/Extension/Propagator/Jaeger/JaegerBaggagePropagator.php new file mode 100644 index 000000000..3a2ba76e1 --- /dev/null +++ b/src/Extension/Propagator/Jaeger/JaegerBaggagePropagator.php @@ -0,0 +1,85 @@ +isEmpty()) { + return; + } + + /** @var Entry $entry */ + foreach ($baggage->getAll() as $key => $entry) { + $key = self::UBER_BAGGAGE_HEADER_PREFIX . $key; + $value = rawurlencode((string) $entry->getValue()); + $setter->set($carrier, $key, $value); + } + } + + /** {@inheritdoc} */ + public function extract($carrier, PropagationGetterInterface $getter = null, ContextInterface $context = null): ContextInterface + { + $getter ??= ArrayAccessGetterSetter::getInstance(); + $context ??= Context::getCurrent(); + + $baggageKeys = $getter->keys($carrier); + + if ($baggageKeys === []) { + return $context; + } + + $baggageBuilder = Baggage::getBuilder(); + + foreach ($baggageKeys as $key) { + if (strpos($key, self::UBER_BAGGAGE_HEADER_PREFIX) === 0) { + $baggageKey = substr($key, strlen(self::UBER_BAGGAGE_HEADER_PREFIX)); + $value = $getter->get($carrier, $key) ?? ''; + $baggageBuilder->set($baggageKey, rawurldecode($value)); + } + } + + return $context->withContextValue($baggageBuilder->build()); + } +} diff --git a/src/Extension/Propagator/Jaeger/JaegerDebugFlagContextKey.php b/src/Extension/Propagator/Jaeger/JaegerDebugFlagContextKey.php new file mode 100644 index 000000000..8133aa70b --- /dev/null +++ b/src/Extension/Propagator/Jaeger/JaegerDebugFlagContextKey.php @@ -0,0 +1,27 @@ +getContext(); + + if (!$spanContext->isValid()) { + return; + } + + $flag = $this->getFlag($spanContext, $context); + + $uberTraceId = sprintf( + '%s:%s:%d:%d', + $spanContext->getTraceId(), + $spanContext->getSpanId(), + self::DEFAULT_PARENT_SPAN_ID, + $flag + ); + + $setter->set($carrier, self::UBER_TRACE_ID_HEADER, $uberTraceId); + } + + /** {@inheritdoc} */ + public function extract($carrier, PropagationGetterInterface $getter = null, ContextInterface $context = null): ContextInterface + { + $getter ??= ArrayAccessGetterSetter::getInstance(); + $context ??= Context::getCurrent(); + + $spanContext = self::extractImpl($carrier, $getter, $context); + if (!$spanContext->isValid()) { + return $context; + } + + return $context->withContextValue(Span::wrap($spanContext)); + } + + private function getFlag(SpanContextInterface $spanContext, ContextInterface $context): int + { + if ($spanContext->isSampled()) { + if ($context->get(JaegerDebugFlagContextKey::instance())) { + return self::IS_DEBUG | self::IS_SAMPLED; + } + + return self::IS_SAMPLED; + } + + return self::IS_NOT_SAMPLED; + } + + private static function extractImpl($carrier, PropagationGetterInterface $getter, ContextInterface &$context): SpanContextInterface + { + $headerValue = $getter->get($carrier, self::UBER_TRACE_ID_HEADER); + + if ($headerValue === null) { + return SpanContext::getInvalid(); + } + + $pieces = explode(':', $headerValue); + + if (count($pieces) != 4) { + return SpanContext::getInvalid(); + } + + [$traceId, $spanId, $parentSpanId, $traceFlags] = $pieces; + + $traceId = str_pad($traceId, SpanContextValidator::TRACE_LENGTH, '0', STR_PAD_LEFT); + $spanId = str_pad($spanId, SpanContextValidator::SPAN_LENGTH, '0', STR_PAD_LEFT); + + if (!SpanContextValidator::isValidTraceId($traceId) || !SpanContextValidator::isValidSpanId($spanId)) { + return SpanContext::getInvalid(); + } + + if ((int) $traceFlags & self::IS_DEBUG) { + $context = $context->with(JaegerDebugFlagContextKey::instance(), true); + } + + $isSampled = ((int) $traceFlags) & 1; + + return SpanContext::createFromRemoteParent( + $traceId, + $spanId, + $isSampled ? TraceFlags::SAMPLED : TraceFlags::DEFAULT + ); + } +} diff --git a/src/Extension/Propagator/Jaeger/README.md b/src/Extension/Propagator/Jaeger/README.md new file mode 100644 index 000000000..6dec0c0c1 --- /dev/null +++ b/src/Extension/Propagator/Jaeger/README.md @@ -0,0 +1,31 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/extension-propagator-jaeger/releases) +[![Source](https://img.shields.io/badge/source-extension--propagator--jaeger-green)](https://github.com/open-telemetry/opentelemetry-php/tree/main/src/Extension/Propagator/Jaeger) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php:extension--propagator--jaeger-blue)](https://github.com/opentelemetry-php/extension-propagator-jaeger) +[![Latest Version](http://poser.pugx.org/open-telemetry/extension-propagator-jaeger/v/unstable)](https://packagist.org/packages/open-telemetry/extension-propagator-jaeger/) +[![Stable](http://poser.pugx.org/open-telemetry/extension-propagator-jaeger/v/stable)](https://packagist.org/packages/open-telemetry/extension-propagator-jaeger/) + +# OpenTelemetry Extension +### Jaeger Propagator + +Jaeger is a propagator that supports the specification for the header "uber-trace-id" used for trace context propagation across +service boundaries.(https://www.jaegertracing.io/docs/1.52/client-libraries/#propagation-format). +OpenTelemetry PHP Jaeger Propagator Extension provides option to use Jaeger Baggage (https://www.jaegertracing.io/docs/1.52/client-libraries/#baggage) propagator. + +### Usage +For Jaeger trace propagator: +```text +JaegerPropagator::getInstance() +``` + +For Jaeger baggage propagator: +```text +JaegerBaggagePropagator::getInstance() +``` + +Both of the above have `extract` and `inject` methods available to extract and inject respectively into the +header. + +## Contributing + +This repository is a read-only git subtree split. +To contribute, please see the main [OpenTelemetry PHP monorepo](https://github.com/open-telemetry/opentelemetry-php). diff --git a/src/Extension/Propagator/Jaeger/_register.php b/src/Extension/Propagator/Jaeger/_register.php new file mode 100644 index 000000000..edad7b5b1 --- /dev/null +++ b/src/Extension/Propagator/Jaeger/_register.php @@ -0,0 +1,18 @@ + $resource->getAttributes()->getDroppedAttributesCount(), ]; } + private function convertInstrumentationScope(InstrumentationScopeInterface $scope): array { return [ diff --git a/tests/Unit/Extension/Propagator/B3/B3SinglePropagatorTest.php b/tests/Unit/Extension/Propagator/B3/B3SinglePropagatorTest.php index bbf29e429..336d86e6f 100644 --- a/tests/Unit/Extension/Propagator/B3/B3SinglePropagatorTest.php +++ b/tests/Unit/Extension/Propagator/B3/B3SinglePropagatorTest.php @@ -307,6 +307,7 @@ public function test_empty_trace_id(): void $this->B3 => '-' . self::SPAN_ID_BASE16 . '-1', ]); } + public function test_invalid_trace_id(): void { $this->assertInvalid([ diff --git a/tests/Unit/Extension/Propagator/Jaeger/JaegerBaggagePropagatorTest.php b/tests/Unit/Extension/Propagator/Jaeger/JaegerBaggagePropagatorTest.php new file mode 100644 index 000000000..911067376 --- /dev/null +++ b/tests/Unit/Extension/Propagator/Jaeger/JaegerBaggagePropagatorTest.php @@ -0,0 +1,102 @@ +propagator = JaegerBaggagePropagator::getInstance(); + } + + public function test_fields(): void + { + $this->assertSame( + [], + $this->propagator->fields() + ); + } + + public function test_inject_empty_baggage(): void + { + $carrier = []; + $this->propagator->inject($carrier); + + $this->assertEmpty($carrier); + } + + public function test_inject_baggage(): void + { + $carrier = []; + $this->propagator->inject( + $carrier, + null, + Context::getCurrent()->withContextValue( + Baggage::getBuilder() + ->set('foo', 'bar') + ->build() + ) + ); + + $this->assertSame( + ['uberctx-foo' => 'bar'], + $carrier + ); + } + + public function test_inject_baggage_encoding(): void + { + $carrier = []; + $this->propagator->inject( + $carrier, + null, + Context::getCurrent()->withContextValue( + Baggage::getBuilder() + ->set('foo', 'foo / bar') + ->build() + ) + ); + + $this->assertSame( + ['uberctx-foo' => 'foo%20%2F%20bar'], + $carrier + ); + } + + public function test_extract_empty_baggage(): void + { + $this->assertEquals( + Context::getCurrent(), + $this->propagator->extract([]) + ); + } + + public function test_extract_baggage(): void + { + $carrier = [ + 'uberctx-foo' => 'bar', + 'uberctxfoo' => 'bar', + 'another' => 'foo', + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertEquals( + Baggage::getBuilder()->set('foo', 'bar')->build(), + Baggage::fromContext($context) + ); + } +} diff --git a/tests/Unit/Extension/Propagator/Jaeger/JaegerDebugFlagContextKeyTest.php b/tests/Unit/Extension/Propagator/Jaeger/JaegerDebugFlagContextKeyTest.php new file mode 100644 index 000000000..32424551a --- /dev/null +++ b/tests/Unit/Extension/Propagator/Jaeger/JaegerDebugFlagContextKeyTest.php @@ -0,0 +1,22 @@ +assertSame( + JaegerDebugFlagContextKey::instance(), + JaegerDebugFlagContextKey::instance() + ); + } +} diff --git a/tests/Unit/Extension/Propagator/Jaeger/JaegerPropagatorTest.php b/tests/Unit/Extension/Propagator/Jaeger/JaegerPropagatorTest.php new file mode 100644 index 000000000..c7c5238ea --- /dev/null +++ b/tests/Unit/Extension/Propagator/Jaeger/JaegerPropagatorTest.php @@ -0,0 +1,361 @@ +withContextValue(Span::wrap($spanContext)); + } + + private function generateTraceIdHeaderValue( + string $traceId, + string $spanId, + string $flag + ): string { + return sprintf( + '%s:%s:0:%s', + $traceId, + $spanId, + $flag + ); + } + + protected function setUp(): void + { + $this->propagator = JaegerPropagator::getInstance(); + [$this->fields] = $this->propagator->fields(); + } + + public function test_fields(): void + { + $this->assertSame( + ['uber-trace-id'], + $this->propagator->fields() + ); + } + + public function test_inject_invalid_context(): void + { + $carrier = []; + $this->propagator->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->propagator->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + TraceFlags::SAMPLED + ), + Context::getCurrent() + ) + ); + + $this->assertSame( + [$this->fields => $this->generateTraceIdHeaderValue( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + '1' + )], + $carrier + ); + } + + public function test_inject_not_sampled_context(): void + { + $carrier = []; + $this->propagator->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + ), + Context::getCurrent() + ) + ); + + $this->assertSame( + [$this->fields => $this->generateTraceIdHeaderValue( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + '0' + )], + $carrier + ); + } + + public function test_inject_null_context(): void + { + $carrier = []; + $this->propagator->inject( + $carrier + ); + + $this->assertEmpty($carrier); + } + + public function test_inject_sampled_with_debug_context(): void + { + $carrier = []; + $this->propagator->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + TraceFlags::SAMPLED + ), + Context::getCurrent() + )->with(JaegerDebugFlagContextKey::instance(), self::DEBUG_FLAG) + ); + + $this->assertSame( + [$this->fields => $this->generateTraceIdHeaderValue( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + '3' + )], + $carrier + ); + } + + public function test_inject_not_sampled_with_debug_context(): void + { + $carrier = []; + $this->propagator->inject( + $carrier, + null, + $this->withSpanContext( + SpanContext::create( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + ), + Context::getCurrent() + )->with(JaegerDebugFlagContextKey::instance(), self::DEBUG_FLAG) + ); + + $this->assertSame( + [$this->fields => $this->generateTraceIdHeaderValue( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + '0' + )], + $carrier + ); + } + + public function test_extract_nothing(): void + { + $this->assertSame( + Context::getCurrent(), + $this->propagator->extract([]) + ); + } + + public function test_extract_sampled_context(): void + { + $carrier = [ + $this->fields => $this->generateTraceIdHeaderValue( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + '1' + ), + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertEquals( + SpanContext::createFromRemoteParent( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + TraceFlags::SAMPLED + ), + Span::fromContext($context)->getContext() + ); + } + + public function test_extract_not_sampled_context(): void + { + $carrier = [ + $this->fields => $this->generateTraceIdHeaderValue( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + '0' + ), + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertEquals( + SpanContext::createFromRemoteParent( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16 + ), + Span::fromContext($context)->getContext() + ); + } + + public function test_extract_debug_context(): void + { + $carrier = [ + $this->fields => $this->generateTraceIdHeaderValue( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16, + '2' + ), + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertEquals( + SpanContext::createFromRemoteParent( + self::TRACE_ID_BASE16, + self::SPAN_ID_BASE16 + ), + Span::fromContext($context)->getContext() + ); + } + + public function test_extract_invalid_uber_trace_id(): void + { + $carrier = [ + $this->fields => '000000000000000000000000deadbeef:00000000deadbef0:00', + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertSame( + Context::getCurrent(), + $context + ); + } + + public function test_extract_invalid_trace_id(): void + { + $carrier = [ + $this->fields => $this->generateTraceIdHeaderValue( + SpanContextValidator::INVALID_TRACE, + '00000000deadbef0', + '1' + ), + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertSame( + Context::getCurrent(), + $context + ); + } + + public function test_extract_invalid_span_id(): void + { + $carrier = [ + $this->fields => $this->generateTraceIdHeaderValue( + '000000000000000053ce929d0e0e4736', + SpanContextValidator::INVALID_SPAN, + '1' + ), + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertSame( + Context::getCurrent(), + $context + ); + } + + public function test_extract_short_trace_id(): void + { + $carrier = [ + $this->fields => $this->generateTraceIdHeaderValue( + self::TRACE_ID_SHORT, + '00000000deadbef0', + '1' + ), + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertEquals( + SpanContext::createFromRemoteParent( + '000000000000000053ce929d0e0e4736', + '00000000deadbef0', + TraceFlags::SAMPLED + ), + Span::fromContext($context)->getContext() + ); + } + + public function test_extract_short_span_id(): void + { + $carrier = [ + $this->fields => $this->generateTraceIdHeaderValue( + '000000000000000053ce929d0e0e4736', + self::SPAN_ID_SHORT, + '1' + ), + ]; + + $context = $this->propagator->extract($carrier); + + $this->assertEquals( + SpanContext::createFromRemoteParent( + '000000000000000053ce929d0e0e4736', + '00000000deadbef0', + TraceFlags::SAMPLED + ), + Span::fromContext($context)->getContext() + ); + } +} diff --git a/tests/Unit/SDK/FactoryRegistryTest.php b/tests/Unit/SDK/FactoryRegistryTest.php index 84b5331e7..20bbd284c 100644 --- a/tests/Unit/SDK/FactoryRegistryTest.php +++ b/tests/Unit/SDK/FactoryRegistryTest.php @@ -106,9 +106,13 @@ public static function textMapPropagator(): array { return [ ['tracecontext'], - ['b3multi'], + ['baggage'], ['b3'], + ['b3multi'], ['cloudtrace'], + ['cloudtrace-oneway'], + ['jaeger'], + ['jaeger-baggage'], ]; } diff --git a/tests/Unit/SDK/Propagation/PropagatorFactoryTest.php b/tests/Unit/SDK/Propagation/PropagatorFactoryTest.php index 3ef72a1d2..67fdb4f91 100644 --- a/tests/Unit/SDK/Propagation/PropagatorFactoryTest.php +++ b/tests/Unit/SDK/Propagation/PropagatorFactoryTest.php @@ -12,6 +12,8 @@ use OpenTelemetry\Context\Propagation\NoopTextMapPropagator; use OpenTelemetry\Extension\Propagator\B3\B3Propagator; use OpenTelemetry\Extension\Propagator\CloudTrace\CloudTracePropagator; +use OpenTelemetry\Extension\Propagator\Jaeger\JaegerBaggagePropagator; +use OpenTelemetry\Extension\Propagator\Jaeger\JaegerPropagator; use OpenTelemetry\SDK\Common\Configuration\KnownValues; use OpenTelemetry\SDK\Common\Configuration\Variables; use OpenTelemetry\SDK\Propagation\PropagatorFactory; @@ -54,6 +56,8 @@ public static function propagatorsProvider(): array [KnownValues::VALUE_B3, B3Propagator::class], [KnownValues::VALUE_CLOUD_TRACE, CloudTracePropagator::class], [KnownValues::VALUE_CLOUD_TRACE_ONEWAY, CloudTracePropagator::class], + [KnownValues::VALUE_JAEGER, JaegerPropagator::class], + [KnownValues::VALUE_JAEGER_BAGGAGE, JaegerBaggagePropagator::class], [KnownValues::VALUE_B3_MULTI, B3Propagator::class], [KnownValues::VALUE_NONE, NoopTextMapPropagator::class], [sprintf('%s,%s', KnownValues::VALUE_B3, KnownValues::VALUE_BAGGAGE), MultiTextMapPropagator::class],