Map OTEL Resources to Jaeger Process Tags (#636)

* Commit in progress work because Gitpod is weird

* Finish initial stab at the changes

* Forgot to update the exporter

* Add in proper handling of serialization of ResourceInfo

* ksort doesn't return the array...

* Only PHP 8.1 supports key-ed array destructuring

* Creating some adapters to break the hard dependency on Batch to make testing easier

* Refactoring to break the hard dependency on Batch

* Add initial unit tests around the new logic in HttpSender

* Some refactoring of the test's helpers

* Another small refactor

* Some more refactoring

* Rename ResourceInfo's test file to match it's file name

* Move a test our of REsourceInfoTest into a more appropriate file

* Add some basic conformance tests around the new ResourceInfo::serialize method

* Add a test to try and catch future properties that aren't added to ResourceInfo::serialize

* CLean up comments

* Replace string with constant

* Improve variable names

* Cleaning up comment

* Split out tag creation into a helper class

* Phan didn't like the type docs

* Fix style

* Fix tests after merge from main

* Fix coverage reporting issues on 2 of the new files

* Fix style

* Rename vals to values

* Shortening the batch adapter factory method name to create

* Inline a method call

* Inline some method calls

* Shorten factory method to just "create"

* Inline some code

* Split assertions into 3 tests

* Rename parameter in interface implementation for psalm + update the rest of the file to match

Co-authored-by: Timo Michna <timomichna@yahoo.de>
This commit is contained in:
Grunet 2022-03-27 20:18:32 -05:00 committed by GitHub
parent a2f64053da
commit 39c99e5a2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 488 additions and 145 deletions

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Contrib\Jaeger\BatchAdapter;
use Jaeger\Thrift\Batch;
use Thrift\Protocol\TProtocol;
class BatchAdapter implements BatchAdapterInterface
{
private Batch $batchInstance;
public function __construct(array $values)
{
$this->batchInstance = new Batch($values);
}
public function write(TProtocol $output): void
{
$this->batchInstance->write($output);
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Contrib\Jaeger\BatchAdapter;
class BatchAdapterFactory implements BatchAdapterFactoryInterface
{
public function create(array $values): BatchAdapterInterface
{
return new BatchAdapter($values);
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Contrib\Jaeger\BatchAdapter;
interface BatchAdapterFactoryInterface
{
public function create(array $values): BatchAdapterInterface;
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Contrib\Jaeger\BatchAdapter;
use Thrift\Protocol\TProtocol;
interface BatchAdapterInterface
{
public function write(TProtocol $output): void;
}

View File

@ -18,8 +18,6 @@ class HttpCollectorExporter implements SpanExporterInterface
use UsesSpanConverterTrait;
use SpanExporterTrait;
private SpanConverter $spanConverter;
private HttpSender $sender;
public function __construct(
@ -39,8 +37,6 @@ class HttpCollectorExporter implements SpanExporterInterface
$name,
$parsedEndpoint
);
$this->spanConverter = new SpanConverter();
}
/**
@ -48,9 +44,7 @@ class HttpCollectorExporter implements SpanExporterInterface
*/
public function doExport(iterable $spans): int
{
$this->sender->send(
$this->spanConverter->convert($spans)
);
$this->sender->send($spans);
return SpanExporterInterface::STATUS_SUCCESS;
}

View File

@ -4,10 +4,14 @@ declare(strict_types=1);
namespace OpenTelemetry\Contrib\Jaeger;
use Jaeger\Thrift\Batch;
use Jaeger\Thrift\Process;
use Jaeger\Thrift\Span as JTSpan;
use OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapterFactory;
use OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapterFactoryInterface;
use OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapterInterface;
use OpenTelemetry\Contrib\Jaeger\TagFactory\TagFactory;
use OpenTelemetry\SDK\Behavior\LogsMessagesTrait;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SemConv\ResourceAttributes;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
@ -22,75 +26,104 @@ class HttpSender
private TProtocol $protocol;
private BatchAdapterFactoryInterface $batchAdapterFactory;
public function __construct(
ClientInterface $client,
RequestFactoryInterface $requestFactory,
StreamFactoryInterface $streamFactory,
string $serviceName,
ParsedEndpointUrl $parsedEndpoint
ParsedEndpointUrl $parsedEndpoint,
BatchAdapterFactoryInterface $batchAdapterFactory = null
) {
$this->serviceName = $serviceName;
$transport = (new ThriftHttpTransport(
$client,
$requestFactory,
$streamFactory,
$parsedEndpoint
));
$this->protocol = new TBinaryProtocol(
new ThriftHttpTransport(
$client,
$requestFactory,
$streamFactory,
$parsedEndpoint
)
);
$this->protocol = new TBinaryProtocol($transport);
$this->batchAdapterFactory = $batchAdapterFactory ?? new BatchAdapterFactory();
}
/**
* @param JTSpan[] $spans
*/
public function send(array $spans): void
public function send(iterable $spans): void
{
///** @var Tag[] $tags */ TODO - uncomment this once the code below is uncommented/adapted
$batches = $this->createBatchesPerResource(
self::groupSpansByResource($spans)
);
foreach ($batches as $batch) {
$this->sendBatch($batch);
}
}
private static function groupSpansByResource(iterable $spans): array
{
$spansGroupedByResource = [];
foreach ($spans as $span) {
/** @var ResourceInfo */
$resource = $span->getResource();
$resourceAsKey = $resource->serialize();
if (!isset($spansGroupedByResource[$resourceAsKey])) {
$spansGroupedByResource[$resourceAsKey] = [
'spans' => [],
'resource' => $resource,
];
}
$spansGroupedByResource[$resourceAsKey]['spans'][] = $span;
}
return $spansGroupedByResource;
}
private function createBatchesPerResource(array $spansGroupedByResource): array
{
$batches = [];
foreach ($spansGroupedByResource as $unused => $dataForBatch) {
$batch = $this->batchAdapterFactory->create([
'spans' => (new SpanConverter())->convert(
$dataForBatch['spans']
),
'process' => $this->createProcessFromResource(
$dataForBatch['resource']
),
]);
$batches[] = $batch;
}
return $batches;
}
private function createProcessFromResource(ResourceInfo $resource): Process
{
$serviceName = $this->serviceName; //Defaulting to (what should be) the default resource's service name
$tags = [];
foreach ($resource->getAttributes() as $key => $value) {
if ($key === ResourceAttributes::SERVICE_NAME) {
$serviceName = (string) $value;
//TODO - determine what of this is still needed and how to adapt it for spec compliance
// foreach ($this->tracer->getTags() as $k => $v) {
// if (!in_array($k, $this->mapper->getSpecialSpanTags())) {
// if (strpos($k, $this->mapper->getProcessTagsPrefix()) !== 0) {
// continue ;
// }
continue;
}
// $quoted = preg_quote($this->mapper->getProcessTagsPrefix());
// $k = preg_replace(sprintf('/^%s/', $quoted), '', $k);
// }
$tags[] = TagFactory::create($key, $value);
}
// if ($k === JAEGER_HOSTNAME_TAG_KEY) {
// $k = "hostname";
// }
// $tags[] = new Tag([
// "key" => $k,
// "vType" => TagType::STRING,
// "vStr" => $v
// ]);
// }
// $tags[] = new Tag([
// "key" => "format",
// "vType" => TagType::STRING,
// "vStr" => "jaeger.thrift"
// ]);
// $tags[] = new Tag([
// "key" => "ip",
// "vType" => TagType::STRING,
// "vStr" => $this->tracer->getIpAddress()
// ]);
$batch = new Batch([
'spans' => $spans,
'process' => new Process([
'serviceName' => $this->serviceName,
'tags' => $tags,
]),
return new Process([
'serviceName' => $serviceName,
'tags' => $tags,
]);
}
private function sendBatch(BatchAdapterInterface $batch): void
{
$batch->write($this->protocol);
$this->protocol->getTransport()->flush();
}

View File

@ -8,10 +8,9 @@ use Jaeger\Thrift\Log;
use Jaeger\Thrift\Span as JTSpan;
use Jaeger\Thrift\SpanRef;
use Jaeger\Thrift\SpanRefType;
use Jaeger\Thrift\Tag;
use Jaeger\Thrift\TagType;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Contrib\Jaeger\TagFactory\TagFactory;
use OpenTelemetry\SDK\Common\Time\Util as TimeUtil;
use OpenTelemetry\SDK\Trace\EventInterface;
use OpenTelemetry\SDK\Trace\LinkInterface;
@ -197,89 +196,12 @@ class SpanConverter implements SpanConverterInterface
{
$tags = [];
foreach ($tagPairs as $key => $value) {
$tags[] = self::buildTag($key, $value);
$tags[] = TagFactory::create($key, $value);
}
return $tags;
}
private static function buildTag(string $key, $value): Tag
{
return self::createJaegerTagInstance(
$key,
self::convertValueToTypeJaegerTagsSupport($value)
);
}
private static function convertValueToTypeJaegerTagsSupport($value)
{
if (is_array($value)) {
return self::serializeArrayToString($value);
}
return $value;
}
private static function createJaegerTagInstance(string $key, $value)
{
if (is_bool($value)) {
return new Tag([
'key' => $key,
'vType' => TagType::BOOL,
'vBool' => $value,
]);
}
if (is_integer($value)) {
return new Tag([
'key' => $key,
'vType' => TagType::LONG,
'vLong' => $value,
]);
}
if (is_numeric($value)) {
return new Tag([
'key' => $key,
'vType' => TagType::DOUBLE,
'vDouble' => $value,
]);
}
return new Tag([
'key' => $key,
'vType' => TagType::STRING,
'vStr' => (string) $value,
]);
}
private static function serializeArrayToString(array $arrayToSerialize): string
{
return self::recursivelySerializeArray($arrayToSerialize);
}
private static function recursivelySerializeArray($value): string
{
if (is_array($value)) {
return join(',', array_map(function ($val) {
return self::recursivelySerializeArray($val);
}, $value));
}
// Casting false to string makes an empty string
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
// Floats will lose precision if their string representation
// is >=14 or >=17 digits, depending on PHP settings.
// Can also throw E_RECOVERABLE_ERROR if $value is an object
// without a __toString() method.
// This is possible because OpenTelemetry\Trace\Span does not verify
// setAttribute() $value input.
return (string) $value;
}
private static function convertOtelEventsToJaegerLogs(SpanDataInterface $span): array
{
return array_map(

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Contrib\Jaeger\TagFactory;
use Jaeger\Thrift\Tag;
use Jaeger\Thrift\TagType;
class TagFactory
{
public static function create(string $key, $value): Tag
{
return self::createJaegerTagInstance(
$key,
self::convertValueToTypeJaegerTagsSupport($value)
);
}
private static function convertValueToTypeJaegerTagsSupport($value)
{
if (is_array($value)) {
return self::serializeArrayToString($value);
}
return $value;
}
private static function createJaegerTagInstance(string $key, $value)
{
if (is_bool($value)) {
return new Tag([
'key' => $key,
'vType' => TagType::BOOL,
'vBool' => $value,
]);
}
if (is_integer($value)) {
return new Tag([
'key' => $key,
'vType' => TagType::LONG,
'vLong' => $value,
]);
}
if (is_numeric($value)) {
return new Tag([
'key' => $key,
'vType' => TagType::DOUBLE,
'vDouble' => $value,
]);
}
return new Tag([
'key' => $key,
'vType' => TagType::STRING,
'vStr' => (string) $value,
]);
}
private static function serializeArrayToString(array $arrayToSerialize): string
{
return self::recursivelySerializeArray($arrayToSerialize);
}
private static function recursivelySerializeArray($value): string
{
if (is_array($value)) {
return join(',', array_map(function ($val) {
return self::recursivelySerializeArray($val);
}, $value));
}
// Casting false to string makes an empty string
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
// Floats will lose precision if their string representation
// is >=14 or >=17 digits, depending on PHP settings.
// Can also throw E_RECOVERABLE_ERROR if $value is an object
// without a __toString() method.
// This is possible because OpenTelemetry\Trace\Span does not verify
// setAttribute() $value input.
return (string) $value;
}
}

View File

@ -41,6 +41,18 @@ class ResourceInfo
return $this->schemaUrl;
}
public function serialize(): string
{
$copyOfAttributesAsArray = array_slice($this->attributes->toArray(), 0); //This may be overly cautious (in trying to avoid mutating the source array)
ksort($copyOfAttributesAsArray); //sort the associative array by keys since the serializer will consider equal arrays different otherwise
//The exact return value doesn't matter, as long as it can distingusih between instances that represent the same/different resources
return serialize([
'schemaUrl' => $this->schemaUrl,
'attributes' => $copyOfAttributesAsArray,
]);
}
/**
* Backward compatibility methods
*

View File

@ -14,6 +14,9 @@ use PHPUnit\Framework\TestCase;
* @covers OpenTelemetry\Contrib\Jaeger\HttpSender
* @covers OpenTelemetry\Contrib\Jaeger\ThriftHttpTransport
* @covers OpenTelemetry\Contrib\Jaeger\ParsedEndpointUrl
* @covers OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapter
* @covers OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapterFactory
*
*/
class JaegerHttpCollectorExporterTest extends TestCase
{

View File

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\Contrib;
use OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapterFactoryInterface;
use OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapterInterface;
use OpenTelemetry\Contrib\Jaeger\HttpSender;
use OpenTelemetry\Contrib\Jaeger\ParsedEndpointUrl;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\Tests\Unit\SDK\Util\SpanData;
use PHPUnit\Framework\TestCase;
use Thrift\Protocol\TProtocol;
/**
* @covers OpenTelemetry\Contrib\Jaeger\HttpSender
*/
class JaegerHttpSenderTest extends TestCase
{
use UsesHttpClientTrait;
private function createSenderAndMocks(array $inputs): array
{
[
'serviceName' => $serviceName
] = $inputs;
$mockBatchAdapterFactory = $this->createBatchAdapterFactoryMock();
/**
* @psalm-suppress PossiblyInvalidArgument
*/
$sender = new HttpSender(
$this->getClientInterfaceMock(),
$this->getRequestFactoryInterfaceMock(),
$this->getStreamFactoryInterfaceMock(),
$serviceName,
$this->createParsedEndpointUrlMock(),
$mockBatchAdapterFactory
);
return [
'sender' => $sender,
'mockBatchAdapterFactory' => $mockBatchAdapterFactory,
];
}
private function createParsedEndpointUrlMock(): ParsedEndpointUrl
{
/** @var ParsedEndpointUrl */
$mock = $this->createMock(ParsedEndpointUrl::class);
return $mock;
}
private function createBatchAdapterFactoryMock(): BatchAdapterFactoryInterface
{
return new class() implements BatchAdapterFactoryInterface {
//Just enough spy functionality for what was needed for now. Generalize and extend as needed
private array $interceptedValues = [];
public function getInterceptedValues()
{
return $this->interceptedValues;
}
public function create(array $values): BatchAdapterInterface
{
$this->interceptedValues[] = $values;
$mockBatchAdapter = new class() implements BatchAdapterInterface {
public function write(TProtocol $output): void
{
}
};
return $mockBatchAdapter;
}
};
}
public function test_span_and_process_data_are_batched_by_resource(): void
{
[
'sender' => $sender,
'mockBatchAdapterFactory' => $mockBatchAdapterFactory
] = $this->createSenderAndMocks([
'serviceName' => 'nameOfThe1stLogicalApp',
]);
$spans = [
(new SpanData())->setResource(ResourceInfo::create(
new Attributes(), //code should default service.name from how its set above
)),
(new SpanData())->setResource(ResourceInfo::create(
new Attributes([
'service.name' => 'nameOfThe2ndLogicalApp',
]),
)),
];
$sender->send($spans);
$interceptedValues = $mockBatchAdapterFactory->getInterceptedValues();
$this->assertSame(2, count($interceptedValues));
//1st batch
$this->assertSame(1, count($interceptedValues[0]['spans'])); //Detailed tests for the span conversion live elsewhere
//2nd batch
$this->assertSame(1, count($interceptedValues[1]['spans'])); //Detailed tests for the span conversion live elsewhere
}
public function test_process_service_names_are_correctly_set_from_resource_attributes_or_the_default_service_name(): void
{
[
'sender' => $sender,
'mockBatchAdapterFactory' => $mockBatchAdapterFactory
] = $this->createSenderAndMocks([
'serviceName' => 'nameOfThe1stLogicalApp',
]);
$spans = [
(new SpanData())->setResource(ResourceInfo::create(
new Attributes(), //code should default service.name from how its set above
)),
(new SpanData())->setResource(ResourceInfo::create(
new Attributes([
'service.name' => 'nameOfThe2ndLogicalApp',
]),
)),
];
$sender->send($spans);
$interceptedValues = $mockBatchAdapterFactory->getInterceptedValues();
//1st batch
$this->assertSame('nameOfThe1stLogicalApp', $interceptedValues[0]['process']->serviceName);
//2nd batch
$this->assertSame('nameOfThe2ndLogicalApp', $interceptedValues[1]['process']->serviceName);
}
public function test_tags_are_correctly_set_from_resource_attributes(): void
{
[
'sender' => $sender,
'mockBatchAdapterFactory' => $mockBatchAdapterFactory
] = $this->createSenderAndMocks([
'serviceName' => 'someServiceName',
]);
$spans = [
(new SpanData())->setResource(ResourceInfo::create(
new Attributes(),
)),
(new SpanData())->setResource(ResourceInfo::create(
new Attributes([
'telemetry.sdk.name' => 'opentelemetry',
]),
)),
];
$sender->send($spans);
$interceptedValues = $mockBatchAdapterFactory->getInterceptedValues();
//1st batch
$this->assertSame(0, count($interceptedValues[0]['process']->tags));
//2nd batch
$this->assertSame(1, count($interceptedValues[1]['process']->tags));
$this->assertSame('telemetry.sdk.name', $interceptedValues[1]['process']->tags[0]->key);
$this->assertSame('opentelemetry', $interceptedValues[1]['process']->tags[0]->vStr);
}
}

View File

@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase;
/**
* @covers OpenTelemetry\Contrib\Jaeger\SpanConverter
* @covers OpenTelemetry\Contrib\Jaeger\TagFactory\TagFactory
*/
class JaegerSpanConverterTest extends TestCase
{

View File

@ -25,4 +25,12 @@ class ComposerTest extends TestCase
$this->assertSame($name, $resource->getAttributes()->get(ResourceAttributes::SERVICE_NAME));
$this->assertSame($version, $resource->getAttributes()->get(ResourceAttributes::SERVICE_VERSION));
}
public function test_composer_detector(): void
{
$resource = (new Detectors\Composer())->getResource();
$this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::SERVICE_NAME));
$this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::SERVICE_VERSION));
}
}

View File

@ -17,7 +17,7 @@ use PHPUnit\Framework\TestCase;
/**
* @covers OpenTelemetry\SDK\Resource\ResourceInfo
*/
class ResourceTest extends TestCase
class ResourceInfoTest extends TestCase
{
use EnvironmentVariables;
@ -322,11 +322,55 @@ class ResourceTest extends TestCase
$this->assertEquals('foo', $resource->getAttributes()->get('service.name'));
}
public function test_composer_detector(): void
/**
* @dataProvider sameResourcesProvider
*/
public function test_serialize_returns_same_output_for_objects_representing_the_same_resource(ResourceInfo $resource1, ResourceInfo $resource2): void
{
$resource = (new Detectors\Composer())->getResource();
$this->assertSame($resource1->serialize(), $resource2->serialize());
}
$this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::SERVICE_NAME));
$this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::SERVICE_VERSION));
public function sameResourcesProvider(): iterable
{
yield 'Attribute keys sorted in ascending order vs Attribute keys sorted in descending order' => [
ResourceInfo::create(new Attributes([
'a' => 'someValue',
'b' => 'someValue',
'c' => 'someValue',
])),
ResourceInfo::create(new Attributes([
'c' => 'someValue',
'b' => 'someValue',
'a' => 'someValue',
])),
];
}
/**
* @dataProvider differentResourcesProvider
*/
public function test_serialize_returns_different_output_for_objects_representing_different_resources(ResourceInfo $resource1, ResourceInfo $resource2): void
{
$this->assertNotSame($resource1->serialize(), $resource2->serialize());
}
public function differentResourcesProvider(): iterable
{
yield 'Null schema url vs Some schema url' => [
ResourceInfo::create(new Attributes(), null),
ResourceInfo::create(new Attributes(), 'someSchemaUrl'),
];
}
public function test_serialize_incorporates_all_properties(): void
{
$resource = ResourceInfoFactory::emptyResource();
$properties = (new \ReflectionClass($resource))->getProperties();
$serializedResource = $resource->serialize();
foreach ($properties as $property) {
$this->assertStringContainsString($property->getName(), $serializedResource);
}
}
}