From 5276df31710e1fd12d7edfc590c4eff8f8c21120 Mon Sep 17 00:00:00 2001 From: Brett McBride Date: Fri, 23 Aug 2024 23:15:18 +1000 Subject: [PATCH] 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 --- .phan/config.php | 1 + composer.json | 1 + deptrac.yaml | 5 + 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 | 10 ++ 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 ++++++++++++++++++ 12 files changed, 352 insertions(+), 1 deletion(-) 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 ed5c34e5e..e2ef86cdc 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -380,6 +380,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 4a52f80f1..b30c221f0 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/deptrac.yaml b/deptrac.yaml index d1098460c..3cca3903c 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -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 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 52709a15c..54d3e458e 100644 --- a/src/SDK/Common/Configuration/Defaults.php +++ b/src/SDK/Common/Configuration/Defaults.php @@ -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'; } diff --git a/src/SDK/Common/Configuration/Variables.php b/src/SDK/Common/Configuration/Variables.php index f256b4526..73024a2ec 100644 --- a/src/SDK/Common/Configuration/Variables.php +++ b/src/SDK/Common/Configuration/Variables.php @@ -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'; } 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 564bb565c..672fe5890 100644 --- a/src/SDK/SdkAutoloader.php +++ b/src/SDK/SdkAutoloader.php @@ -30,6 +30,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; @@ -55,6 +56,14 @@ public static function autoload(): bool } self::registerInstrumentations(); + if (AutoRootSpan::isEnabled()) { + $request = AutoRootSpan::createRequest(); + if ($request) { + AutoRootSpan::create($request); + AutoRootSpan::registerShutdownHandler(); + } + } + return true; } @@ -228,4 +237,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 972ab9ad1..ab3807683 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(); + } +}