agent/php/ElasticApm/Impl/AutoInstrument/PDOAutoInstrumentation.php (213 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\DbConnectionStringParser;
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 PDO;
use PDOStatement;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class PDOAutoInstrumentation extends AutoInstrumentationBase
{
private const PDO_CLASS_NAME = 'PDO';
private const PDO_STATEMENT_CLASS_NAME = 'PDOStatement';
private const PER_OBJECT_KEYS_TO_PROPAGATE = [
DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_TYPE,
DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME,
];
/** @var Logger */
private $logger;
/** @var AutoInstrumentationUtil */
private $util;
/** @var MapPerWeakObject */
private $mapPerObject;
/** @var DbConnectionStringParser */
private $dataSourceNameParser;
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());
$this->dataSourceNameParser = new DbConnectionStringParser($tracer->loggerFactory());
}
/** @inheritDoc */
public function requiresAttachContextToExternalObjects(): bool
{
return true;
}
/** @inheritDoc */
public function name(): string
{
return InstrumentationNames::PDO;
}
/** @inheritDoc */
public function keywords(): array
{
return [InstrumentationKeywords::DB];
}
/** @inheritDoc */
public function register(RegistrationContextInterface $ctx): void
{
if (!extension_loaded('pdo')) {
return;
}
$this->interceptPDOConstruct($ctx);
$this->interceptPDOExec($ctx);
$this->interceptPDOQuery($ctx);
$this->interceptPDOPrepare($ctx);
$this->interceptPDOStatementExecute($ctx);
$this->interceptPDOMethodToSpanAsFuncCall($ctx, 'beginTransaction');
$this->interceptPDOMethodToSpanAsFuncCall($ctx, 'commit');
$this->interceptPDOMethodToSpanAsFuncCall($ctx, 'rollBack');
}
private function interceptPDOConstruct(RegistrationContextInterface $ctx): void
{
$ctx->interceptCallsToInternalMethod(
self::PDO_CLASS_NAME,
'__construct',
/**
* @param ?object $interceptedCallThis
* @param mixed[] $interceptedCallArgs
*
* @return callable
*/
function (?object $interceptedCallThis, array $interceptedCallArgs): ?callable {
if (!$this->util->verifyInstanceOf(PDO::class, $interceptedCallThis)) {
return null;
}
/** @var PDO $interceptedCallThis */
if (!$this->util->verifyMinArgsCount(1, $interceptedCallArgs)) {
return null;
}
$dsn = $interceptedCallArgs[0];
if (!$this->util->verifyIsString($dsn)) {
return null;
}
/** @var string $dsn */
/** @var ?string $dbType */
$dbType = null;
/** @var ?string $dbName */
$dbName = null;
$this->dataSourceNameParser->parse($dsn, /* ref */ $dbType, /* ref */ $dbName);
$mapToStoreForPdoObj = [];
if ($dbType !== null) {
$mapToStoreForPdoObj[DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_TYPE] = $dbType;
}
if ($dbName !== null) {
$mapToStoreForPdoObj[DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME] = $dbName;
}
$this->mapPerObject->setMultiple($interceptedCallThis, $mapToStoreForPdoObj);
return null; // no post-hook
}
);
}
private function interceptPDOMethodToSpan(RegistrationContextInterface $ctx, string $methodName, bool $isFirstArgStatement): void
{
$ctx->interceptCallsToInternalMethod(
self::PDO_CLASS_NAME,
$methodName,
/**
* @param ?object $interceptedCallThis
* @param mixed[] $interceptedCallArgs
*
* @return callable
*/
function (?object $interceptedCallThis, array $interceptedCallArgs) use ($methodName, $isFirstArgStatement): ?callable {
if (!$this->util->verifyInstanceOf(PDO::class, $interceptedCallThis)) {
return null;
}
/** @var PDO $interceptedCallThis */
$statement = null;
if ($isFirstArgStatement) {
if ($this->util->verifyMinArgsCount(1, $interceptedCallArgs) && $this->util->verifyIsString($interceptedCallArgs[0])) {
$statement = $interceptedCallArgs[0];
}
}
/** @var ?string $statement */
/** @var string $dbType */
$dbType = $this->mapPerObject->getOr($interceptedCallThis, DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_TYPE, /* defaultValue */ Constants::SPAN_SUBTYPE_UNKNOWN);
/** @var ?string $dbName */
$dbName = $this->mapPerObject->getOr($interceptedCallThis, DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME, /* defaultValue */ null);
return AutoInstrumentationUtil::createInternalFuncPostHookFromEndSpan(DbAutoInstrumentationUtil::beginDbSpan(self::PDO_CLASS_NAME, $methodName, $dbType, $dbName, $statement));
}
);
}
private function interceptPDOExec(RegistrationContextInterface $ctx): void
{
$this->interceptPDOMethodToSpan($ctx, 'exec', /* isFirstArgStatement */ true);
}
private function interceptPDOQuery(RegistrationContextInterface $ctx): void
{
$this->interceptPDOMethodToSpan($ctx, 'query', /* isFirstArgStatement */ true);
}
private function interceptPDOMethodToSpanAsFuncCall(RegistrationContextInterface $ctx, string $methodName): void
{
$this->interceptPDOMethodToSpan($ctx, $methodName, /* isFirstArgStatement */ false);
}
private function interceptPDOPrepare(RegistrationContextInterface $ctx): void
{
$ctx->interceptCallsToInternalMethod(
self::PDO_CLASS_NAME,
'prepare',
/**
* Pre-hook
*
* @param ?object $interceptedCallThis
* @param mixed[] $interceptedCallArgs
*
* @return null|callable(int, bool, mixed): void
*/
function (
?object $interceptedCallThis,
/** @noinspection PhpUnusedParameterInspection */ array $interceptedCallArgs
): ?callable {
if (!$this->util->verifyInstanceOf(PDO::class, $interceptedCallThis)) {
return null;
}
/** @var PDO $interceptedCallThis */
$keyValueMapPerObjectToPropagate = $this->mapPerObject->getMultiple(
$interceptedCallThis,
self::PER_OBJECT_KEYS_TO_PROPAGATE
);
/**
* Post-hook
*
* @param int $numberOfStackFramesToSkip
* @param bool $hasExitedByException
* @param mixed $returnValueOrThrown Return value of the intercepted call or thrown object
*/
return function (
int $numberOfStackFramesToSkip,
bool $hasExitedByException,
$returnValueOrThrown
) use (
$keyValueMapPerObjectToPropagate
): void {
if ($hasExitedByException || $returnValueOrThrown === false) {
return;
}
if (!($returnValueOrThrown instanceof PDOStatement)) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'returnValueOrThrown is not an instance of class PDOStatement',
['returnValueOrThrown' => $returnValueOrThrown]
);
return;
}
$this->mapPerObject->setMultiple($returnValueOrThrown, $keyValueMapPerObjectToPropagate);
};
}
);
}
private function interceptPDOStatementExecute(RegistrationContextInterface $ctx): void
{
$className = self::PDO_STATEMENT_CLASS_NAME;
$methodName = 'execute';
$ctx->interceptCallsToInternalMethod(
$className,
$methodName,
/**
* @param ?object $interceptedCallThis
* @param mixed[] $interceptedCallArgs
*
* @return callable
*
*/
function (
?object $interceptedCallThis,
/** @noinspection PhpUnusedParameterInspection */ array $interceptedCallArgs
) use (
$className,
$methodName
): ?callable {
if (!$this->util->verifyInstanceOf(PDOStatement::class, $interceptedCallThis)) {
return null;
}
/** @var PDOStatement $interceptedCallThis */
$statement = (
isset($interceptedCallThis->queryString)
&& $this->util->verifyIsString($interceptedCallThis->queryString)
)
? $interceptedCallThis->queryString
: null;
/** @var string $dbType */
$dbType = $this->mapPerObject->getOr(
$interceptedCallThis,
DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_TYPE,
Constants::SPAN_SUBTYPE_UNKNOWN /* <- defaultValue */
);
/** @var ?string $dbName */
$dbName = $this->mapPerObject->getOr(
$interceptedCallThis,
DbAutoInstrumentationUtil::PER_OBJECT_KEY_DB_NAME,
null /* <- defaultValue */
);
return AutoInstrumentationUtil::createInternalFuncPostHookFromEndSpan(
DbAutoInstrumentationUtil::beginDbSpan(
$className,
$methodName,
$dbType,
$dbName,
$statement
)
);
}
);
}
}