Group otlp spans by resource (#740)

* Group spans by resource

* Use proto methods instead of array constructor

* Use consistent naming for methods

* Suppress `InvalidArgument` false positives

Caused by `RepeatedField` being not generic.
This commit is contained in:
Tobias Bachert 2022-07-01 18:45:26 +02:00 committed by GitHub
parent 7c0c71ccab
commit 4c10268162
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 229 deletions

View File

@ -4,53 +4,130 @@ declare(strict_types=1);
namespace OpenTelemetry\Contrib\Otlp;
use function array_key_exists;
use function hex2bin;
use function iterator_to_array;
use OpenTelemetry\API\Trace as API;
use Opentelemetry\Proto\Collector\Trace\V1\ExportTraceServiceRequest;
use Opentelemetry\Proto\Common\V1\AnyValue;
use Opentelemetry\Proto\Common\V1\ArrayValue;
use Opentelemetry\Proto\Common\V1\InstrumentationScope;
use Opentelemetry\Proto\Common\V1\KeyValue;
use Opentelemetry\Proto\Resource\V1\Resource;
use Opentelemetry\Proto\Resource\V1\Resource as Resource_;
use Opentelemetry\Proto\Trace\V1\ResourceSpans;
use Opentelemetry\Proto\Trace\V1\ScopeSpans;
use Opentelemetry\Proto\Trace\V1\Span as CollectorSpan;
use Opentelemetry\Proto\Trace\V1\Span;
use Opentelemetry\Proto\Trace\V1\Span\Event;
use Opentelemetry\Proto\Trace\V1\Span\Link;
use Opentelemetry\Proto\Trace\V1\Span\SpanKind;
use Opentelemetry\Proto\Trace\V1\Status;
use Opentelemetry\Proto\Trace\V1\Status\StatusCode;
use OpenTelemetry\SDK\Common\Instrumentation\KeyGenerator;
use OpenTelemetry\SDK\Common\Attribute\AttributesInterface;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Trace\SpanConverterInterface;
use OpenTelemetry\SDK\Trace\SpanDataInterface;
use function serialize;
use function spl_object_id;
class SpanConverter implements SpanConverterInterface
{
public function convert(iterable $spans): array
{
return [$this->as_otlp_resource_span($spans)];
$pExportTraceServiceRequest = new ExportTraceServiceRequest();
$resourceSpans = [];
$resourceCache = [];
$scopeSpans = [];
$scopeCache = [];
foreach ($spans as $span) {
$resource = $span->getResource();
$instrumentationScope = $span->getInstrumentationScope();
$resourceId = $resourceCache[spl_object_id($resource)] ??= serialize([
$resource->getSchemaUrl(),
$resource->getAttributes()->toArray(),
$resource->getAttributes()->getDroppedAttributesCount(),
]);
$instrumentationScopeId = $scopeCache[spl_object_id($instrumentationScope)] ??= serialize([
$instrumentationScope->getName(),
$instrumentationScope->getVersion(),
$instrumentationScope->getSchemaUrl(),
$instrumentationScope->getAttributes()->toArray(),
$instrumentationScope->getAttributes()->getDroppedAttributesCount(),
]);
if (!$pResourceSpans = $resourceSpans[$resourceId] ?? null) {
/** @psalm-suppress InvalidArgument */
$pExportTraceServiceRequest->getResourceSpans()[]
= $resourceSpans[$resourceId]
= $pResourceSpans
= $this->convertResourceSpans($resource);
}
if (!$pScopeSpans = $scopeSpans[$resourceId][$instrumentationScopeId] ?? null) {
/** @psalm-suppress InvalidArgument */
$pResourceSpans->getScopeSpans()[]
= $scopeSpans[$resourceId][$instrumentationScopeId]
= $pScopeSpans
= $this->convertScopeSpans($instrumentationScope);
}
/** @psalm-suppress InvalidArgument */
$pScopeSpans->getSpans()[] = $this->convertSpan($span);
}
return iterator_to_array($pExportTraceServiceRequest->getResourceSpans());
}
private function as_otlp_key_value($key, $value): KeyValue
private function convertResourceSpans(ResourceInfo $resource): ResourceSpans
{
return new KeyValue([
'key' => $key,
'value' => $this->as_otlp_any_value($value),
]);
$pResourceSpans = new ResourceSpans();
$pResource = new Resource_();
$this->setAttributes($pResource, $resource->getAttributes());
$pResourceSpans->setResource($pResource);
$pResourceSpans->setSchemaUrl((string) $resource->getSchemaUrl());
return $pResourceSpans;
}
private function as_otlp_any_value($value): AnyValue
private function convertScopeSpans(InstrumentationScopeInterface $instrumentationScope): ScopeSpans
{
$pScopeSpans = new ScopeSpans();
$pInstrumentationScope = new InstrumentationScope();
$pInstrumentationScope->setName($instrumentationScope->getName());
$pInstrumentationScope->setVersion((string) $instrumentationScope->getVersion());
$pScopeSpans->setScope($pInstrumentationScope);
$pScopeSpans->setSchemaUrl((string) $instrumentationScope->getSchemaUrl());
return $pScopeSpans;
}
/**
* @param Resource_|Span|Event|Link $pElement
*/
private function setAttributes($pElement, AttributesInterface $attributes): void
{
foreach ($attributes as $key => $value) {
/** @psalm-suppress InvalidArgument */
$pElement->getAttributes()[] = (new KeyValue())
->setKey($key)
->setValue($this->convertAnyValue($value));
}
$pElement->setDroppedAttributesCount($attributes->getDroppedAttributesCount());
}
private function convertAnyValue($value): AnyValue
{
$result = new AnyValue();
switch (true) {
case is_array($value):
$values = [];
$values = new ArrayValue();
foreach ($value as $element) {
$values[] = $this->as_otlp_any_value($element);
/** @psalm-suppress InvalidArgument */
$values->getValues()[] = $this->convertAnyValue($element);
}
$result->setArrayValue(new ArrayValue(['values' => $values]));
$result->setArrayValue($values);
break;
case is_int($value):
@ -74,152 +151,69 @@ class SpanConverter implements SpanConverterInterface
return $result;
}
private function as_otlp_span_kind($kind): int
private function convertSpanKind(int $kind): int
{
switch ($kind) {
case 0: return SpanKind::SPAN_KIND_INTERNAL;
case 1: return SpanKind::SPAN_KIND_CLIENT;
case 2: return SpanKind::SPAN_KIND_SERVER;
case 3: return SpanKind::SPAN_KIND_PRODUCER;
case 4: return SpanKind::SPAN_KIND_CONSUMER;
case API\SpanKind::KIND_INTERNAL: return SpanKind::SPAN_KIND_INTERNAL;
case API\SpanKind::KIND_CLIENT: return SpanKind::SPAN_KIND_CLIENT;
case API\SpanKind::KIND_SERVER: return SpanKind::SPAN_KIND_SERVER;
case API\SpanKind::KIND_PRODUCER: return SpanKind::SPAN_KIND_PRODUCER;
case API\SpanKind::KIND_CONSUMER: return SpanKind::SPAN_KIND_CONSUMER;
}
return SpanKind::SPAN_KIND_UNSPECIFIED;
}
private function as_otlp_span(SpanDataInterface $span): CollectorSpan
private function convertStatusCode(string $status): int
{
$parent_span = $span->getParentContext();
$parent_span_id = $parent_span->isValid() ? $parent_span->getSpanId() : null;
switch ($status) {
case API\StatusCode::STATUS_UNSET: return StatusCode::STATUS_CODE_UNSET;
case API\StatusCode::STATUS_OK: return StatusCode::STATUS_CODE_OK;
case API\StatusCode::STATUS_ERROR: return StatusCode::STATUS_CODE_ERROR;
}
$row = [
'trace_id' => hex2bin($span->getTraceId()),
'span_id' => hex2bin($span->getSpanId()),
'parent_span_id' => $parent_span_id ? hex2bin($parent_span_id) : null,
'name' => $span->getName(),
'start_time_unix_nano' => $span->getStartEpochNanos(),
'end_time_unix_nano' => $span->getEndEpochNanos(),
'kind' => $this->as_otlp_span_kind($span->getKind()),
'trace_state' => (string) $span->getContext()->getTraceState(),
'dropped_attributes_count' => $span->getAttributes()->getDroppedAttributesCount(),
'dropped_events_count' => $span->getTotalDroppedEvents(),
'dropped_links_count' => $span->getTotalDroppedLinks(),
];
return StatusCode::STATUS_CODE_UNSET;
}
private function convertSpan(SpanDataInterface $span): Span
{
$pSpan = new Span();
$pSpan->setTraceId(hex2bin($span->getContext()->getTraceId()));
$pSpan->setSpanId(hex2bin($span->getContext()->getSpanId()));
$pSpan->setTraceState((string) $span->getContext()->getTraceState());
if ($span->getParentContext()->isValid()) {
$pSpan->setParentSpanId(hex2bin($span->getParentContext()->getSpanId()));
}
$pSpan->setName($span->getName());
$pSpan->setKind($this->convertSpanKind($span->getKind()));
$pSpan->setStartTimeUnixNano($span->getStartEpochNanos());
$pSpan->setEndTimeUnixNano($span->getEndEpochNanos());
$this->setAttributes($pSpan, $span->getAttributes());
foreach ($span->getEvents() as $event) {
if (!array_key_exists('events', $row)) {
$row['events'] = [];
}
$attrs = [];
foreach ($event->getAttributes() as $k => $v) {
$attrs[] = $this->as_otlp_key_value($k, $v);
}
$row['events'][] = new Event([
'time_unix_nano' => $event->getEpochNanos(),
'name' => $event->getName(),
'attributes' => $attrs,
'dropped_attributes_count' => $event->getAttributes()->getDroppedAttributesCount(),
]);
/** @psalm-suppress InvalidArgument */
$pSpan->getEvents()[] = $pEvent = new Event();
$pEvent->setTimeUnixNano($event->getEpochNanos());
$pEvent->setName($event->getName());
$this->setAttributes($pEvent, $event->getAttributes());
}
$pSpan->setDroppedEventsCount($span->getTotalDroppedEvents());
foreach ($span->getLinks() as $link) {
if (!array_key_exists('links', $row)) {
$row['links'] = [];
}
$attrs = [];
foreach ($link->getAttributes() as $k => $v) {
$attrs[] = $this->as_otlp_key_value($k, $v);
}
$row['links'][] = new Link([
'trace_id' => hex2bin($link->getSpanContext()->getTraceId()),
'span_id' => hex2bin($link->getSpanContext()->getSpanId()),
'trace_state' => (string) $link->getSpanContext()->getTraceState(),
'attributes' => $attrs,
'dropped_attributes_count' => $link->getAttributes()->getDroppedAttributesCount(),
]);
/** @psalm-suppress InvalidArgument */
$pSpan->getLinks()[] = $pLink = new Link();
$pLink->setTraceId(hex2bin($link->getSpanContext()->getTraceId()));
$pLink->setSpanId(hex2bin($link->getSpanContext()->getSpanId()));
$pLink->setTraceState((string) $link->getSpanContext()->getTraceState());
$this->setAttributes($pLink, $link->getAttributes());
}
$pSpan->setDroppedLinksCount($span->getTotalDroppedLinks());
foreach ($span->getAttributes() as $k => $v) {
if (!array_key_exists('attributes', $row)) {
$row['attributes'] = [];
}
$row['attributes'][] = $this->as_otlp_key_value($k, $v);
}
$pStatus = new Status();
$pStatus->setMessage($span->getStatus()->getDescription());
$pStatus->setCode($this->convertStatusCode($span->getStatus()->getCode()));
$pSpan->setStatus($pStatus);
$status = new Status();
switch ($span->getStatus()->getCode()) {
case API\StatusCode::STATUS_OK:
$status->setCode(StatusCode::STATUS_CODE_OK);
break;
case API\StatusCode::STATUS_ERROR:
$status->setCode(StatusCode::STATUS_CODE_ERROR)->setMessage($span->getStatus()->getDescription());
break;
default:
$status->setCode(StatusCode::STATUS_CODE_UNSET);
}
$row['status'] = $status;
return new CollectorSpan(array_filter($row));
}
// @return KeyValue[]
private function as_otlp_resource_attributes(iterable $spans): array
{
$attrs = [];
foreach ($spans as $span) {
foreach ($span->getResource()->getAttributes() as $k => $v) {
$attrs[$k] = $this->as_otlp_key_value($k, $v);
}
}
return array_values($attrs);
}
private function as_otlp_resource_span(iterable $spans): ResourceSpans
{
$isSpansEmpty = true; //Waiting for the loop to prove otherwise
$scopeKeyCache = [];
$instrumentationScopes = $convertedSpans = $schemas = [];
foreach ($spans as /** @var SpanDataInterface $span */ $span) {
$isSpansEmpty = false;
$scope = $span->getInstrumentationScope();
$isKey = $scopeKeyCache[spl_object_id($scope)] ??= KeyGenerator::generateInstanceKey($scope);
if (!isset($instrumentationScopes[$isKey])) {
$convertedSpans[$isKey] = [];
$instrumentationScopes[$isKey] = new InstrumentationScope(['name' => $scope->getName(), 'version' => $scope->getVersion() ?? '']);
$schemas[$isKey] = $scope->getSchemaUrl();
}
$convertedSpans[$isKey][] = $this->as_otlp_span($span);
}
if ($isSpansEmpty == true) {
return new ResourceSpans();
}
$isSpans = [];
foreach ($instrumentationScopes as $isKey => $scope) {
$isSpans[] = new ScopeSpans([
'scope' => $scope,
'spans' => $convertedSpans[$isKey],
'schema_url' => $schemas[$isKey] ?? '',
]);
}
return new ResourceSpans([
'resource' => new Resource([
'attributes' => $this->as_otlp_resource_attributes($spans),
]),
'scope_spans' => $isSpans,
]);
return $pSpan;
}
}

View File

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Common\Instrumentation;
use function serialize;
class KeyGenerator
{
/**
* Generate a unique key for an instance of InstrumentationScope.
*/
public static function generateInstanceKey(InstrumentationScopeInterface $instrumentationScope): string
{
return serialize([
$instrumentationScope->getName(),
$instrumentationScope->getVersion(),
$instrumentationScope->getSchemaUrl(),
$instrumentationScope->getAttributes()->toArray(),
$instrumentationScope->getAttributes()->getDroppedAttributesCount(),
]);
}
}

View File

@ -261,13 +261,21 @@ class OTLPSpanConverterTest extends TestCase
$this->assertCount(2, $result[0]->getResource()->getAttributes());
}
public function test_multiple_resources_result_in_multiple_resource_spans(): void
{
$resourceA = ResourceInfo::create(Attributes::create(['foo' => 'bar']));
$resourceB = ResourceInfo::create(Attributes::create(['foo' => 'baz']));
$converter = new SpanConverter();
$result = $converter->convert([
(new SpanData())->setResource($resourceA),
(new SpanData())->setResource($resourceB),
]);
$this->assertCount(2, $result);
}
public function test_otlp_no_spans(): void
{
$spans = [];
$row = (new SpanConverter())->convert($spans)[0];
$this->assertEquals(new ResourceSpans(), $row);
$this->assertSame([], (new SpanConverter())->convert([]));
}
/**

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Common\Instrumentation;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScope;
use OpenTelemetry\SDK\Common\Instrumentation\KeyGenerator;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Common\Instrumentation\KeyGenerator
*/
class KeyGeneratorTest extends TestCase
{
public function test_equal_instrumentation_scope_return_same_key(): void
{
$this->assertSame(
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.1', null, Attributes::create([]))),
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.1', null, Attributes::create([]))),
);
}
public function test_non_equal_instrumentation_scope_return_different_keys_name(): void
{
$this->assertNotSame(
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.1', null, Attributes::create([]))),
KeyGenerator::generateInstanceKey(new InstrumentationScope('bar', '0.0.1', null, Attributes::create([]))),
);
}
public function test_non_equal_instrumentation_scope_return_different_keys_version(): void
{
$this->assertNotSame(
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.1', null, Attributes::create([]))),
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.2', null, Attributes::create([]))),
);
}
public function test_non_equal_instrumentation_scope_return_different_keys_schemaurl(): void
{
$this->assertNotSame(
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.1', 'https://bar', Attributes::create([]))),
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.1', 'https://baz', Attributes::create([]))),
);
}
public function test_non_equal_instrumentation_scope_return_different_keys_attributes(): void
{
$this->assertNotSame(
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.1', null, Attributes::create(['foo' => 'bar']))),
KeyGenerator::generateInstanceKey(new InstrumentationScope('foo', '0.0.1', null, Attributes::create(['foo' => 'baz']))),
);
}
}