agent/php/ElasticApm/Impl/AutoInstrument/CurlAutoInstrumentation.php (190 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\Log\LogCategory;
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Tracer;
use Elastic\Apm\Impl\Util\DbgUtil;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class CurlAutoInstrumentation extends AutoInstrumentationBase
{
private const HANDLE_TRACKER_MAX_COUNT_HIGH_WATER_MARK = 2000;
private const HANDLE_TRACKER_MAX_COUNT_LOW_WATER_MARK = 1000;
private const CURL_INIT_ID = 1;
public const CURL_SETOPT_ID = 2;
public const CURL_SETOPT_ARRAY_ID = 3;
public const CURL_COPY_HANDLE_ID = 4;
public const CURL_EXEC_ID = 5;
private const CURL_CLOSE_ID = 6;
/** @var Logger */
private $logger;
/** @var array<int, CurlHandleTracker> */
private $handleIdToTracker = [];
public function __construct(Tracer $tracer)
{
parent::__construct($tracer);
$this->logger = $tracer->loggerFactory()->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__)->addContext('this', $this);
}
/** @inheritDoc */
public function name(): string
{
return InstrumentationNames::CURL;
}
/** @inheritDoc */
public function keywords(): array
{
return [InstrumentationKeywords::HTTP_CLIENT];
}
/** @inheritDoc */
public function register(RegistrationContextInterface $ctx): void
{
if (!extension_loaded('curl')) {
return;
}
$this->registerDelegatingToHandleTracker($ctx, 'curl_init', self::CURL_INIT_ID);
$this->registerDelegatingToHandleTracker($ctx, 'curl_setopt', self::CURL_SETOPT_ID);
$this->registerDelegatingToHandleTracker($ctx, 'curl_setopt_array', self::CURL_SETOPT_ARRAY_ID);
$this->registerDelegatingToHandleTracker($ctx, 'curl_copy_handle', self::CURL_COPY_HANDLE_ID);
$this->registerDelegatingToHandleTracker($ctx, 'curl_exec', self::CURL_EXEC_ID);
$this->registerDelegatingToHandleTracker($ctx, 'curl_close', self::CURL_CLOSE_ID);
}
public function registerDelegatingToHandleTracker(RegistrationContextInterface $ctx, string $funcName, int $funcId): void
{
$ctx->interceptCallsToInternalFunction(
$funcName,
/**
* @param mixed[] $interceptedCallArgs
*
* @return null|callable(int, bool, mixed): void
*/
function (array $interceptedCallArgs) use ($funcName, $funcId): ?callable {
return $this->preHook($funcName, $funcId, $interceptedCallArgs);
}
);
}
/**
* @param string $funcName
* @param int $funcId
* @param mixed[] $interceptedCallArgs Intercepted call arguments
*
* @return null|callable(int, bool, mixed): void
*/
private function preHook(string $funcName, int $funcId, array $interceptedCallArgs): ?callable
{
$curlHandleTracker = $this->createHandleTracker($funcName, $funcId, $interceptedCallArgs);
if ($curlHandleTracker === null) {
return null;
}
/**
* @param int $numberOfStackFramesToSkip
* @param bool $hasExitedByException
* @param mixed $returnValueOrThrown Return value of the intercepted call or thrown object
*
* @phpstan-param 0|positive-int $numberOfStackFramesToSkip
*/
return function (int $numberOfStackFramesToSkip, bool $hasExitedByException, $returnValueOrThrown) use ($funcName, $funcId, $interceptedCallArgs, $curlHandleTracker): void {
/** @var 0|positive-int $numberOfStackFramesToSkip */
$this->postHook($funcName, $funcId, $interceptedCallArgs, $curlHandleTracker, $numberOfStackFramesToSkip + 1, $hasExitedByException, $returnValueOrThrown);
};
}
/**
* @param string $dbgFuncName
* @param int $funcId
* @param mixed[] $interceptedCallArgs
* @param CurlHandleTracker $curlHandleTracker
* @param int $numberOfStackFramesToSkip
* @param bool $hasExitedByException
* @param mixed $returnValueOrThrown Return value of the intercepted call or thrown object
*
* @phpstan-param 0|positive-int $numberOfStackFramesToSkip
*/
private function postHook(
string $dbgFuncName,
int $funcId,
array $interceptedCallArgs,
CurlHandleTracker $curlHandleTracker,
int $numberOfStackFramesToSkip,
bool $hasExitedByException,
$returnValueOrThrown
): void {
AutoInstrumentationUtil::assertInterceptedCallNotExitedByException($hasExitedByException, ['functionName' => $dbgFuncName]);
switch ($funcId) {
case self::CURL_INIT_ID:
case self::CURL_COPY_HANDLE_ID:
$this->setTrackerHandle($curlHandleTracker, $returnValueOrThrown);
return;
// no need to handle self::CURL_CLOSE because null is returned in preHook
default:
$curlHandleTracker->postHook($dbgFuncName, $funcId, $numberOfStackFramesToSkip + 1, $interceptedCallArgs, $returnValueOrThrown);
}
}
private function addToHandleIdToTracker(int $handleId, CurlHandleTracker $curlHandleTracker): void
{
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Adding to curl handle ID to CurlHandleTracker map...', ['handleId' => $handleId]);
$handleIdToTrackerCount = count($this->handleIdToTracker);
if ($handleIdToTrackerCount >= self::HANDLE_TRACKER_MAX_COUNT_HIGH_WATER_MARK) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'curl handle ID to CurlHandleTracker map reached its max capacity - purging it...',
['handleIdToTrackerCount' => $handleIdToTrackerCount]
);
$this->handleIdToTracker = array_slice(
$this->handleIdToTracker,
$handleIdToTrackerCount - self::HANDLE_TRACKER_MAX_COUNT_LOW_WATER_MARK
);
}
$this->handleIdToTracker[$handleId] = $curlHandleTracker;
}
/**
* @param Logger $logger
* @param string $dbgFuncName
* @param mixed[] $interceptedCallArgs
*
* @return ?CurlHandleWrapped
*/
public static function extractCurlHandleFromArgs(Logger $logger, string $dbgFuncName, array $interceptedCallArgs): ?CurlHandleWrapped
{
if (count($interceptedCallArgs) !== 0) {
$curlHandle = $interceptedCallArgs[0];
if (CurlHandleWrapped::isValidValue($curlHandle)) {
/** @var resource|object $curlHandle */
return new CurlHandleWrapped($curlHandle);
}
}
$ctxToLog = [
'functionName' => $dbgFuncName,
'count($interceptedCallArgs)' => count($interceptedCallArgs),
];
if (count($interceptedCallArgs) !== 0) {
$ctxToLog['firstArgumentType'] = DbgUtil::getType($interceptedCallArgs[0]);
$ctxToLog['interceptedCallArgs'] = $logger->possiblySecuritySensitive($interceptedCallArgs);
}
($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Expected curl handle to be the first argument but it is not', $ctxToLog);
return null;
}
/**
* @param string $dbgFuncName
* @param mixed[] $interceptedCallArgs
*
* @return int|null
*/
private function findHandleId(string $dbgFuncName, array $interceptedCallArgs): ?int
{
$curlHandle = self::extractCurlHandleFromArgs($this->logger, $dbgFuncName, $interceptedCallArgs);
if ($curlHandle === null) {
return null;
}
$handleId = $curlHandle->asInt();
if (!array_key_exists($handleId, $this->handleIdToTracker)) {
($loggerProxy = $this->logger->ifWarningLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Not found in curl handle ID to CurlHandleTracker map', ['handleId' => $handleId]);
return null;
}
return $handleId;
}
/**
* @param string $dbgFuncName
* @param mixed[] $interceptedCallArgs
*
* @return CurlHandleTracker|null
*/
private function findHandleTracker(string $dbgFuncName, array $interceptedCallArgs): ?CurlHandleTracker
{
$handleId = $this->findHandleId($dbgFuncName, $interceptedCallArgs);
return $handleId === null ? null : $this->handleIdToTracker[$handleId];
}
/**
* @param string $dbgFuncName
* @param int $funcId
* @param mixed[] $interceptedCallArgs
*
* @return CurlHandleTracker|null
*/
public function createHandleTracker(string $dbgFuncName, int $funcId, array $interceptedCallArgs): ?CurlHandleTracker
{
switch ($funcId) {
case self::CURL_INIT_ID:
return $this->curlInitPreHook($interceptedCallArgs);
case self::CURL_COPY_HANDLE_ID:
return $this->curlCopyHandlePreHook($dbgFuncName, $interceptedCallArgs);
case self::CURL_CLOSE_ID:
$this->curlClosePreHook($dbgFuncName, $interceptedCallArgs);
return null;
default:
$curlHandleTracker = $this->findHandleTracker($dbgFuncName, $interceptedCallArgs);
if ($curlHandleTracker !== null) {
$curlHandleTracker->preHook($dbgFuncName, $funcId, $interceptedCallArgs);
}
return $curlHandleTracker;
}
}
/**
* @param mixed[] $interceptedCallArgs
*
* @return CurlHandleTracker
*/
public function curlInitPreHook(array $interceptedCallArgs): CurlHandleTracker
{
$curlHandleTracker = new CurlHandleTracker($this->tracer);
$curlHandleTracker->curlInitPreHook($interceptedCallArgs);
return $curlHandleTracker;
}
/**
* @param CurlHandleTracker $curlHandleTracker
* @param mixed $curlHandle
*/
public function setTrackerHandle(CurlHandleTracker $curlHandleTracker, $curlHandle): void
{
$handleId = $curlHandleTracker->setHandle($curlHandle);
if ($handleId !== null) {
$this->addToHandleIdToTracker($handleId, $curlHandleTracker);
}
}
/**
* @param string $dbgFuncName
* @param mixed[] $interceptedCallArgs
*
* @return CurlHandleTracker|null
*/
public function curlCopyHandlePreHook(string $dbgFuncName, array $interceptedCallArgs): ?CurlHandleTracker
{
$srcCurlHandleTracker = $this->findHandleTracker($dbgFuncName, $interceptedCallArgs);
if ($srcCurlHandleTracker === null) {
return null;
}
return $srcCurlHandleTracker->copy();
}
/**
* @param string $dbgFuncName
* @param mixed[] $interceptedCallArgs
*/
public function curlClosePreHook(string $dbgFuncName, array $interceptedCallArgs): void
{
$handleId = $this->findHandleId($dbgFuncName, $interceptedCallArgs);
if ($handleId === null) {
return;
}
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Removing from curl handle ID to CurlHandleTracker map...', ['handleId' => $handleId]);
unset($this->handleIdToTracker[$handleId]);
}
}