Trace context propagation (#213)

* Add W3C traceparent propagation

* Add traceparent propagation test

* clean up styling

* clean up styling

* increase test coverage

* fix phan faiure

* add propagation map tests

* PR comments

* fix docblock errors
This commit is contained in:
Fahmy Mohammed 2020-11-18 10:36:18 -08:00 committed by GitHub
parent ed07a9e13b
commit bed3ec20f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 457 additions and 4 deletions

View File

@ -14,5 +14,5 @@ interface PropagationSetter
* @param string $value * @param string $value
* @return void * @return void
*/ */
public function set($carrier, string $key, string $value) : void; public function set(&$carrier, string $key, string $value) : void;
} }

View File

@ -11,7 +11,7 @@ interface TextMapFormatPropagator
* *
* @return array * @return array
*/ */
public function fields() : array; public static function fields() : array;
/** /**
* Encodes the given SpanContext into propagator specific format and injects * Encodes the given SpanContext into propagator specific format and injects
@ -22,7 +22,7 @@ interface TextMapFormatPropagator
* @param PropagationSetter $setter * @param PropagationSetter $setter
* @return void * @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. * Retrieves encoded SpanContext using Getter from the associated carrier.
@ -31,5 +31,5 @@ interface TextMapFormatPropagator
* @param PropagationGetter $getter * @param PropagationGetter $getter
* @return SpanContext * @return SpanContext
*/ */
public function extract($carrier, PropagationGetter $getter): SpanContext; public static function extract($carrier, PropagationGetter $getter): SpanContext;
} }

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Sdk\Trace;
use ArrayAccess;
use OpenTelemetry\Trace as API;
class PropagationMap implements API\PropagationGetter, API\PropagationSetter
{
/**
* {@inheritdoc}
*/
public function get($carrier, string $key): ?string
{
$lKey = \strtolower($key);
if ($carrier instanceof ArrayAccess) {
return $carrier->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
)
);
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Sdk\Trace;
use OpenTelemetry\Trace as API;
final class TraceContext implements API\TextMapFormatPropagator
{
public const TRACEPARENT = 'http_traceparent';
public const TRACESTATE = 'http_tracestate';
// TODO Consolidate these and the validity checks in SpanContext.php
private const SUPPORTED_VERSION = '00';
private const INVALID_TRACE = '00000000000000000000000000000000';
private const INVALID_SPAN = '0000000000000000';
private const VALID_VERSION = '/^[0-9a-f]{2}$/';
private const VALID_TRACE = '/^[0-9a-f]{32}$/';
private const VALID_SPAN = '/^[0-9a-f]{16}$/';
private const VALID_TRACEFLAGS = '/^[0-9a-f]{2}$/';
private const SAMPLED_FLAG = 1;
/**
* {@inheritdoc}
*/
public static function fields(): array
{
return [self::TRACEPARENT, self::TRACESTATE];
}
/**
* {@inheritdoc}
*/
public static function inject(API\SpanContext $context, &$carrier, API\PropagationSetter $setter): void
{
$traceparent = self::SUPPORTED_VERSION . '-' . $context->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);
}
}

View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Sdk\Unit\Trace;
use ArrayObject;
use OpenTelemetry\Sdk\Trace\PropagationMap;
use PHPUnit\Framework\TestCase;
use stdClass;
class PropagationMapTest extends TestCase
{
/**
* @test
*/
public function testGetFromMapArray()
{
$carrier = ['a' => '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');
}
}

View File

@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Sdk\Unit\Trace;
use OpenTelemetry\Sdk\Trace\PropagationMap;
use OpenTelemetry\Sdk\Trace\SpanContext;
use OpenTelemetry\Sdk\Trace\TraceContext;
use PHPUnit\Framework\TestCase;
class TraceContextTest extends TestCase
{
private const VERSION = '00';
private const TRACEID = '00000000000000000000000000000032';
private const SPANID = '0000000000000016';
private const SAMPLED = '01';
private const TRACEPARENTVALUE = self::VERSION . '-' . self::TRACEID . '-' . self::SPANID . '-' . self::SAMPLED;
/**
* @test
*/
public function testTraceContextFields()
{
$fields = TraceContext::fields();
$this->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);
}
}
}