Skip to content

laokiea/eapm-php

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EAPM-PHP

A php agent for elastic-apm

安装

composer require laokiea/eapm-php

基本用法

示例代码在最后

初始化agent

$agent = new EApmPhp\EApmComposer();

也可以使用单例模式

$agent = EApmPhp\EApmComposer::getAgent();

APM相关配置

SERVER_URL:APM地址

SECRET_TOKEN:提交请求所需要的token

SERVICE_NAME: 使用APM的服务名称,例如Frontend

上述配置在使用中,可以配置到系统的环境变量里,格式:ELASTIC_APM_xxx,例如:ELASTIC_APM_SERVER_URL

其他相关配置查看 APM接入文档

代码
$configure = new \EApmPhp\EApmConfigure(
    "your_apm_server_url_here",
    "your_apm_secret_token_here",
    "your_apm_service_name_here"
);
$agent = new EApmPhp\EApmComposer();
$agent->setConfigure($configure);

如果apm的配置写到了环境变量中,那么上面的代码可以改成如下:

$configure = new \EApmPhp\EApmConfigure();
$agent = new EApmPhp\EApmComposer();
$agent->setConfigure($configure);

服务相关配置

设置debug模式
//设置debug模式,一些错误会直接输出或者记录到日志中
$agent->setAppConfig("debug", true);

可以查看日志小节

设置用户uid
// 设置用户id可以在APM面板上直接粘贴搜索
$agent->setUserId(1000);
设置采样率
// 设置采样率可以控制日志记录的数量
$agent->setSampleRate(0.5);
接入
$agent->EApmUse();

建议在项目入口文件,比如index.php中进行agent的初始化及接入。

EApmUse可以接受一个匿名函数,当作中间件传入agent中,以Frontend为例: 经典的匿名函数,可以改成以下

$invoke_func = function() use ($_INVOKING_FILE_) {
    require_once($_INVOKING_FILE_);
};
$agent->EApmUse($invoke_func);

这样可以在入口处直接调用agent现有的中间件,当然你可以在真正使用到的地方再调用 $agent->EApmUse();

启动一个Transaction(会话)

Transaction(会话)代表着一系列Http/DB/Grpc等操作的集合

$transaction = $agent->startNewTransaction("POST /avatar/{uid}/upload", "request");

startNewTransaction方法接受name和type参数,这两个参数标示着一次会话的特征,比如在一次HTTP请求中,逻辑是处理用户上传头像,那么name和type可以是

name: POST /avatar/{uid}/upload(也可以直接用中文:用户上传头像,好处是在面板上可以一目了然)

⚠️如果请求地址中有ID,token等不同值的参数,比如uid,那么用{uid}代替,其他类型的参数也一样,这样在APM面板上不会产生很多的会话名称。

type: http.request

结束一个会话
$transaction->end();

会话(包括其他的Event,比如Span,Error)可以手动结束,像上面这样,使用中我们可以不写这行代码,agent会自动帮我们处理 如果你正在使用swoole作为服务,务必在合适的地方手动调用end

发送所有的会话信息
$agent->eventsPush();

和结束会话类似,我们不需要手动操作,agent会自动帮我们发送。 如果你正在使用swoole作为服务,务必在合适的地方手动调用eventPush*

添加Span

Span和会话类似,也是代表一种操作,只不过更具体。下面具体Http/DB等操作添加Span为例

DB Span

当我们操作数据库时,实际都是数据在网络环境中的传输,往往一个TCP请求会影响整个HTTP服务的性能,所以DB的操作非常值得去记录追踪

添加Mysql Span

代码
$mysqlSpan = $agent->startNewSpan("SELECT", "db.mysql", "adm", $transaction);

$result = $mysqlSpan->startMysqlTypeSpan(getMysqlInstance(), "select * from adm where type = 1 and status = 1");

var_dump($result);

首先调用startNewSpan实例化一个任意类型的Span,接受四个参数: name:和transaction的name类似

type:和transaction的type类似

sub_type:多一个子类型,更加具体的描述操作

parent: 父节点,大部分情况是上面实例化的transaction,但是也可以是其他的span

⚠️对于Mysql操作,上面参数的值有以下建议:

  1. name传入SELECT/UPDATE/DELETE操作名
  2. type以点分隔符,记录操作的数据库类型,例如db.mysql
  3. sub_type以点分隔符,记录数据库及表名,例如adm

startMysqlTypeSpan方法表示进行具体的mysql操作,这里agent做了一个封装 可以传入实例化后的mysqli/PDO对象以及需要执行的sql语句,函数会将执行后的结果返回。

⚠️如果函数执行失败,方法会返回false,有必要在使用时进行false结果的判断

⚠️需要注意的是,传入mysqli实例之前,必须已经调用select_db方法

添加Redis Span

代码
$redisSpan = $agent->startNewSpan("ZREVRANGE", "db.redis", "hermes", $transaction);
$result = $redisSpan->startRedisTypeSpan(getRedisInstance(), "ZREVRANGE", "u:9336644:sessions", 0, 199, true);
var_dump($result);

$redisSpan = $agent->startNewSpan("HGET", "db.redis", "hermes", $transaction);
$result = $redisSpan->startRedisTypeSpan(getRedisInstance(), "HGET", "u:9336644:got_cursor", "2_15261526-9225711");
var_dump($result);

与上面创建Mysql Span类似,也是先实例化一个任意类型的Span,然后利用封装好的startRedisTypeSpan方法完成命令执行。

⚠️对于Redis操作,上面参数的值有以下建议:

  1. name传入ZREVRANGE/HGET命令名
  2. type以点分隔符,记录操作的数据库类型,例如db.redis
  3. sub_type以点分隔符,记录db实例名,例如hermes
  4. startRedisTypeSpan的参数从第三个开始,和执行命令的参数保持一致即可。

⚠️如果函数执行失败,方法会返回false,有必要在使用时进行false结果的判断

HTTP Span

见分布式追踪章节

消息队列操作

还有一种常见的数据息在网络间传播的操作就是消息队列的生产和消费

代码
$publishSpan = $agent->startNewSpan("Publish", "kafka.nezha", "topic-publish", $transaction);
// 这里是你的生产代码
// xxxx
$publishSpan->setMessageQueueSpanContext("publish data body");

$consumeSpan = $agent->startNewSpan("Consume", "rabbitMQ", "topic-consume", $transaction);
// 这里是你的消费代码
// xxxx
$consumeSpan->setMessageQueueSpanContext("consume data body");

// 如果是消费,这里是你的处理消息的代码
// xxxx

如上,先创建一个任意类型的Span,和db及http Span不同的是,agent没有封装生产消费的逻辑,但是消息队列操作的Span可以调用setMessageQueueSpanContext方法设置context

⚠️对于消息队列的操作,上面参数的值有以下建议:

  1. name传入Publish/Consume表示生产还是消费
  2. type以点分隔符,记录操作的消息队列种类, 可以带上额外的实例名,例如kafka.nezha, rabbitMQ
  3. sub_type表示消费的queue/Topic等,例如pns-sender-topic

setMessageQueueSpanContext方法接受生产或者消费到的消息体作为参数(如果生产或者消费多条消息,可以传空字符串或者最后一条消息的内容)

分布式追踪

一个完整的HTTP请求,可能会经过多个微服务或者其他第三方服务。 例如在Frontend里调用user-profile,广告,用户关系等微服务,那么可以在以上的微服务中也接入eapm-php,达到分布式追踪的效果。 agent会自动处理分布式追踪中的trace信息。 当然如果微服务里没有接入agent或者访问是第三方api,也没关系,agent也会记录该次请求。

还是以代码为示例:

假设Frontend里有代码如下:

$httpSpan = $agent->startNewSpan("GET user_profile", "request.microservice", "user_profile", $transaction);
    
$response = $httpSpan->startHttpTypeSpan("GET", "http://localhost:8812/test_distribute.php", []);

与上面添加DB Span类似,添加HTTP Span也是同样的做法,先利用startNewSpan实例化一个任意类型的Span,然后利用封装好的startHttpTypeSpan方法来完成微服务调用。

⚠️对于HTTP操作,上面参数的值有以下建议:

  1. name传入请求方法 微服务名,例如GET user_profile
  2. type以点分隔符,记录操作的类型,例如调用微服务则是request.microservice
  3. sub_type以点分隔符,记录微服务名,例如user_profile

⚠️如果函数执行失败,方法会返回false,有必要在使用时进行false结果的判断

⚠️对于startHttpTypeSpan方法的参数,说明如下: 前两个分别是请求类型和地址(完整的地址,最好以https/http打头) 第三个参数,可以支持多个参数

  1. timeout,超时时间
  2. verify,验证https证书,默认为false
  3. headers,请求所带的headers头,格式为key-value数组
  4. json,POST请求所带的json body。
// 一个示例
[
    "timeout" => 1.0,
    "verify" => false,
    "headers" => [
        "Content-Type" => "application/json",
    ],
    "json" => [
        "app" => "",
    ],
]

假设User-Profile微服务里有代码如下:

$configure = new \EApmPhp\EApmConfigure(
    "your_apm_server_url_here",
    "your_apm_secret_token_here",
    "eapm-php-user-profile"
);

$agent = new EApmPhp\EApmComposer();
$agent->setConfigure($configure);
$agent->setAppConfig("debug", true);
$agent->setUserId(1001);
$agent->EApmUse();

$transaction = $agent->startNewTransaction("GET /user/{uid}/info", "request");
$mysqlSpan = $agent->startNewSpan("SELECT", "db.mysql", ".adm", $transaction);
$result = $mysqlSpan->startMysqlTypeSpan(getMysqlInstance(), "select * from adm where type = 1 and status = 1");

$transaction->end();
$agent->eventPush();

可以看到上面的代码其实就是基本实例的完整版。没有额外的处理,agent会自动处理分布式追踪,效果在APM服务里就像这样:

在APM面板上的效果: image 可以看到两个服务串成一个完整的会话,微服务里的db操作也可以清晰的看到

设置Label

对于transaction,span,erro对象,可以使用setLabel方法设置Label label实际上是一对key-value串 value可以设置所有的标量类型包括NULL,对于字符串类型的label值,最大长度为1024

$span = $agent->startNewSpan("SELECT", "db.mysql.select", ".adm", $transaction);
$span->setLabel("team", "team-backend");
$span->setLabel("mid", 1234);

每一个设置label后的对象,都可以在APM面板上直接粘贴label值进行搜索。

Error捕获

代码

$agent->captureError(new Error("test error"), $transaction);
$agent->captureError(new Exception("test exception"), $transaction);

agent可以手动捕获Throwable类型的任意错误和异常,并发给APM server 在APM面板上可以看到详细的错误信息

日志

当设置debug参数为true时,agent会自动记录一些运行时错误到/data/logs/EAPM-PHP目录下 也可以手动记录日志

$agent->getLogger()->logWarn("log warning info");
$agent->getLogger()->logError("log error info");

示例代码

<?php

require_once "./vendor/autoload.php";

$configure = new \EApmPhp\EApmConfigure(
    "your_apm_server_url_here",
    "your_apm_secret_token_here",
    "eapm-php-project"
);

$agent = new EApmPhp\EApmComposer();
$agent->setConfigure($configure);

$agent->setAppConfig("debug", true);
$agent->setUserId(1000);
$agent->setSampleRate(0.5);

// start
$agent->EApmUse(functoion(){
    echo "trace start.";
});

$transaction = $agent->startNewTransaction("POST /user/{uid}/avatar/upload", "http.request");

// mysql span
$mysqlSpan = $agent->startNewSpan("SELECT", "db.mysql", "adm", $transaction);
$mysqlSpan->setLabel("adid", 1234);
// getMysqlInstance 返回一个数据库实例
$result = $mysqlSpan->startMysqlTypeSpan(getMysqlInstance(), "select * from adm where type = 1 and status = 1");
$mysqlSpan->end();

// redis span
$redisSpan = $agent->startNewSpan("ZREVRANGE", "db.redis", "hermes", $transaction);
$mysqlSpan->setLabel("adid", 1234);
// getRedisInstance返回一个redis实例
$result = $redisSpan->startRedisTypeSpan(getRedisInstance(), "ZREVRANGE", "u:9336644:sessions", 0, 199, true);
$redisSpan->end();

// http span
try {
    $httpSpan = $agent->startNewSpan("GET user_profile", "request.microservice", "user_profile", $transaction);
    // 请求地址需要替换
    $response = $httpSpan->startHttpTypeSpan("GET", "http://localhost:8812/test_distribute.php", []);
} catch (\Exception $e) {
    $agent->getLogger()->logError((string)$e);
    $agent->captureError($e, $httpSpan);
} finally {
  $httpSpan->end();
}

$transaction->end();
$agent->eventPush();