icyboy/zipkin

PHP 的 Zipkin 仪表化工具

1.0.1 2017-11-28 09:28 UTC

README

Build Status Minimum PHP Version Total Downloads License

这是一个 生产就绪 的 PHP Zipkin 库。

这是一个简单的使用示例,对于更完整的客户端/服务器端示例,请查看 此存储库

安装

composer require icyboy/zipkin

设置

use GuzzleHttp\Client;
use Zipkin\Annotations;
use Zipkin\Endpoint;
use Zipkin\Samplers\BinarySampler;
use Zipkin\Timestamp;
use Zipkin\TracingBuilder;
use Zipkin\Reporters\HttpLogging;
use Zipkin\Propagation\Map;
use Zipkin\Propagation\DefaultSamplingFlags;
use Symfony\Component\HttpFoundation\Request;

$request  = Request::createFromGlobals();

$endpoint = Endpoint::create('server_name', 'server_ipv4', 'server_ipv6', 'server_ipv4');
$client = new Client();

// Logger to stdout
$logger = new \Monolog\Logger('log');
$logger->pushHandler(new \Monolog\Handler\ErrorLogHandler());

$reporter = new HttpLogging($client, $logger, ["host"=>"zipkin_server_address"]);
$sampler = BinarySampler::createAsAlwaysSample();
$tracing = TracingBuilder::create()
    ->havingLocalEndpoint($endpoint)
    ->havingSampler($sampler)
    ->havingReporter($reporter)
    ->build();

/***** this for client *****/
/* Always sample traces */
//$defaultSamplingFlags = DefaultSamplingFlags::createAsSampled();
/* Creates the main span */
//$span = $tracer->newTrace($defaultSamplingFlags);
/* Creates the span for getting the users list */
//$childSpan = $tracer->newChild($span->getContext());
//$childSpan->start();
//$childSpan->setName('users:get_list');
//$headers = [];
/* Injects the context into the wire */
//$injector = $tracing->getPropagation()->getInjector(new Map());
//$injector($childSpan->getContext(), $headers);
/* HTTP Request to the backend */
//$httpClient = new Client();
//$request = new \GuzzleHttp\Psr7\Request('POST', 'localhost:9000', $headers);
//$response = $httpClient->send($request);


//get request header
$carrier  = array_map(function ($header) {
    return $header[0];
}, $request->headers->all());

/* Extracts the context from the HTTP headers */
//get X─B3─xxx from headers if existed or auto generate
$extractor    = $tracing->getPropagation()->getExtractor(new Map());
$traceContext = $extractor($carrier);

//create trace
$tracer       = $tracing->getTracer();
$span         = $tracer->newChild($traceContext);
$span->start();

//get request parameters
$content = $request->getContent();
$data = json_decode($content, true);
if (is_array($data)) {
    $output = $data;
} elseif (!empty($content)) {
    parse_str($content, $output);
}

if (!empty($output) && is_array($output)) {
    foreach ($output as $k => $v) {
        $value = is_array($v) ? json_encode($v) : $v;
        $span->tag($k, $value);
    }
}

//set name
$span->setName(sprintf("%s %s", $request->getMethod(), $request->server->get("REQUEST_URI")));

//set Annotation
$span->annotate(Annotations\SERVER_RECV, Timestamp\now());
$span->finish(Timestamp\now());

//push info to zipkin server befor close program
register_shutdown_function(function () use ($tracer) {
    $tracer->flush();
});

追踪

追踪器创建并连接跨度,以模拟可能分布式工作的延迟。它可以采用采样来减少过程开销或减少发送到 Zipkin 的数据量。

追踪器返回的跨度在完成后向 Zipkin 报告数据,或者在未采样的情况下不执行任何操作。在启动跨度后,您可以注释感兴趣的事件或添加包含详细信息或查找键的标签。

跨度有一个上下文,其中包含放置在表示分布式操作的树中正确位置的跟踪标识符。

本地追踪

当追踪本地代码时,只需在跨度内运行它

$span = $tracer->newTrace()->setName('encode')->start();

try {
  doSomethingExpensive();
} finally {
  $span->finish();
}

在上面的示例中,跨度是跟踪的根。在许多情况下,您将是一个现有跟踪的一部分。在这种情况下,调用 newChild 而不是 newTrace

$span = $tracer->newChild($root->getContext())->setName('encode')->start();
try {
  doSomethingExpensive();
} finally {
  $span->finish();
}

自定义跨度

一旦您有了跨度,您就可以向其添加标签,这些标签可以用作查找键或详细信息。例如,您可能添加一个包含您的运行时版本的标签。

$span->tag('http.status_code', '200');

RPC 追踪

RPC 追踪通常由拦截器自动执行。幕后,它们添加与它们在 RPC 操作中角色相关的标签和事件。

这是一个客户端跨度的示例

// before you send a request, add metadata that describes the operation
$span = $tracer->newTrace()->setName('get')->setKind(Kind\CLIENT);
$span->tag('http.status_code', '200');
$span->tag(Tags\HTTP_PATH, '/api');
$span->setRemoteEndpoint(Remote::create('backend', 127 << 24 | 1, null, 8080);

// when the request is scheduled, start the span
$span->start();

// if you have callbacks for when data is on the wire, note those events
$span->annotate(Annotations\SERVER_SEND);
$span->annotate(Annotations\SERVER_RECV);

// when the response is complete, finish the span
$span->finish();

单向追踪

有时您需要模拟一个异步操作,其中有一个请求,但没有响应。在正常的 RPC 追踪中,您使用 $span->finish() 来指示已收到响应。在单向追踪中,您使用 $span->flush() 代替,因为您不期望收到响应。

这是一个客户端如何模拟单向操作的示例

// start a new span representing a client request
$oneWaySend = $tracer->newChild($parent)->setKind(Kind\CLIENT);

// Add the trace context to the request, so it can be propagated in-band
$tracing->getPropagation()->getInjector(new RequestHeaders)
                     ->inject($oneWaySend->getContext(), $request);

// fire off the request asynchronously, totally dropping any response
$client->execute($request);

// start the client side and flush instead of finish
$oneWaySend->start()->flush();

服务器可能这样处理...

// pull the context out of the incoming request
$extractor = $tracing->getPropagation()->getExtractor(new RequestHeaders);

// convert that context to a span which you can name and add tags to
$oneWayReceive = $tracer->newChild($extractor($request))
    ->setName('process-request')
    ->setKind(Kind\SERVER)
    ... add tags etc.

// start the server side and flush instead of finish
$oneWayReceive->start()->flush();

// you should not modify this span anymore as it is complete. However,
// you can create children to represent follow-up work.
$next = $tracer->newChild($oneWayReceive->getContext())->setName('step2')->start();

采样

采样可以用于减少收集和报告的进程数据。当一个跨度未被采样时,它不会增加任何开销(空操作)。

采样是一个前瞻性决策,这意味着在跟踪中的第一个操作处就做出报告数据的决策,并且该决策会向下传播。

默认情况下,有一个全局采样器,它对所有追踪操作应用一个单一速率。 Sampler 是您指示此的方式,并且默认为追踪每个请求。

自定义采样

您可能希望根据操作应用不同的策略。例如,您可能不想追踪对静态资源(如图像)的请求,或者您可能想追踪对新 api 的所有请求。

大多数用户将使用框架拦截器来自动化此类策略。以下是它们可能内部工作的示例。

private function newTrace(Request $request) {
  $flags = SamplingFlags::createAsEmpty();
  if (strpos($request->getUri(), '/experimental') === 0) {
    $flags = DefaultSamplingFlags::createAsSampled();
  } else if (strpos($request->getUri(), '/static') === 0) {
    $flags = DefaultSamplingFlags::createAsSampled();
  }
  return $tracer->newTrace($flags);
}

传播

传播对于确保来自同一根的活动被收集到相同的跟踪中是必需的。最常用的传播方法是将跟踪上下文从发送 RPC 请求的客户端复制到接收它的服务器。

例如,当进行下游 Http 调用时,其跟踪上下文将随请求一起发送,编码为请求头。

   Client Span                                                Server Span
┌──────────────────┐                                       ┌──────────────────┐
│                  │                                       │                  │
│   TraceContext   │           Http Request Headers        │   TraceContext   │
│ ┌──────────────┐ │          ┌───────────────────┐        │ ┌──────────────┐ │
│ │ TraceId      │ │          │ X─B3─TraceId      │        │ │ TraceId      │ │
│ │              │ │          │                   │        │ │              │ │
│ │ ParentSpanId │ │ Extract  │ X─B3─ParentSpanId │ Inject │ │ ParentSpanId │ │
│ │              ├─┼─────────>│                   ├────────┼>│              │ │
│ │ SpanId       │ │          │ X─B3─SpanId       │        │ │ SpanId       │ │
│ │              │ │          │                   │        │ │              │ │
│ │ Sampled      │ │          │ X─B3─Sampled      │        │ │ Sampled      │ │
│ └──────────────┘ │          └───────────────────┘        │ └──────────────┘ │
│                  │                                       │                  │
└──────────────────┘                                       └──────────────────┘

上面的名称来自 B3 Propagation,它是 Brave 内置的,并且在许多语言和框架中都有实现。

大多数用户会使用框架拦截器来自动化传播。以下是它们可能如何内部工作的。

以下是客户端传播可能的样子

// configure a function that injects a trace context into a request
$injector = $tracing->getPropagation()->getInjector(new RequestHeaders);

// before a request is sent, add the current span's context to it
$injector->inject($span->getContext(), $request);

以下是服务器端传播可能的样子

// configure a function that extracts the trace context from a request
$extracted = $tracing->getPropagation()->extractor(new RequestHeaders);

$span = $tracer->newChild($extracted)->setKind(Kind\SERVER);

提取传播上下文

Extractor 从传入的请求或消息中读取跟踪标识符和采样状态。载体通常是请求对象或头。

SamplingFlags|TraceContext 通常仅与 $tracer->newChild(extracted) 一起使用,除非你在客户端和服务器之间共享span ID。

实现传播

Extractor 将输出一个包含以下之一的 SamplingFlags|TraceContext

  • 如果存在跟踪和span ID,则输出 TraceContext
  • 如果没有标识符,则输出 SamplingFlags

当前span

Zipkin 支持一个“当前span”的概念,它表示正在进行的操作。Tracer::currentSpan() 可以用来向span添加自定义标签,而 Tracer::nextSpan() 可以用来创建当前正在进行的span的子span。

当前span的一个常见用例是用于检测RPC客户端。例如

/**
  * This http clients composes an http client using PSR7
  */
class TraceClient implements ClientInterface
{
    public function request($method, $uri = '', array $options = [])
    {
        /* Gets the child Span of the current one */
        $span = $this->tracer->nextSpan();
        $span->setKind(Zipkin\Kind\CLIENT);
        $span->tag(Tags\HTTP_PATH, $uri);
        
        try {
            $response = $this->client->request($method, $uri, $options);
            $span->tag(Tags\HTTP_STATUS_CODE, $response->getStatusCode());
            
            return $response;
        catch (\Exception $e) {
            $span->tag(Tags\Error, $response->getReasonPhrase());        
            $span->tag(Tags\HTTP_STATUS_CODE, $e->getResponse->getStatusCode());
            
            throw $e;
        } finally {
            $span->finish();
        }
    }
}

手动设置作用域内的span

当编写新的检测代码时,将你创建的span放置在作用域内作为当前span是很重要的。

在边缘情况下,你可能需要暂时清除当前span。例如,启动一个不应该与当前请求关联的任务。为此,只需将null传递给 openScope

测试

可以通过以下方式运行测试:

composer test

参考