From f38cbe74634bbb3306231f8599a946d9f17e468e Mon Sep 17 00:00:00 2001 From: Brett McBride Date: Tue, 23 Jul 2024 14:09:36 +1000 Subject: [PATCH] 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 --- .phan/config.php | 1 + composer.json | 1 + examples/traces/features/auto_root_span.php | 22 +++ src/SDK/Common/Configuration/Defaults.php | 1 + src/SDK/Common/Configuration/Variables.php | 1 + src/SDK/Common/Util/ShutdownHandler.php | 2 +- src/SDK/SdkAutoloader.php | 30 +--- src/SDK/Trace/AutoRootSpan.php | 108 +++++++++++++ src/SDK/composer.json | 1 + .../Trace/test_auto_root_span_creation.phpt | 56 +++++++ tests/Unit/SDK/Trace/AutoRootSpanTest.php | 145 ++++++++++++++++++ 11 files changed, 345 insertions(+), 23 deletions(-) create mode 100644 examples/traces/features/auto_root_span.php create mode 100644 src/SDK/Trace/AutoRootSpan.php create mode 100644 tests/Integration/SDK/Trace/test_auto_root_span_creation.phpt create mode 100644 tests/Unit/SDK/Trace/AutoRootSpanTest.php diff --git a/.phan/config.php b/.phan/config.php index 7914c189a..29abf7a91 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -378,6 +378,7 @@ '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 diff --git a/composer.json b/composer.json index 40567c6eb..f0251c412 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "require": { "php": "^8.1", "google/protobuf": "^3.22", + "nyholm/psr7-server": "^1.1", "php-http/discovery": "^1.14", "psr/http-client": "^1.0", "psr/http-client-implementation": "^1.0", diff --git a/examples/traces/features/auto_root_span.php b/examples/traces/features/auto_root_span.php new file mode 100644 index 000000000..5ac62379b --- /dev/null +++ b/examples/traces/features/auto_root_span.php @@ -0,0 +1,22 @@ +getLogger('test')->emit(new LogRecord('I processed a request')); +echo 'hello world!' . PHP_EOL; diff --git a/src/SDK/Common/Configuration/Defaults.php b/src/SDK/Common/Configuration/Defaults.php index fcfea6e4e..f61c5aca3 100644 --- a/src/SDK/Common/Configuration/Defaults.php +++ b/src/SDK/Common/Configuration/Defaults.php @@ -119,4 +119,5 @@ 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'; } diff --git a/src/SDK/Common/Configuration/Variables.php b/src/SDK/Common/Configuration/Variables.php index 8ccb28ac1..b18a59406 100644 --- a/src/SDK/Common/Configuration/Variables.php +++ b/src/SDK/Common/Configuration/Variables.php @@ -140,4 +140,5 @@ 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'; } diff --git a/src/SDK/Common/Util/ShutdownHandler.php b/src/SDK/Common/Util/ShutdownHandler.php index d748c3e81..481c23662 100644 --- a/src/SDK/Common/Util/ShutdownHandler.php +++ b/src/SDK/Common/Util/ShutdownHandler.php @@ -72,7 +72,7 @@ private static function registerShutdownFunction(): void // 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); diff --git a/src/SDK/SdkAutoloader.php b/src/SDK/SdkAutoloader.php index f204d8540..8803b3013 100644 --- a/src/SDK/SdkAutoloader.php +++ b/src/SDK/SdkAutoloader.php @@ -15,6 +15,7 @@ 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; @@ -64,31 +65,15 @@ public static function autoload(): bool ; }); - return true; - } - - /** - * Test whether a request URI is set, and if it matches the excluded urls configuration option - * - * @internal - */ - public static function isIgnoredUrl(): bool - { - $ignoreUrls = Configuration::getList(Variables::OTEL_PHP_EXCLUDED_URLS, []); - if ($ignoreUrls === []) { - return false; - } - $url = $_SERVER['REQUEST_URI'] ?? null; - if (!$url) { - return false; - } - foreach ($ignoreUrls as $ignore) { - if (preg_match(sprintf('|%s|', $ignore), (string) $url) === 1) { - return true; + if (AutoRootSpan::isEnabled()) { + $request = AutoRootSpan::createRequest(); + if ($request) { + AutoRootSpan::create($request); + AutoRootSpan::registerShutdownHandler(); } } - return false; + return true; } /** @@ -129,4 +114,5 @@ public static function isExcludedUrl(): bool return false; } + } diff --git a/src/SDK/Trace/AutoRootSpan.php b/src/SDK/Trace/AutoRootSpan.php new file mode 100644 index 000000000..ed4f6eb7f --- /dev/null +++ b/src/SDK/Trace/AutoRootSpan.php @@ -0,0 +1,108 @@ +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(); + } +} diff --git a/src/SDK/composer.json b/src/SDK/composer.json index 1c16a0046..df1c82c64 100644 --- a/src/SDK/composer.json +++ b/src/SDK/composer.json @@ -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", diff --git a/tests/Integration/SDK/Trace/test_auto_root_span_creation.phpt b/tests/Integration/SDK/Trace/test_auto_root_span_creation.phpt new file mode 100644 index 000000000..65b896c7d --- /dev/null +++ b/tests/Integration/SDK/Trace/test_auto_root_span_creation.phpt @@ -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-- + +--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" + } +] diff --git a/tests/Unit/SDK/Trace/AutoRootSpanTest.php b/tests/Unit/SDK/Trace/AutoRootSpanTest.php new file mode 100644 index 000000000..5c75acdc9 --- /dev/null +++ b/tests/Unit/SDK/Trace/AutoRootSpanTest.php @@ -0,0 +1,145 @@ +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(); + } +}