auto root span creation (#1354)

* auto root span creation
proof of concept for automatically creating a root span on startup.
the obvious deficiencies are:
- no idea of response values (status code etc)
- does not capture exceptions

* deptrac
This commit is contained in:
Brett McBride 2024-08-23 23:15:18 +10:00 committed by GitHub
parent 69825d395a
commit 5276df3171
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 352 additions and 1 deletions

View File

@ -380,6 +380,7 @@ return [
'vendor/phpunit/phpunit/src',
'vendor/google/protobuf/src',
'vendor/ramsey/uuid/src',
'vendor/nyholm/psr7-server/src',
],
// A list of individual files to include in analysis

View File

@ -9,6 +9,7 @@
"require": {
"php": "^8.1",
"google/protobuf": "^3.22 || ^4.0",
"nyholm/psr7-server": "^1.1",
"php-http/discovery": "^1.14",
"psr/http-client": "^1.0",
"psr/http-client-implementation": "^1.0",

View File

@ -105,6 +105,10 @@ deptrac:
collectors:
- type: className
regex: ^Ramsey\\Uuid\\*
- name: NyholmPsr7Server
collectors:
- type: className
regex: ^Nyholm\\Psr7Server\\*
ruleset:
Context:
@ -134,6 +138,7 @@ deptrac:
- HttpClients
- SPI
- RamseyUuid
- NyholmPsr7Server
Contrib:
- +SDK
- +OtelProto

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Example;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Logs\LogRecord;
putenv('OTEL_PHP_AUTOLOAD_ENABLED=true');
putenv('OTEL_TRACES_EXPORTER=console');
putenv('OTEL_METRICS_EXPORTER=none');
putenv('OTEL_LOGS_EXPORTER=console');
putenv('OTEL_PROPAGATORS=tracecontext');
putenv('OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN=true');
//Usage: php -S localhost:8080 examples/traces/features/auto_root_span.php
require dirname(__DIR__, 3) . '/vendor/autoload.php';
Globals::loggerProvider()->getLogger('test')->emit(new LogRecord('I processed a request'));
echo 'hello world!' . PHP_EOL;

View File

@ -119,5 +119,6 @@ interface Defaults
public const OTEL_PHP_DISABLED_INSTRUMENTATIONS = [];
public const OTEL_PHP_LOGS_PROCESSOR = 'batch';
public const OTEL_PHP_LOG_DESTINATION = 'default';
public const OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN = 'false';
public const OTEL_EXPERIMENTAL_CONFIG_FILE = 'sdk-config.yaml';
}

View File

@ -140,5 +140,6 @@ interface Variables
public const OTEL_PHP_INTERNAL_METRICS_ENABLED = 'OTEL_PHP_INTERNAL_METRICS_ENABLED'; //whether the SDK should emit its own metrics
public const OTEL_PHP_DISABLED_INSTRUMENTATIONS = 'OTEL_PHP_DISABLED_INSTRUMENTATIONS';
public const OTEL_PHP_EXCLUDED_URLS = 'OTEL_PHP_EXCLUDED_URLS';
public const OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN = 'OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN';
public const OTEL_EXPERIMENTAL_CONFIG_FILE = 'OTEL_EXPERIMENTAL_CONFIG_FILE';
}

View File

@ -72,7 +72,7 @@ final class ShutdownHandler
// Push shutdown to end of queue
// @phan-suppress-next-line PhanTypeMismatchArgumentInternal
register_shutdown_function(static function (array $handlers): void {
foreach ($handlers as $handler) {
foreach (array_reverse($handlers) as $handler) {
$handler();
}
}, $handlers);

View File

@ -30,6 +30,7 @@ use OpenTelemetry\SDK\Logs\LoggerProviderFactory;
use OpenTelemetry\SDK\Metrics\MeterProviderFactory;
use OpenTelemetry\SDK\Propagation\PropagatorFactory;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Trace\AutoRootSpan;
use OpenTelemetry\SDK\Trace\ExporterFactory;
use OpenTelemetry\SDK\Trace\SamplerFactory;
use OpenTelemetry\SDK\Trace\SpanProcessorFactory;
@ -55,6 +56,14 @@ class SdkAutoloader
}
self::registerInstrumentations();
if (AutoRootSpan::isEnabled()) {
$request = AutoRootSpan::createRequest();
if ($request) {
AutoRootSpan::create($request);
AutoRootSpan::registerShutdownHandler();
}
}
return true;
}
@ -228,4 +237,5 @@ class SdkAutoloader
return false;
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Trace;
use Http\Discovery\Exception\NotFoundException;
use Http\Discovery\Psr17FactoryDiscovery;
use Nyholm\Psr7Server\ServerRequestCreator;
use OpenTelemetry\API\Behavior\LogsMessagesTrait;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Common\Util\ShutdownHandler;
use OpenTelemetry\SemConv\TraceAttributes;
use OpenTelemetry\SemConv\Version;
use Psr\Http\Message\ServerRequestInterface;
class AutoRootSpan
{
use LogsMessagesTrait;
public static function isEnabled(): bool
{
return
!empty($_SERVER['REQUEST_METHOD'] ?? null)
&& Configuration::getBoolean(Variables::OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN);
}
/**
* @psalm-suppress ArgumentTypeCoercion
* @internal
*/
public static function create(ServerRequestInterface $request): void
{
$tracer = Globals::tracerProvider()->getTracer(
'io.opentelemetry.php.auto-root-span',
null,
Version::VERSION_1_25_0->url(),
);
$parent = Globals::propagator()->extract($request->getHeaders());
$startTime = array_key_exists('REQUEST_TIME_FLOAT', $request->getServerParams())
? $request->getServerParams()['REQUEST_TIME_FLOAT']
: (int) microtime(true);
$span = $tracer->spanBuilder($request->getMethod())
->setSpanKind(SpanKind::KIND_SERVER)
->setStartTimestamp((int) ($startTime*1_000_000))
->setParent($parent)
->setAttribute(TraceAttributes::URL_FULL, (string) $request->getUri())
->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $request->getMethod())
->setAttribute(TraceAttributes::HTTP_REQUEST_BODY_SIZE, $request->getHeaderLine('Content-Length'))
->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $request->getHeaderLine('User-Agent'))
->setAttribute(TraceAttributes::SERVER_ADDRESS, $request->getUri()->getHost())
->setAttribute(TraceAttributes::SERVER_PORT, $request->getUri()->getPort())
->setAttribute(TraceAttributes::URL_SCHEME, $request->getUri()->getScheme())
->setAttribute(TraceAttributes::URL_PATH, $request->getUri()->getPath())
->startSpan();
Context::storage()->attach($span->storeInContext($parent));
}
/**
* @internal
*/
public static function createRequest(): ?ServerRequestInterface
{
assert(array_key_exists('REQUEST_METHOD', $_SERVER) && !empty($_SERVER['REQUEST_METHOD']));
try {
$creator = new ServerRequestCreator(
Psr17FactoryDiscovery::findServerRequestFactory(),
Psr17FactoryDiscovery::findUriFactory(),
Psr17FactoryDiscovery::findUploadedFileFactory(),
Psr17FactoryDiscovery::findStreamFactory(),
);
return $creator->fromGlobals();
} catch (NotFoundException $e) {
self::logError('Unable to initialize server request creator for auto root span creation', ['exception' => $e]);
}
return null;
}
/**
* @internal
*/
public static function registerShutdownHandler(): void
{
ShutdownHandler::register(self::shutdownHandler(...));
}
/**
* @internal
*/
public static function shutdownHandler(): void
{
$scope = Context::storage()->scope();
if (!$scope) {
return;
}
$scope->detach();
$span = Span::fromContext($scope->context());
$span->end();
}
}

View File

@ -19,6 +19,7 @@
"require": {
"php": "^8.1",
"ext-json": "*",
"nyholm/psr7-server": "^1.1",
"open-telemetry/api": "~1.0 || ~1.1",
"open-telemetry/context": "^1.0",
"open-telemetry/sem-conv": "^1.0",

View File

@ -0,0 +1,56 @@
--TEST--
Auto root span creation
--ENV--
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN=true
OTEL_TRACES_EXPORTER=console
OTEL_METRICS_EXPORTER=none
OTEL_LOGS_EXPORTER=console
REQUEST_METHOD=GET
REQUEST_URI=/foo?bar=baz
REQUEST_SCHEME=https
SERVER_NAME=example.com
SERVER_PORT=8080
HTTP_HOST=example.com:8080
HTTP_USER_AGENT=my-user-agent/1.0
REQUEST_TIME_FLOAT=1721706151.242976
HTTP_TRACEPARENT=00-ff000000000000000000000000000041-ff00000000000041-01
--FILE--
<?php
require_once 'vendor/autoload.php';
?>
--EXPECTF--
[
{
"name": "GET",
"context": {
"trace_id": "ff000000000000000000000000000041",
"span_id": "%s",
"trace_state": "",
"trace_flags": 1
},
"resource": {%A
},
"parent_span_id": "ff00000000000041",
"kind": "KIND_SERVER",
"start": 1721706151242976,
"end": %d,
"attributes": {
"url.full": "%s",
"http.request.method": "GET",
"http.request.body.size": "",
"user_agent.original": "my-user-agent\/1.0",
"server.address": "%S",
"server.port": %d,
"url.scheme": "https",
"url.path": "\/foo"
},
"status": {
"code": "Unset",
"description": ""
},
"events": [],
"links": [],
"schema_url": "%s"
}
]

View File

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\SDK\Trace;
use Nyholm\Psr7\ServerRequest;
use OpenTelemetry\API\Instrumentation\Configurator;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanBuilderInterface;
use OpenTelemetry\API\Trace\SpanInterface;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\TracerInterface;
use OpenTelemetry\API\Trace\TracerProviderInterface;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\Context\ContextKeys;
use OpenTelemetry\Context\ScopeInterface;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Trace\AutoRootSpan;
use OpenTelemetry\Tests\TestState;
use PHPUnit\Framework\Attributes\BackupGlobals;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
#[CoversClass(AutoRootSpan::class)]
class AutoRootSpanTest extends TestCase
{
use TestState;
/** @var (TracerInterface&MockObject) */
private TracerInterface $tracer;
private ScopeInterface $scope;
public function setUp(): void
{
$tracerProvider = $this->createMock(TracerProviderInterface::class);
$this->tracer = $this->createMock(TracerInterface::class);
$tracerProvider->method('getTracer')->willReturn($this->tracer);
$this->scope = Configurator::create()
->withTracerProvider($tracerProvider)
->withPropagator(new TraceContextPropagator())
->activate();
}
public function tearDown(): void
{
$this->scope->detach();
}
#[BackupGlobals(true)]
#[DataProvider('enabledProvider')]
public function test_is_enabled(string $enabled, ?string $method, bool $expected): void
{
$this->setEnvironmentVariable(Variables::OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN, $enabled);
$_SERVER['REQUEST_METHOD'] = $method;
$this->assertSame($expected, AutoRootSpan::isEnabled());
}
public static function enabledProvider(): array
{
return [
['true', 'GET', true],
['true', null, false],
['true', '', false],
['false', 'GET', false],
];
}
#[BackupGlobals(true)]
public function test_create_request(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/foo';
$request = AutoRootSpan::createRequest();
$this->assertNotNull($request);
$this->assertSame('GET', $request->getMethod());
$this->assertSame('/foo', $request->getUri()->getPath());
}
public function test_create(): void
{
$body = 'hello otel';
$traceId = 'ff000000000000000000000000000041';
$spanId = 'ff00000000000041';
$traceParent = '00-' . $traceId . '-' . $spanId . '-01';
$request = new ServerRequest('POST', 'https://example.com/foo?bar=baz', ['traceparent' => $traceParent], $body);
$spanBuilder = $this->createMock(SpanBuilderInterface::class);
$spanBuilder
->expects($this->once())
->method('setSpanKind')
->with($this->equalTo(SpanKind::KIND_SERVER))
->willReturnSelf();
$spanBuilder
->expects($this->once())
->method('setStartTimestamp')
->willReturnSelf();
$spanBuilder
->expects($this->once())
->method('setParent')
->with($this->callback(function (ContextInterface $parent) use ($traceId, $spanId) {
$span = Span::fromContext($parent);
$this->assertSame($traceId, $span->getContext()->getTraceId());
$this->assertSame($spanId, $span->getContext()->getSpanId());
return true;
}))
->willReturnSelf();
$spanBuilder
->expects($this->atLeast(8))
->method('setAttribute')
->willReturnSelf();
$this->tracer
->expects($this->once())
->method('spanBuilder')
->with($this->equalTo('POST'))
->willReturn($spanBuilder);
AutoRootSpan::create($request);
$scope = Context::storage()->scope();
$this->assertNotNull($scope);
$scope->detach();
}
public function test_shutdown_handler(): void
{
$this->setEnvironmentVariable('OTEL_PHP_DEBUG_SCOPES_DISABLED', 'true');
$span = $this->createMock(SpanInterface::class);
$span
->expects($this->once())
->method('end');
Context::getCurrent()->with(ContextKeys::span(), $span)->activate();
AutoRootSpan::shutdownHandler();
}
}