Skip to content

Intro pinpoint php aop

eeliu edited this page Aug 12, 2024 · 1 revision

English | 中文 | 한국어

Inside pinpoint-php-aop

A php library that supports pinpoint-php agent:

  1. auto inject php built-in library, such as redis,pdo, mysqli
  2. auto injection on user class, as guzzlehttp, predis

How to handle built-in library

what's built-in library, as for PHP

    PHP comes standard with many functions and constructs. There are also functions that require specific 
    PHP extensions compiled in, otherwise fatal "undefined function" errors will appear. For example, to 
    use image functions such as imagecreatetruecolor(), PHP must be compiled with GD support. Or, to use 
    mysqli_connect(), PHP must be compiled with MySQLi support. There are many core functions that are 
    included in every version of PHP, such as the string and variable functions. A call to phpinfo() 
    or get_loaded_extensions() will show which extensions are loaded into PHP. Also note that many 
    extensions are enabled by default and that the PHP manual is split up by extension. ...

> https://www.php.net/manual/en/functions.internal.php#functions.internal

By CG(class_table)

Inspired by https://www.phpinternalsbook.com/php7/extensions_design/hooks.html#overwriting-an-internal-function

PHP kernel exposes class_table that user can redirect the handler to user's function, by that we wrapper all cared functions and insert all pinpointed plugins.

Steps

  1. Support interceptor in build-in function.
// https://github.com/pinpoint-apm/pinpoint-c-agent/blob/9c544f139665dde3a9cee2a244a9c3be2f32bff9/src/PHP/pinpoint_php.cpp#L887
zend_function *func = (zend_function *)zend_hash_str_find_ptr(
      CG(function_table), ZSTR_VAL(name), ZSTR_LEN(name));
  if (func != NULL &&
      func->internal_function.handler == pinpoint_interceptor_handler_entry) {
    pp_trace("function `%s` interceptor already added", ZSTR_VAL(name));
  } else if (func != NULL) {
    pp_interceptor_v_t *interceptor =
        make_interceptor(name, before, end, exception, func);
    // insert into hash
    if (!zend_hash_add_ptr(PPG(interceptors), name, interceptor)) {
      free_interceptor(interceptor);
      pp_trace("added interceptor on `function`: %s failed. reason: already "
               "exist ",
               ZSTR_VAL(name));
      return;
    }
    func->internal_function.handler = pinpoint_interceptor_handler_entry;
    pp_trace("added interceptor on `function`: %s success", ZSTR_VAL(name));
  1. Add pinpointed plugins into injection point.
// https://github.com/pinpoint-apm/pinpoint-php-aop/blob/5994253869d516c38d528a8ef784a5c1c18b20f3/lib/Pinpoint/Plugins/SysV2/_curl/curl.php#L78
pinpoint_join_cut(
    ["curl_close"],
    function ($ch) use (&$ch_res) {
        unset($ch_res[(int) $ch]);
        pinpoint_start_trace();
        pinpoint_add_clue(PP_INTERCEPTOR_NAME, "curl_close");
        pinpoint_add_clue(PP_SERVER_TYPE, PP_PHP_METHOD);
    },
    function ($ret) {
        pinpoint_end_trace();
    },
    function ($e) {
    }
);
  1. Load all built-in plugins when needs.
// https://github.com/pinpoint-apm/pinpoint-php-aop/blob/5994253869d516c38d528a8ef784a5c1c18b20f3/lib/Pinpoint/Plugins/PinpointPerRequestPlugins.php#L126C12-L126C58
if(sampled){
    require_once __DIR__ . "/SysV2/__init__.php";
}else{
    require_once __DIR__ . "/SysV2/_curl/__init__.php";
}

How to handle user library

Before that, you should know php class loader.

By registering autoloaders, PHP is given a last chance to load the class or interface before it fails with an error.
> https://www.php.net/manual/en/language.oop5.autoload.php 

For php, when user use keyword use to import a class/function, if kernel does not find that class in class map, it will call auto_loader to find and import that file. Here is what pinpoint-php-aop intercepted.

  1. When the autoloader is ready, it modifies all autoloader function/class, intercepts all function/class when be loaded. When a intercepted class is imported, it will be mapped to new file which is rendered by pinpoint plugins.

  2. When pinpoint autoloader find that file does not rendered by pinpoint plugins, it will generate a class map and rendered class files and insert into autoloader class map. The most important, these files will be cached in cache_dir,it reused in subsequence request which can be save lots of time.

ast_loader

Let's use Pinpoint\Plugins\autoload\_MongoPlugin as an example to explain whole process.

Steps

  1. User new a mongo client
//https://github.com/pinpoint-apm/pinpoint-c-agent/blob/9c544f139665dde3a9cee2a244a9c3be2f32bff9/testapps/SimplePHP/run.php#L92-L93
 $client = new MongoDB\Client("mongodb://$mongodb_host:27017");
  1. Register pinpoint plugins on method __construct from MongoDB\Client
//https://github.com/pinpoint-apm/pinpoint-php-aop/blob/5994253869d516c38d528a8ef784a5c1c18b20f3/lib/Pinpoint/Plugins/autoload/_MongoPlugin/__init__.php#L25
$classHandler = new AspectClassHandle(\MongoDB\Client::class);
$classHandler->addJoinPoint('__construct', MongoPlugin::class);
$cls[] = $classHandler;
  1. After pinpoint rendered, you can get a new class map at /tmp/.cache/__class_index.php

default cache directory is /tmp/

$pinpoint_class_map = array('MongoDB\\Client' => '/tmp/.cache/MongoDB/Client.php', ...);
return $pinpoint_class_map;

php rendered class

//Client.php
namespace MongoDB;
class Client{
...
    // origin methods
    public function __pinpoint____construct(?string $uri = null, array $uriOptions = [], array $driverOptions = [])
    {

    }
    // rendered methods 
    public function __construct(?string $uri = null, array $uriOptions = [], array $driverOptions = [])
    {
        $_pinpoint___construct_var = new \Pinpoint\Plugins\autoload\_MongoPlugin\MongoPlugin(__METHOD__, $this, $uri, $uriOptions, $driverOptions);
        try {
            $_pinpoint___construct_var->onBefore();
            $this->__pinpoint____construct($uri, $uriOptions, $driverOptions);
            $_pinpoint___construct_var->onEnd($_pinpoint___construct_ret);
        } catch (\Exception $e) {
            $_pinpoint___construct_var->onException($e);
            throw $e;
        }
    }
...
}
  1. When rendered class (client.php) is loaded into php kernel, that means pinpoint plugins works into your project.

i.e. User class was intercept by pinpoint-php-aop