diff --git a/Makefile b/Makefile
index 0ad5300b..7a13e451 100644
--- a/Makefile
+++ b/Makefile
@@ -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:
diff --git a/README.md b/README.md
index 35897ead..185a3626 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# OpenTelemetry php library
[](https://gitter.im/open-telemetry/opentelemetry-php?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[](https://travis-ci.org/open-telemetry/opentelemetry-php)
-[](https://codecov.io/gh/opentelemety/opentelemetry-php)
+[](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.
diff --git a/docker-compose.yaml b/docker-compose.yaml
index bc7e1270..9596dd9a 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -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
diff --git a/examples/AlwaysOffTraceExample.php b/examples/AlwaysOffTraceExample.php
index c7244fab..9e3ede3f 100644
--- a/examples/AlwaysOffTraceExample.php
+++ b/examples/AlwaysOffTraceExample.php
@@ -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;
diff --git a/examples/AlwaysOnTraceExample.php b/examples/AlwaysOnTraceExample.php
index 4717f9b0..ed81e496 100644
--- a/examples/AlwaysOnTraceExample.php
+++ b/examples/AlwaysOnTraceExample.php
@@ -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;
diff --git a/examples/JaegerExporterExample.php b/examples/JaegerExporterExample.php
new file mode 100644
index 00000000..0bb6b5d1
--- /dev/null
+++ b/examples/JaegerExporterExample.php
@@ -0,0 +1,51 @@
+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;
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index deb03cd4..18b3e337 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -32,7 +32,10 @@
- src
+ sdk
+
+ sdk/Tests
+
diff --git a/sdk/Tests/TracingTest.php b/sdk/Tests/TracingTest.php
index 088cb509..0c581282 100644
--- a/sdk/Tests/TracingTest.php
+++ b/sdk/Tests/TracingTest.php
@@ -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']);
- }
}
diff --git a/sdk/Trace/JaegerExporter.php b/sdk/Trace/JaegerExporter.php
new file mode 100644
index 00000000..74793e06
--- /dev/null
+++ b/sdk/Trace/JaegerExporter.php
@@ -0,0 +1,95 @@
+endpointUrl = $endpointUrl;
+
+ $this->spanConverter = $spanConverter ?? new SpanConverter($name);
+ }
+
+ /**
+ * Exports the provided Span data via the Zipkin protocol
+ *
+ * @param iterable $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.
+ }
+}
diff --git a/sdk/Trace/Tracer.php b/sdk/Trace/Tracer.php
index a680f7e0..d7fc652e 100644
--- a/sdk/Trace/Tracer.php
+++ b/sdk/Trace/Tracer.php
@@ -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
diff --git a/sdk/Trace/Zipkin/SpanConverter.php b/sdk/Trace/Zipkin/SpanConverter.php
new file mode 100644
index 00000000..cfe431ea
--- /dev/null
+++ b/sdk/Trace/Zipkin/SpanConverter.php
@@ -0,0 +1,58 @@
+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;
+ }
+}
diff --git a/sdk/Trace/ZipkinExporter.php b/sdk/Trace/ZipkinExporter.php
index f32a29bf..ad272856 100644
--- a/sdk/Trace/ZipkinExporter.php
+++ b/sdk/Trace/ZipkinExporter.php
@@ -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.
diff --git a/tests/unit/Exporter/JaegerExporterTest.php b/tests/unit/Exporter/JaegerExporterTest.php
new file mode 100644
index 00000000..c98cc9e7
--- /dev/null
+++ b/tests/unit/Exporter/JaegerExporterTest.php
@@ -0,0 +1,37 @@
+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'],
+ ];
+ }
+}
diff --git a/tests/unit/Exporter/ZipkinExporterTest.php b/tests/unit/Exporter/ZipkinExporterTest.php
index 28b21492..0fbdaa25 100644
--- a/tests/unit/Exporter/ZipkinExporterTest.php
+++ b/tests/unit/Exporter/ZipkinExporterTest.php
@@ -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
diff --git a/tests/unit/Exporter/ZipkinSpanConverterTest.php b/tests/unit/Exporter/ZipkinSpanConverterTest.php
new file mode 100644
index 00000000..97107010
--- /dev/null
+++ b/tests/unit/Exporter/ZipkinSpanConverterTest.php
@@ -0,0 +1,54 @@
+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']);
+ }
+}