adding LocalRootSpan (#1310)

* adding LocalRootSpan class
this is based on Java's implementation, https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/v2.3.0/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/LocalRootSpan.java
and adds the ability to identify and locate the "local root span" in a trace. The local root span is the top-level active span which has either
an invalid or remote parent.
It's tracked automatically as part of making a span active, either via `Span::activate()` or `Span::storeInContext()`, and can be retrieved in
a couple of ways, but most easily via `LocalRootSpan::current()`.

* remove redundant local root span check

* move context key to api

* internal

* adding example

* mark LocalRootSpan as experimental

* adding an example of local root span usage

* style, fix broken build
This commit is contained in:
Brett McBride 2024-08-06 21:58:33 +10:00 committed by GitHub
parent 68b1b43cab
commit 10b8d9732b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 284 additions and 0 deletions

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
use OpenTelemetry\API\Trace\LocalRootSpan;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\SDK\Common\Util\ShutdownHandler;
use OpenTelemetry\SDK\Propagation\PropagatorFactory;
use OpenTelemetry\SDK\Trace\TracerProviderFactory;
putenv('OTEL_TRACES_EXPORTER=console');
putenv('OTEL_PHP_DETECTORS=none');
require_once __DIR__ . '/../../../vendor/autoload.php';
$provider = (new TracerProviderFactory())->create();
$propagator = (new PropagatorFactory())->create();
ShutdownHandler::register([$provider, 'shutdown']);
$tracer = $provider->getTracer('example');
//start and activate a root span
$root = $tracer
->spanBuilder('root')
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$rootScope = $root->activate();
//start and activate a child span
$child = $tracer
->spanBuilder('child')
->startSpan();
$childScope = $child->activate();
//update the name of the root span
LocalRootSpan::current()->updateName('updated')->setAttribute('my-attr', true);
//end spans and detach contexts
$child->end();
$childScope->detach();
$root->end();
$rootScope->detach();

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @see https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/#consumer-spans
*/
use OpenTelemetry\API\Trace\LocalRootSpan;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanContext;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SDK\Common\Util\ShutdownHandler;
use OpenTelemetry\SDK\Propagation\PropagatorFactory;
use OpenTelemetry\SDK\Trace\TracerProviderFactory;
putenv('OTEL_TRACES_EXPORTER=console');
putenv('OTEL_PHP_DETECTORS=none');
require_once __DIR__ . '/../../../vendor/autoload.php';
$provider = (new TracerProviderFactory())->create();
$propagator = (new PropagatorFactory())->create();
ShutdownHandler::register([$provider, 'shutdown']);
$tracer = $provider->getTracer('example');
// “Receive” spans SHOULD be created for operations of passing messages to the application when those operations are initiated by the application code (pull-based scenarios).
$root = $tracer
->spanBuilder('receive')
->setSpanKind(SpanKind::KIND_CONSUMER)
->startSpan();
$rootScope = $root
->storeInContext(Context::getCurrent())
->activate();
assert(LocalRootSpan::fromContext(Context::getCurrent()) === $root);
$root->addLink(SpanContext::createFromRemoteParent('fabebb164f22d4afc51df50d9a3ff629', '87c6836d8610ac6d', 768));
// “Process” spans MAY be created in addition to “Receive” spans for pull-based scenarios for operations of processing messages.
$child = $tracer
->spanBuilder('process')
->startSpan();
$childScope = $child
//->storeInContext($remoteContext) // preserve remote baggage etc.
->storeInContext(Context::getCurrent())
->activate();
$child->setAttribute('local_root', LocalRootSpan::current() === Span::getCurrent());
try {
assert(LocalRootSpan::current() === $root);
assert(LocalRootSpan::current() !== Span::getCurrent());
} finally {
$root->end();
$child->end();
$childScope->detach();
$rootScope->detach();
}

View File

@ -20,4 +20,9 @@ class LateBindingLogger implements LoggerInterface
{
($this->logger ??= ($this->factory)())->emit($logRecord);
}
public function enabled(): bool
{
return true;
}
}

View File

@ -20,4 +20,9 @@ class LateBindingTracer implements TracerInterface
{
return ($this->tracer ??= ($this->factory)())->spanBuilder($spanName);
}
public function enabled(): bool
{
return true;
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Trace;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\Context\ContextKeyInterface;
/**
* @experimental
*/
class LocalRootSpan
{
/**
* Retrieve the local root span. This is the root-most active span which has
* a remote or invalid parent.
* If there is no active local root span, then an invalid span is returned.
* @experimental
*/
public static function current(): SpanInterface
{
return self::fromContext(Context::getCurrent());
}
/**
* Retrieve the local root span from a Context.
* @experimental
*/
public static function fromContext(ContextInterface $context): SpanInterface
{
return $context->get(self::key()) ?? Span::getInvalid();
}
/**
* @internal
*/
public static function store(ContextInterface $context, SpanInterface $span): ContextInterface
{
return $context->with(self::key(), $span);
}
/**
* @internal
*/
public static function key(): ContextKeyInterface
{
static $key;
return $key ??= Context::createKey(self::class);
}
/**
* @internal
*/
public static function isLocalRoot(ContextInterface $parentContext): bool
{
$spanContext = Span::fromContext($parentContext)->getContext();
return !$spanContext->isValid() || $spanContext->isRemote();
}
}

View File

@ -54,6 +54,10 @@ abstract class Span implements SpanInterface
/** @inheritDoc */
final public function storeInContext(ContextInterface $context): ContextInterface
{
if (LocalRootSpan::isLocalRoot($context)) {
$context = LocalRootSpan::store($context, $this);
}
return $context->with(ContextKeys::span(), $this);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Integration\SDK;
use OpenTelemetry\API\Trace\LocalRootSpan;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanInterface;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SDK\Trace\TracerProvider;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;
#[CoversNothing]
class LocalRootSpanTest extends TestCase
{
private SpanInterface $span;
public function setUp(): void
{
$tracerProvider = new TracerProvider();
$this->span = $tracerProvider->getTracer('test')->spanBuilder('my-local-root-span')->startSpan();
}
public function test_active_root_span_is_local_root(): void
{
$scope = $this->span->activate();
try {
$this->assertSame($this->span, LocalRootSpan::current());
} finally {
$scope->detach();
}
$this->assertSame(Span::getInvalid(), LocalRootSpan::current(), 'root span ended, a local root span does not exist');
}
public function test_root_span_stored_in_context_is_local_root(): void
{
$root = Context::getRoot();
Context::storage()->attach($this->span->storeInContext($root));
$this->assertSame($this->span, LocalRootSpan::current());
$scope = Context::storage()->scope();
$this->assertNotNull($scope);
$this->assertSame($this->span, Span::fromContext($scope->context()));
$scope->detach();
$this->assertSame(Span::getInvalid(), LocalRootSpan::current(), 'root span ended, a local root span does not exist');
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\API\Trace;
use OpenTelemetry\API\Trace\LocalRootSpan;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanContext;
use OpenTelemetry\Context\Context;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(LocalRootSpan::class)]
class LocalRootSpanTest extends TestCase
{
public function test_span_with_remote_parent_is_local_root(): void
{
$context = Context::getRoot()->with(
LocalRootSpan::key(),
Span::wrap(
SpanContext::createFromRemoteParent(
'00000000000000000000000000000001',
'0000000000000002',
)
)
);
$this->assertTrue(LocalRootSpan::isLocalRoot($context));
}
public function test_get_local_root_span(): void
{
$span = Span::getInvalid();
$context = LocalRootSpan::store(Context::getCurrent(), $span);
$scope = $context->activate();
try {
$this->assertSame($span, LocalRootSpan::fromContext($context));
$this->assertSame($span, LocalRootSpan::current());
} finally {
$scope->detach();
}
}
public function test_get_local_root_span_when_not_set(): void
{
$context = Context::getRoot();
$scope = $context->activate();
try {
$this->assertFalse(LocalRootSpan::fromContext($context)->getContext()->isValid());
$this->assertFalse(LocalRootSpan::current()->getContext()->isValid());
$this->assertSame(Span::getInvalid(), LocalRootSpan::current());
} finally {
$scope->detach();
}
}
}