prod/php/ElasticOTel/Traces/ElasticRootSpan.php (159 lines of code) (raw):

<?php /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** @noinspection PhpIllegalPsrClassPathInspection */ declare(strict_types=1); namespace Elastic\OTel\Traces; use Elastic\OTel\Util\ArrayUtil; use Elastic\OTel\Util\TextUtil; use Http\Discovery\Exception\NotFoundException; use Http\Discovery\Psr17FactoryDiscovery; use Nyholm\Psr7Server\ServerRequestCreator; use OpenTelemetry\API\Globals; use OpenTelemetry\API\Behavior\LogsMessagesTrait; use OpenTelemetry\API\Trace\Span; use OpenTelemetry\API\Trace\SpanKind; use OpenTelemetry\Context\Context; use OpenTelemetry\SDK\Common\Configuration\Configuration; use OpenTelemetry\SDK\Common\Util\ShutdownHandler; use OpenTelemetry\SemConv\TraceAttributes; use OpenTelemetry\SemConv\Version; use Psr\Http\Message\ServerRequestInterface; use Elastic\OTel\Util\WildcardListMatcher; class ElasticRootSpan { use LogsMessagesTrait; private const DEFAULT_SPAN_NAME_FOR_SCRIPT = '<script>'; private static function isCliSapi(): bool { return php_sapi_name() === 'cli'; } public static function startRootSpan(?callable $notifySpanEnded): void { if (self::isCliSapi()) { if (!Configuration::getBoolean('ELASTIC_OTEL_TRANSACTION_SPAN_ENABLED_CLI', true)) { self::logDebug('ELASTIC_OTEL_TRANSACTION_SPAN_ENABLED_CLI set to false'); return; } } elseif (!Configuration::getBoolean('ELASTIC_OTEL_TRANSACTION_SPAN_ENABLED', true)) { self::logDebug('ELASTIC_OTEL_TRANSACTION_SPAN_ENABLED set to false'); return; } $request = self::createRequest(); if ($request) { self::create($request); self::registerShutdownHandler($request, $notifySpanEnded); } else { self::logWarning('Unable to create server request'); } } private static function getStartTime(ServerRequestInterface $request): float { if (ArrayUtil::getValueIfKeyExists('REQUEST_TIME_FLOAT', $request->getServerParams(), /* out */ $serverRequestTime)) { if (is_float($serverRequestTime)) { return $serverRequestTime; } if (is_string($serverRequestTime)) { return floatval($serverRequestTime); } } return microtime(true); } /** * @psalm-suppress ArgumentTypeCoercion * @internal */ private static function create(ServerRequestInterface $request): void { $tracer = Globals::tracerProvider()->getTracer( 'co.elastic.php.elastic-root-span', null, Version::VERSION_1_25_0->url(), ); $parent = Globals::propagator()->extract($request->getHeaders()); $spanBuilder = $tracer->spanBuilder(self::getSpanName($request)) ->setSpanKind(SpanKind::KIND_SERVER) ->setStartTimestamp((int) (self::getStartTime($request) * 1_000_000_000)) ->setParent($parent); if (!self::isCliSapi()) { $spanBuilder->setAttributes( [ TraceAttributes::URL_FULL => strval($request->getUri()), TraceAttributes::HTTP_REQUEST_METHOD => $request->getMethod(), TraceAttributes::HTTP_REQUEST_BODY_SIZE => $request->getHeaderLine('Content-Length'), TraceAttributes::USER_AGENT_ORIGINAL => $request->getHeaderLine('User-Agent'), TraceAttributes::SERVER_ADDRESS => $request->getUri()->getHost(), TraceAttributes::SERVER_PORT => $request->getUri()->getPort(), TraceAttributes::URL_SCHEME => $request->getUri()->getScheme(), TraceAttributes::URL_PATH => $request->getUri()->getPath(), ] ); } $span = $spanBuilder->startSpan(); Context::storage()->attach($span->storeInContext($parent)); } /** * @internal */ private static function createRequest(): ?ServerRequestInterface { 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 */ private static function registerShutdownHandler(ServerRequestInterface $request, ?callable $notifySpanEnded): void { $shutdownFunc = function () use ($request, $notifySpanEnded) { if ($notifySpanEnded) { $notifySpanEnded(); } self::shutdownHandler($request); }; ShutdownHandler::register($shutdownFunc(...)); } /** * @internal */ public static function shutdownHandler(ServerRequestInterface $request): void { $scope = Context::storage()->scope(); if (!$scope) { self::logDebug('Root span not created or ended too early'); return; } $scope->detach(); $span = Span::fromContext($scope->context()); if (is_int(http_response_code())) { $span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, http_response_code()); } elseif (ArrayUtil::getValueIfKeyExists('REDIRECT_STATUS', $request->getServerParams(), /* out */ $redirectStatus)) { if (is_int($redirectStatus)) { $span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $redirectStatus); } } $span->end(); } private static function getOptionalServerVarElement(string $key): mixed { /** @noinspection PhpIssetCanBeReplacedWithCoalesceInspection */ return isset($_SERVER[$key]) ? $_SERVER[$key] : null; } /** * @return non-empty-string */ private static function getSpanName(ServerRequestInterface $request): string { if (php_sapi_name() === 'cli') { if (is_string($scriptName = self::getOptionalServerVarElement('SCRIPT_NAME'))) { $processedScriptName = self::processPathMatchers($scriptName); return TextUtil::isEmptyString($processedScriptName) ? self::DEFAULT_SPAN_NAME_FOR_SCRIPT : $processedScriptName; } else { return self::DEFAULT_SPAN_NAME_FOR_SCRIPT; } } $method = $request->getMethod(); $path = $request->getUri()->getPath(); return $method . ' ' . self::processPathMatchers($path); } private static function processPathMatchers(string $path): string { /** @var string[] $groups */ $groups = Configuration::getList('ELASTIC_OTEL_TRANSACTION_URL_GROUPS', []); if (count($groups) == 0) { return $path; } $matcher = new WildcardListMatcher($groups); return $matcher->match($path) ?? $path; } }