Jaeger exporter (#70)

* make zipkin-exporter implementation simpler

* add Jaeger exporter and fix zipkin exporter timestamps

* move zipkin converter logic outside of exporters

* start containers before running examples

* fix open-telemetry badge codecov.io url

* fix phpunit whitelist after api and sdk splitting
This commit is contained in:
Beniamin 2020-04-05 23:05:52 +03:00 committed by GitHub
parent 783ccdde06
commit e5eda43ad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 353 additions and 153 deletions

View File

@ -5,11 +5,14 @@ install:
update:
$(DC_RUN_PHP) composer update
test:
$(DC_RUN_PHP) php ./vendor/bin/phpunit --colors=always
$(DC_RUN_PHP) php ./vendor/bin/phpunit --colors=always --coverage-text
phan:
$(DC_RUN_PHP) env PHAN_DISABLE_XDEBUG_WARN=1 php ./vendor/bin/phan
examples: FORCE
docker-compose up -d
$(DC_RUN_PHP) php ./examples/AlwaysOnTraceExample.php
$(DC_RUN_PHP) php ./examples/AlwaysOffTraceExample.php
$(DC_RUN_PHP) php ./examples/JaegerExporterExample.php
bash:
$(DC_RUN_PHP) bash
style:

View File

@ -1,7 +1,7 @@
# OpenTelemetry php library
[![Gitter](https://badges.gitter.im/open-telemetry/opentelemetry-php.svg)](https://gitter.im/open-telemetry/opentelemetry-php?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Build Status](https://travis-ci.org/open-telemetry/opentelemetry-php.svg?branch=master)](https://travis-ci.org/open-telemetry/opentelemetry-php)
[![codecov](https://codecov.io/gh/open-telemetry/opentelemetry-php/branch/master/graph/badge.svg)](https://codecov.io/gh/opentelemety/opentelemetry-php)
[![codecov](https://codecov.io/gh/open-telemetry/opentelemetry-php/branch/master/graph/badge.svg)](https://codecov.io/gh/open-telemetry/opentelemetry-php)
## Communication
Most of our communication is done on gitter.im in the [opentelemetry-php](https://gitter.im/open-telemetry/opentelemetry-php) channel.

View File

@ -10,3 +10,10 @@ services:
image: openzipkin/zipkin-slim
ports:
- 9411:9411
jaeger:
image: jaegertracing/all-in-one
environment:
COLLECTOR_ZIPKIN_HTTP_PORT: 9412
ports:
- 9412:9412
- 16686:16686

View File

@ -36,5 +36,7 @@ if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult) {
$span->end(); // pass status as an optional argument
print_r($span); // print the span as a resulting output
} else {
echo 'Sampling is not enabled';
echo PHP_EOL . 'Sampling is not enabled';
}
echo PHP_EOL;

View File

@ -29,9 +29,12 @@ if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult->getDecision()) {
[new SimpleSpanProcessor($zipkinExporter)]
))
->getTracer('io.opentelemetry.contrib.php');
echo PHP_EOL . sprintf('Trace with id %s started ', $tracer->getActiveSpan()->getContext()->getTraceId());
for ($i = 0; $i < 5; $i++) {
// start a span, register some events
$span = $tracer->startAndActivateSpan('session.generate.span' . $i);
$span = $tracer->startAndActivateSpan('session.generate.span.' . time());
$tracer->setActiveSpan($span);
$span->setAttribute('remote_ip', '1.2.3.4')
@ -47,7 +50,9 @@ if (SamplingResult::RECORD_AND_SAMPLED === $samplingResult->getDecision()) {
$tracer->endActiveSpan();
}
echo 'AlwaysOnTraceExample complete! See the results at http://localhost:9411/';
echo PHP_EOL . 'AlwaysOnTraceExample complete! See the results at http://localhost:9411/';
} else {
echo 'Sampling is not enabled';
echo PHP_EOL . 'Sampling is not enabled';
}
echo PHP_EOL;

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use OpenTelemetry\Sdk\Trace\AlwaysOnSampler;
use OpenTelemetry\Sdk\Trace\Attributes;
use OpenTelemetry\Sdk\Trace\JaegerExporter;
use OpenTelemetry\Sdk\Trace\SimpleSpanProcessor;
use OpenTelemetry\Sdk\Trace\TracerProvider;
$sampler = (new AlwaysOnSampler())->shouldSample();
$exporter = new JaegerExporter(
'jaegerExporterExample',
'http://jaeger:9412/api/v2/spans'
);
if ($sampler) {
echo 'Starting JaegerExporterExample';
$tracer = (TracerProvider::getInstance(
[new SimpleSpanProcessor($exporter)]
))
->getTracer('io.opentelemetry.contrib.php');
echo PHP_EOL . sprintf('Trace with id %s started ', $tracer->getActiveSpan()->getContext()->getTraceId());
for ($i = 0; $i < 5; $i++) {
// start a span, register some events
$span = $tracer->startAndActivateSpan('session.generate.span' . time());
$tracer->setActiveSpan($span);
$span->setAttribute('remote_ip', '1.2.3.4')
->setAttribute('country', 'USA');
$span->addEvent('found_login' . $i, new Attributes([
'id' => $i,
'username' => 'otuser' . $i,
]));
$span->addEvent('generated_session', new Attributes([
'id' => md5((string) microtime(true)),
]));
$tracer->endActiveSpan();
}
echo PHP_EOL . 'JaegerExporterExample complete! See the results at http://localhost:16686/';
} else {
echo PHP_EOL . 'Sampling is not enabled';
}
echo PHP_EOL;

View File

@ -32,7 +32,10 @@
<filter>
<whitelist>
<directory>src</directory>
<directory>sdk</directory>
<exclude>
<directory>sdk/Tests</directory>
</exclude>
</whitelist>
</filter>

View File

@ -10,9 +10,7 @@ use OpenTelemetry\Sdk\Trace\Attributes;
use OpenTelemetry\Sdk\Trace\SpanContext;
use OpenTelemetry\Sdk\Trace\SpanStatus;
use OpenTelemetry\Sdk\Trace\Tracer;
use OpenTelemetry\Sdk\Trace\ZipkinExporter;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
class TracingTest extends TestCase
{
@ -222,34 +220,4 @@ class TracingTest extends TestCase
$this->assertNull($global->getParent());
$this->assertNotNull($request->getParent());
}
public function testZipkinConverter()
{
$tracer = new Tracer();
$span = $tracer->startAndActivateSpan('guard.validate');
$span->setAttribute('service', 'guard');
$span->addEvent('validators.list', new Attributes(['job' => 'stage.updateTime']));
$span->end();
$method = new ReflectionMethod(ZipkinExporter::class, 'convertSpan');
$method->setAccessible(true);
$exporter = new ZipkinExporter(
'test.name',
'http://host:123/path'
);
$row = $method->invokeArgs($exporter, ['span' => $span]);
$this->assertSame($row['name'], $span->getSpanName());
self::assertCount(1, $row['tags']);
self::assertEquals($span->getAttribute('service')->getValue(), $row['tags']['service']);
self::assertCount(1, $row['annotations']);
[$annotation] = $row['annotations'];
self::assertEquals('validators.list', $annotation['value']);
[$event] = \iterator_to_array($span->getEvents());
self::assertEquals($event->getTimestamp(), $annotation['timestamp']);
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Sdk\Trace;
use Exception;
use InvalidArgumentException;
use OpenTelemetry\Sdk\Trace\Zipkin\SpanConverter;
use OpenTelemetry\Trace as API;
class JaegerExporter implements Exporter
{
const IMPLEMENTED_FORMATS = [
'/api/v1/spans',
'/api/v2/spans',
];
/**
* @var string
*/
private $endpointUrl;
/**
* @var SpanConverter
*/
private $spanConverter;
public function __construct($name, string $endpointUrl, SpanConverter $spanConverter = null)
{
$url = parse_url($endpointUrl);
if (!is_array($url)) {
throw new InvalidArgumentException('Unable to parse provided DSN');
}
if (!isset($url['scheme'])
|| !isset($url['host'])
|| !isset($url['port'])
|| !isset($url['path'])
) {
throw new InvalidArgumentException('Endpoint should have scheme, host, port and path');
}
if (!in_array($url['path'], self::IMPLEMENTED_FORMATS)) {
throw new InvalidArgumentException(
sprintf("Current implementation supports only '%s' format", implode(' or ', self::IMPLEMENTED_FORMATS))
);
}
$this->endpointUrl = $endpointUrl;
$this->spanConverter = $spanConverter ?? new SpanConverter($name);
}
/**
* Exports the provided Span data via the Zipkin protocol
*
* @param iterable<API\Span> $spans Array of Spans
* @return int return code, defined on the Exporter interface
*/
public function export(iterable $spans) : int
{
if (empty($spans)) {
return Exporter::SUCCESS;
}
$convertedSpans = [];
foreach ($spans as &$span) {
array_push($convertedSpans, $this->spanConverter->convert($span));
}
try {
$json = json_encode($convertedSpans);
$contextOptions = [
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => $json,
],
];
$context = stream_context_create($contextOptions);
@file_get_contents($this->endpointUrl, false, $context);
} catch (Exception $e) {
return Exporter::FAILED_RETRYABLE;
}
return Exporter::SUCCESS;
}
public function shutdown(): void
{
// TODO: Implement shutdown() method.
}
}

View File

@ -100,14 +100,18 @@ class Tracer implements API\Tracer
public function endActiveSpan(?string $timestamp = null)
{
// todo: should processors be called before or after end()?
if ($this->getActiveSpan()->isRecording()) {
/**
* a span should be ended before is sent to exporters, because the exporters need's span duration.
*/
$span = $this->getActiveSpan();
$wasRecording = $span->isRecording();
$span->end();
if ($wasRecording) {
foreach ($this->spanProcessors as $spanProcessor) {
$spanProcessor->onEnd($this->getActiveSpan());
$spanProcessor->onEnd($span);
}
}
$this->getActiveSpan()->end($timestamp);
}
private function generateSpanInstance($name, API\SpanContext $context): Span

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Sdk\Trace\Zipkin;
use OpenTelemetry\Trace\Span;
class SpanConverter
{
/**
* @var string
*/
private $serviceName;
public function __construct(string $serviceName)
{
$this->serviceName = $serviceName;
}
public function convert(Span $span)
{
$row = [
'id' => $span->getContext()->getSpanId(),
'traceId' => $span->getContext()->getTraceId(),
'parentId' => $span->getParent() ? $span->getParent()->getSpanId() : null,
'localEndpoint' => [
'serviceName' => $this->serviceName,
],
'name' => $span->getSpanName(),
'timestamp' => (int) ((float) $span->getStartTimestamp() * 1000),
'duration' => (int) ((float) $span->getEndTimestamp() * 1000 - (float) $span->getStartTimestamp() * 1000),
];
foreach ($span->getAttributes() as $k => $v) {
if (!array_key_exists('tags', $row)) {
$row['tags'] = [];
}
$v = $v->getValue();
if (is_bool($v)) {
$v = (string) $v;
}
$row['tags'][$k] = $v;
}
foreach ($span->getEvents() as $event) {
if (!array_key_exists('annotations', $row)) {
$row['annotations'] = [];
}
$row['annotations'][] = [
'timestamp' => (int) round((float) $event->getTimestamp() * 1000),
'value' => $event->getName(),
];
}
return $row;
}
}

View File

@ -7,6 +7,7 @@ namespace OpenTelemetry\Sdk\Trace;
use Exception;
use InvalidArgumentException;
use OpenTelemetry\Sdk\Trace\Zipkin\SpanConverter;
use OpenTelemetry\Trace as API;
/**
@ -15,23 +16,35 @@ use OpenTelemetry\Trace as API;
*/
class ZipkinExporter implements Exporter
{
/**
* @var string
*/
private $endpointUrl;
/**
* @var $endpoint array to send Spans to
* @var SpanConverter
*/
private $endpoint;
private $name;
private $spanConverter;
public function __construct($name, string $endpointDsn)
public function __construct($name, string $endpointUrl, SpanConverter $spanConverter = null)
{
$parsedDsn = parse_url($endpointDsn);
$parsedDsn = parse_url($endpointUrl);
if (!is_array($parsedDsn)) {
throw new InvalidArgumentException('Unable to parse provided DSN');
}
$this->setEndpoint($parsedDsn);
$this->name = $name;
if (!isset($parsedDsn['scheme'])
|| !isset($parsedDsn['host'])
|| !isset($parsedDsn['port'])
|| !isset($parsedDsn['path'])
) {
throw new InvalidArgumentException('Endpoint should have scheme, host, port and path');
}
$this->endpointUrl = $endpointUrl;
$this->spanConverter = $spanConverter ?? new SpanConverter($name);
}
/**
@ -48,7 +61,7 @@ class ZipkinExporter implements Exporter
$convertedSpans = [];
foreach ($spans as &$span) {
array_push($convertedSpans, $this->convertSpan($span));
array_push($convertedSpans, $this->spanConverter->convert($span));
}
try {
@ -61,7 +74,7 @@ class ZipkinExporter implements Exporter
],
];
$context = stream_context_create($contextOptions);
file_get_contents($this->getEndpointUrl(), false, $context);
@file_get_contents($this->endpointUrl, false, $context);
} catch (Exception $e) {
return Exporter::FAILED_RETRYABLE;
}
@ -69,93 +82,6 @@ class ZipkinExporter implements Exporter
return Exporter::SUCCESS;
}
/**
* Converts spans to Zipkin format for export
*
* @param API\Span $span
* @return array
*/
private function convertSpan(API\Span $span) : array
{
$row = [
'id' => $span->getContext()->getSpanId(),
'traceId' => $span->getContext()->getTraceId(),
'parentId' => $span->getParent() ? $span->getParent()->getSpanId() : null,
'localEndpoint' => [
'serviceName' => $this->name,
'port' => $this->getEndpoint()['port'] ?? 0,
],
'name' => $span->getSpanName(),
'timestamp' => (int) round((float) $span->getStartTimestamp()),
'duration' => (int) round((float) $span->getEndTimestamp()) - round((float) $span->getStartTimestamp()),
];
foreach ($span->getAttributes() as $k => $v) {
if (!array_key_exists('tags', $row)) {
$row['tags'] = [];
}
$v = $v->getValue();
if (is_bool($v)) {
$v = (string) $v;
}
$row['tags'][$k] = $v;
}
foreach ($span->getEvents() as $event) {
if (!array_key_exists('annotations', $row)) {
$row['annotations'] = [];
}
$row['annotations'][] = [
'timestamp' => $event->getTimestamp(),
'value' => $event->getName(),
];
}
return $row;
}
/**
* Gets the configured endpoint for the Zipkin exporter
*
* @return array |null
*/
public function getEndpoint(): ?array
{
return $this->endpoint;
}
/**
* Sets the configured endpoint for the zipkin exportedr
*
* @param array $endpoint
* @return $this
*/
public function setEndpoint(array $endpoint) : self
{
if (!isset($endpoint['scheme'])
|| !isset($endpoint['host'])
|| !isset($endpoint['port'])
|| !isset($endpoint['path'])
) {
throw new InvalidArgumentException('Endpoint should have scheme, host, port and path');
}
$this->endpoint = $endpoint;
return $this;
}
protected function getEndpointUrl(): string
{
return sprintf(
'%s://%s:%s%s',
$this->endpoint['scheme'],
$this->endpoint['host'],
$this->endpoint['port'],
$this->endpoint['path']
);
}
public function shutdown(): void
{
// TODO: Implement shutdown() method.

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\Exporter;
use InvalidArgumentException;
use OpenTelemetry\Sdk\Trace\JaegerExporter;
use PHPUnit\Framework\TestCase;
class JaegerExporterTest extends TestCase
{
/**
* @test
* @dataProvider invalidDsnDataProvider
*/
public function shouldThrowExceptionIfInvalidDsnIsPassed($invalidDsn)
{
$this->expectException(InvalidArgumentException::class);
new JaegerExporter('test.zipkin', $invalidDsn);
}
public function invalidDsnDataProvider()
{
return [
'missing scheme' => ['host:123/path'],
'missing host' => ['scheme://123/path'],
'missing port' => ['scheme://host/path'],
'missing path' => ['scheme://host:123'],
'invalid port' => ['scheme://host:port/path'],
'invalid scheme' => ['1234://host:port/path'],
'invalid host' => ['scheme:///end:1234/path'],
'unimplemented path' => ['scheme:///host:1234/api/v1/spans'],
];
}
}

View File

@ -10,19 +10,6 @@ use PHPUnit\Framework\TestCase;
class ZipkinExporterTest extends TestCase
{
/**
* @test
*/
public function shouldParseAnValidDsn()
{
$exporter = new ZipkinExporter('test.zipkin', 'scheme://host:1234/path');
$this->assertArrayHasKey('scheme', $exporter->getEndpoint());
$this->assertArrayHasKey('host', $exporter->getEndpoint());
$this->assertArrayHasKey('port', $exporter->getEndpoint());
$this->assertArrayHasKey('path', $exporter->getEndpoint());
}
/**
* @test
* @dataProvider invalidDsnDataProvider

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace unit\Exporter;
use OpenTelemetry\Sdk\Trace\Attributes;
use OpenTelemetry\Sdk\Trace\Tracer;
use OpenTelemetry\Sdk\Trace\Zipkin\SpanConverter;
use PHPUnit\Framework\TestCase;
class ZipkinSpanConverterTest extends TestCase
{
/**
* @test
*/
public function shouldConvertASpanToAPayloadForZipkin()
{
$tracer = new Tracer();
$span = $tracer->startAndActivateSpan('guard.validate');
$span->setAttribute('service', 'guard');
$span->addEvent('validators.list', new Attributes(['job' => 'stage.updateTime']));
$span->end();
$converter = new SpanConverter('test.name');
$row = $converter->convert($span);
$this->assertSame($span->getContext()->getSpanId(), $row['id']);
$this->assertSame($span->getContext()->getTraceId(), $row['traceId']);
$this->assertSame('test.name', $row['localEndpoint']['serviceName']);
$this->assertSame($span->getSpanName(), $row['name']);
$this->assertIsInt($row['timestamp']);
// timestamp should be in microseconds
$this->assertGreaterThan(1e15, $row['timestamp']);
$this->assertIsInt($row['duration']);
$this->assertGreaterThan(0, $row['duration']);
$this->assertCount(1, $row['tags']);
$this->assertEquals($span->getAttribute('service')->getValue(), $row['tags']['service']);
$this->assertCount(1, $row['annotations']);
[$annotation] = $row['annotations'];
$this->assertEquals('validators.list', $annotation['value']);
[$event] = \iterator_to_array($span->getEvents());
$this->assertIsInt($annotation['timestamp']);
// timestamp should be in microseconds
$this->assertGreaterThan(1e15, $annotation['timestamp']);
}
}