agent/php/ElasticApm/Impl/AutoInstrument/MySQLiAutoInstrumentation.php (390 lines of code) (raw):

<?php /* * 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 PhpComposerExtensionStubsInspection */ declare(strict_types=1); namespace Elastic\Apm\Impl\AutoInstrument; use Elastic\Apm\Impl\AutoInstrument\Util\AutoInstrumentationUtil; use Elastic\Apm\Impl\AutoInstrument\Util\DbAutoInstrumentationUtil; use Elastic\Apm\Impl\AutoInstrument\Util\MapPerWeakObject; use Elastic\Apm\Impl\Constants; use Elastic\Apm\Impl\Log\LogCategory; use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Tracer; use Elastic\Apm\Impl\Util\DbgUtil; use Elastic\Apm\SpanInterface; use mysqli; use mysqli_stmt; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ final class MySQLiAutoInstrumentation extends AutoInstrumentationBase { private const DB_TYPE = Constants::SPAN_SUBTYPE_MYSQL; private const MYSQLI_CLASS_NAME = 'mysqli'; private const MYSQLI_STMT_CLASS_NAME = 'mysqli_stmt'; private const PER_OBJECT_KEYS_TO_PROPAGATE = [DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME]; /** @var Logger */ private $logger; /** @var AutoInstrumentationUtil */ private $util; /** @var MapPerWeakObject */ private $mapPerObject; public function __construct(Tracer $tracer) { parent::__construct($tracer); $this->logger = $tracer->loggerFactory()->loggerForClass( LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__ )->addContext('this', $this); $this->util = new AutoInstrumentationUtil($tracer->loggerFactory()); $this->mapPerObject = MapPerWeakObject::create($tracer->loggerFactory()); } /** @inheritDoc */ public function requiresAttachContextToExternalObjects(): bool { return true; } /** @inheritDoc */ public function name(): string { return InstrumentationNames::MYSQLI; } /** @inheritDoc */ public function keywords(): array { return [InstrumentationKeywords::DB]; } /** @inheritDoc */ public function register(RegistrationContextInterface $ctx): void { if (!extension_loaded('mysqli')) { return; } $this->interceptMySQLiConstructConnect($ctx); $this->interceptMySQLiSelectDb($ctx); $this->interceptMySQLiFirstArgQuery($ctx, 'query'); $this->interceptMySQLiFirstArgQuery($ctx, 'multi_query'); $this->interceptMySQLiFirstArgQuery($ctx, 'real_query'); $this->interceptMySQLiPrepare($ctx); $this->interceptMySQLiStmtExecute($ctx); $this->interceptMySQLiMethodToSpanAsFuncCall($ctx, 'ping'); $this->interceptMySQLiMethodToSpanAsFuncCall($ctx, 'begin_transaction'); $this->interceptMySQLiMethodToSpanAsFuncCall($ctx, 'commit'); $this->interceptMySQLiMethodToSpanAsFuncCall($ctx, 'rollback'); // Consider capturing the argument for // $this->interceptMySQLiToSpanAsFuncCall($ctx, 'autocommit'); $this->interceptMySQLiMethodToSpanAsFuncCall($ctx, 'kill'); } private function interceptMySQLiConstructConnect(RegistrationContextInterface $ctx): void { /** * @param ?string $className * @param string $funcName * @param ?object $interceptedCallThis * @param array $interceptedCallArgs * * @return null|callable(int, bool, mixed): void */ $preHook = function ( ?string $className, string $funcName, ?object $interceptedCallThis, array $interceptedCallArgs ): ?callable { // function mysqli_connect( // $host = null, // <- $interceptedCallArgs[0] // $username = null, // <- $interceptedCallArgs[1] // $password = null, // <- $interceptedCallArgs[2] // $database = null, // <- $interceptedCallArgs[3] // $port = null, // <- $interceptedCallArgs[4] // $socket = null // <- $interceptedCallArgs[5] // ); // // public function __construct ( // $host = null, // <- $interceptedCallArgs[0] // $username = null, // <- $interceptedCallArgs[1] // $passwd = null, // <- $interceptedCallArgs[2] // $database = null, // <- $interceptedCallArgs[3] // $port = null, // <- $interceptedCallArgs[4] // $socket = null // <- $interceptedCallArgs[5] // ); /** @var ?mysqli $mysqliObj */ $mysqliObj = null; if ($interceptedCallThis !== null) { if (!$this->util->verifyInstanceOf(mysqli::class, $interceptedCallThis)) { return null; } /** @var mysqli $interceptedCallThis */ $mysqliObj = $interceptedCallThis; } /** @var ?string $dbName */ $dbName = null; if (count($interceptedCallArgs) >= 4) { $fourthArg = $interceptedCallArgs[3]; if ($fourthArg !== null) { if (is_string($fourthArg)) { $dbName = $fourthArg; } else { ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Expected 4th argument to be database name but it is not a string.', [ 'className' => $className, 'funcName' => $funcName, '4th argument type' => DbgUtil::getType($fourthArg), '4th argument' => $this->logger->possiblySecuritySensitive($fourthArg), 'interceptedCallArgs' => $this->logger->possiblySecuritySensitive($interceptedCallArgs), ] ); } } } return AutoInstrumentationUtil::createInternalFuncPostHookFromEndSpan( self::beginSpan($className, $funcName, $dbName, /* statement: */ null), /** * doBeforeSpanEnd * * @param bool $hasExitedByException * @param mixed $returnValueOrThrown */ function (bool $hasExitedByException, $returnValueOrThrown) use ($mysqliObj, $dbName): void { if ($hasExitedByException) { return; } if ($mysqliObj == null) { if (!$this->util->verifyInstanceOf(mysqli::class, $returnValueOrThrown)) { return; } /** @var mysqli $returnValueOrThrown */ $mysqliObj = $returnValueOrThrown; } $this->mapPerObject->set( $mysqliObj, DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME, $dbName ); } ); }; $funcName = 'mysqli_connect'; $ctx->interceptCallsToInternalFunction( $funcName, /** * @param mixed[] $interceptedCallArgs * * @return null|callable(int, bool, mixed): mixed */ function (array $interceptedCallArgs) use ($preHook, $funcName): ?callable { return $preHook( null /* <- className */, $funcName, null /* <- interceptedCallThis */, $interceptedCallArgs ); } ); $className = self::MYSQLI_CLASS_NAME; $methodName = '__construct'; $ctx->interceptCallsToInternalMethod( $className, $methodName, function ( ?object $interceptedCallThis, array $interceptedCallArgs ) use ( $preHook, $className, $methodName ): ?callable { return $preHook( $className, $methodName, $interceptedCallThis, $interceptedCallArgs ); } ); } private function interceptMySQLiSelectDb(RegistrationContextInterface $ctx): void { /** * @param ?string $className * @param string $funcName * @param ?object $interceptedCallThis * @param array<mixed> $interceptedCallArgs * * @return ?callable */ $preHook = function ( ?string $className, string $funcName, ?object $interceptedCallThis, array $interceptedCallArgs ): ?callable { if (!$this->util->verifyInstanceOf(mysqli::class, $interceptedCallThis)) { return null; } /** @var mysqli $interceptedCallThis */ /** @var ?string $currentDbName */ $currentDbName = $this->mapPerObject->getOr( $interceptedCallThis, DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME, null /* <- defaultValue */ ); if ( !$this->util->verifyMinArgsCount(1, $interceptedCallArgs) || !$this->util->verifyIsString($interceptedCallArgs[0]) ) { return null; } /** @var string $newDbName */ $newDbName = $interceptedCallArgs[0]; $span = self::beginSpan( $className, $funcName, $currentDbName, null /* <- statement */ ); return AutoInstrumentationUtil::createInternalFuncPostHookFromEndSpan( $span, /** * doBeforeSpanEnd * * @param bool $hasExitedByException * @param mixed $returnValueOrThrown */ function ( bool $hasExitedByException, $returnValueOrThrown ) use ( $interceptedCallThis, $newDbName, $span ): void { if ($hasExitedByException) { return; } if ($this->util->verifyIsBool($returnValueOrThrown) && $returnValueOrThrown) { DbAutoInstrumentationUtil::setServiceForDbSpan($span, self::DB_TYPE, $newDbName); $this->mapPerObject->set( $interceptedCallThis, DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME, $newDbName ); } } ); }; $this->interceptCallsTo($ctx, self::MYSQLI_CLASS_NAME, 'select_db', $preHook); } private function interceptMySQLiMethodToSpan( RegistrationContextInterface $ctx, string $methodName, bool $isFirstArgStatement ): void { $preHook = function ( ?string $className, string $funcName, ?object $interceptedCallThis, /** @noinspection PhpUnusedParameterInspection */ array $interceptedCallArgs ) use ( $isFirstArgStatement ): ?callable { /** @var ?string $dbName */ $dbName = ($interceptedCallThis !== null) ? $this->mapPerObject->getOr( $interceptedCallThis, DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME, null /* <- defaultValue */ ) : null; /** @var ?string $statement */ $statement = null; if ($isFirstArgStatement) { if ( $this->util->verifyMinArgsCount(1, $interceptedCallArgs) && $this->util->verifyIsString($interceptedCallArgs[0]) ) { $statement = $interceptedCallArgs[0]; } } return AutoInstrumentationUtil::createInternalFuncPostHookFromEndSpan( self::beginSpan( $className, $funcName, $dbName, $statement ) ); }; $this->interceptCallsTo($ctx, self::MYSQLI_CLASS_NAME, $methodName, $preHook); } private function interceptMySQLiMethodToSpanAsFuncCall(RegistrationContextInterface $ctx, string $methodName): void { $this->interceptMySQLiMethodToSpan($ctx, $methodName, /* isFirstArgStatement */ false); } private function interceptMySQLiFirstArgQuery(RegistrationContextInterface $ctx, string $methodName): void { $this->interceptMySQLiMethodToSpan($ctx, $methodName, /* isFirstArgStatement */ true); } private function interceptMySQLiPrepare(RegistrationContextInterface $ctx): void { /** * @param ?string $className * @param string $funcName * @param ?object $interceptedCallThis * @param array $interceptedCallArgs * * @return null|callable(int, bool, mixed): void */ $preHook = function ( /** @noinspection PhpUnusedParameterInspection */ ?string $className, /** @noinspection PhpUnusedParameterInspection */ string $funcName, ?object $interceptedCallThis, array $interceptedCallArgs ): ?callable { if (!$this->util->verifyInstanceOf(mysqli::class, $interceptedCallThis)) { return null; } /** @var mysqli $interceptedCallThis */ $keyValueMapPerObjectToPropagate = $this->mapPerObject->getMultiple( $interceptedCallThis, self::PER_OBJECT_KEYS_TO_PROPAGATE ); if ( $this->util->verifyMinArgsCount(1, $interceptedCallArgs) && $this->util->verifyIsString($interceptedCallArgs[0]) ) { $keyValueMapPerObjectToPropagate[DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_QUERY] = $interceptedCallArgs[0]; } return function ( int $numberOfStackFramesToSkip, bool $hasExitedByException, $returnValueOrThrown ) use ( $keyValueMapPerObjectToPropagate ): void { // We use 'instanceof mysqli_stmt' instead of verifyInstanceOf on purpose // because mysqli_prepare return type is: // mysqli_stmt|false A statement object or FALSE if an error occurred. if (!$hasExitedByException && $returnValueOrThrown instanceof mysqli_stmt) { $this->mapPerObject->setMultiple($returnValueOrThrown, $keyValueMapPerObjectToPropagate); } }; }; $this->interceptCallsTo($ctx, self::MYSQLI_CLASS_NAME, 'prepare', $preHook); } private function interceptMySQLiStmtExecute(RegistrationContextInterface $ctx): void { $className = self::MYSQLI_STMT_CLASS_NAME; $methodName = 'execute'; /** * @param ?string $className * @param string $methodName * @param ?object $interceptedCallThis * @param array $interceptedCallArgs * * @return null|callable(int, bool, mixed): void */ $preHook = function ( ?string $className, string $methodName, ?object $interceptedCallThis, /** @noinspection PhpUnusedParameterInspection */ array $interceptedCallArgs ): ?callable { if (!$this->util->verifyInstanceOf(mysqli_stmt::class, $interceptedCallThis)) { return null; } /** @var mysqli_stmt $interceptedCallThis */ /** @var ?string $dbName */ $dbName = $this->mapPerObject->getOr( $interceptedCallThis, DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME, null /* <- defaultValue */ ); /** @var ?string $query */ $query = $this->mapPerObject->getOr( $interceptedCallThis, DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_QUERY, null /* <- defaultValue */ ); return AutoInstrumentationUtil::createInternalFuncPostHookFromEndSpan( self::beginSpan( $className, $methodName, $dbName, $query /* <- statement */ ) ); }; $this->interceptCallsTo($ctx, $className, $methodName, $preHook); } private static function buildFuncName(string $className, string $methodName): string { return $className . '_' . $methodName; } /** * @param RegistrationContextInterface $ctx * @param string $className * @param string $methodName * @param callable(?string, string, ?object, mixed[]): ?callable $preHook * * @return void */ private function interceptCallsTo( RegistrationContextInterface $ctx, string $className, string $methodName, callable $preHook ): void { $funcName = self::buildFuncName($className, $methodName); $ctx->interceptCallsToInternalFunction( $className . '_' . $methodName, /** * @param array $interceptedCallArgs * * @return null|callable(int, bool, mixed): void */ function (array $interceptedCallArgs) use ($preHook, $funcName): ?callable { if (!$this->util->verifyMinArgsCount(1, $interceptedCallArgs)) { return null; } $interceptedCallThis = $interceptedCallArgs[0]; if ( $interceptedCallThis !== null && !$this->util->verifyIsObject($interceptedCallThis) ) { return null; } /** @var ?object $interceptedCallThis */ return $preHook( null /* <- className */, $funcName /* <- funcName / methodName */, $interceptedCallThis, array_slice($interceptedCallArgs, 1) /* <- interceptedCallArgs */ ); } ); $ctx->interceptCallsToInternalMethod( $className, $methodName, /** * @param ?object $interceptedCallThis * @param array $interceptedCallArgs * * @return null|callable(int, bool, mixed): void */ function ( ?object $interceptedCallThis, array $interceptedCallArgs ) use ( $className, $methodName /* <- funcName / methodName */, $preHook ): ?callable { return $preHook($className, $methodName, $interceptedCallThis, $interceptedCallArgs); } ); } private static function beginSpan( ?string $className, string $funcName, ?string $dbName, ?string $statement ): SpanInterface { return DbAutoInstrumentationUtil::beginDbSpan( $className, $funcName, self::DB_TYPE, $dbName, $statement ); } }