Skip to content
This repository was archived by the owner on Jul 31, 2023. It is now read-only.

Commit 213bbd0

Browse files
authored
Move ZipkinExporter (#2)
1 parent 9e7316a commit 213bbd0

3 files changed

Lines changed: 338 additions & 9 deletions

File tree

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "library",
55
"require": {
66
"php": ">=5.6",
7-
"opencensus/opencensus": "^0.2"
7+
"opencensus/opencensus": "~0.4"
88
},
99
"require-dev": {
1010
"phpunit/phpunit": "^6.0",
@@ -20,7 +20,7 @@
2020
"minimum-stability": "stable",
2121
"autoload": {
2222
"psr-4": {
23-
"OpenCensus\\Trace\\": "src/Trace/"
23+
"OpenCensus\\Trace\\Exporter\\": "src/"
2424
}
2525
}
2626
}

src/ZipkinExporter.php

Lines changed: 184 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
namespace OpenCensus\Trace\Exporter;
1919

20-
use OpenCensus\Trace\Tracer\TracerInterface;
20+
use OpenCensus\Trace\MessageEvent;
21+
use OpenCensus\Trace\Span;
22+
use OpenCensus\Trace\SpanData;
2123

2224
/**
2325
* This implementation of the ExporterInterface appends a json
@@ -34,15 +36,192 @@
3436
*/
3537
class ZipkinExporter implements ExporterInterface
3638
{
39+
const KIND_SERVER = 'SERVER';
40+
const KIND_CLIENT = 'CLIENT';
41+
const DEFAULT_ENDPOINT = 'http://localhost:9411/api/v2/spans';
42+
const KIND_MAP = [
43+
Span::KIND_UNSPECIFIED => null,
44+
Span::KIND_SERVER => self::KIND_SERVER,
45+
Span::KIND_CLIENT => self::KIND_CLIENT
46+
];
47+
48+
/**
49+
* @var string
50+
*/
51+
private $endpointUrl;
52+
53+
/**
54+
* @var array
55+
*/
56+
private $localEndpoint;
57+
58+
/**
59+
* Create a new ZipkinExporter
60+
*
61+
* @param string $name The name of this application
62+
* @param string $endpointUrl (optional) The url for the span reporting
63+
* endpoint. **Defaults to** `http://localhost:9411/api/v2/spans`
64+
* @param array $server (optional) The server array to search for the
65+
* SERVER_PORT. **Defaults to** $_SERVER
66+
*/
67+
public function __construct($name, $endpointUrl = null, array $server = null)
68+
{
69+
$server = $server ?: $_SERVER;
70+
$this->endpointUrl = ($endpointUrl === null) ? self::DEFAULT_ENDPOINT : $endpointUrl;
71+
$this->localEndpoint = [
72+
'serviceName' => $name
73+
];
74+
if (array_key_exists('SERVER_PORT', $server)) {
75+
$this->localEndpoint['port'] = intval($server['SERVER_PORT']);
76+
}
77+
}
78+
79+
/**
80+
* Set the localEndpoint ipv4 value for all reported spans. Note that this
81+
* is optional because the reverse DNS lookup can be slow.
82+
*
83+
* @param string $ipv4 IPv4 address
84+
*/
85+
public function setLocalIpv4($ipv4)
86+
{
87+
$this->localEndpoint['ipv4'] = $ipv4;
88+
}
89+
90+
/**
91+
* Set the localEndpoint ipv6 value for all reported spans. Note that this
92+
* is optional because the reverse DNS lookup can be slow.
93+
*
94+
* @param string $ipv6 IPv6 address
95+
*/
96+
public function setLocalIpv6($ipv6)
97+
{
98+
$this->localEndpoint['ipv6'] = $ipv6;
99+
}
100+
37101
/**
38102
* Report the provided Trace to a backend.
39103
*
40-
* @param TracerInterface $tracer
104+
* @param SpanData[] $spans
41105
* @return bool
42106
*/
43-
public function report(TracerInterface $tracer)
107+
public function export(array $spans)
44108
{
45-
// TODO: Implement this
46-
return false;
109+
if (empty($spans)) {
110+
return false;
111+
}
112+
113+
$spans = $this->convertSpans($spans);
114+
115+
try {
116+
$json = json_encode($spans);
117+
$contextOptions = [
118+
'http' => [
119+
'method' => 'POST',
120+
'header' => 'Content-Type: application/json',
121+
'content' => $json
122+
]
123+
];
124+
125+
$context = stream_context_create($contextOptions);
126+
file_get_contents($this->endpointUrl, false, $context);
127+
} catch (\Exception $e) {
128+
return false;
129+
}
130+
return true;
131+
}
132+
133+
/**
134+
* Convert spans into Zipkin's expected JSON output format. See
135+
* <a href="http://zipkin.io/zipkin-api/#/default/post_spans">output format definition</a>.
136+
*
137+
* @access private
138+
*
139+
* @param SpanData[] $spans
140+
* @param array $headers [optional] HTTP headers to parse. **Defaults to** $_SERVER
141+
* @return array Representation of the collected trace spans ready for serialization
142+
*/
143+
public function convertSpans(array $spans, $headers = null)
144+
{
145+
$headers = $headers ?: $_SERVER;
146+
147+
// True is a request to store this span even if it overrides sampling policy.
148+
// This is true when the X-B3-Flags header has a value of 1.
149+
$isDebug = array_key_exists('HTTP_X_B3_FLAGS', $headers) && $headers['HTTP_X_B3_FLAGS'] == '1';
150+
151+
// True if we are contributing to a span started by another tracer (ex on a different host).
152+
$isShared = !empty($spans) && $spans[0]->parentSpanId() !== null;
153+
154+
$zipkinSpans = [];
155+
foreach ($spans as $span) {
156+
$startTime = (int)((float) $span->startTime()->format('U.u') * 1000 * 1000);
157+
$endTime = (int)((float) $span->endTime()->format('U.u') * 1000 * 1000);
158+
$spanId = str_pad($span->spanId(), 16, '0', STR_PAD_LEFT);
159+
$parentSpanId = $span->parentSpanId()
160+
? str_pad($span->parentSpanId(), 16, '0', STR_PAD_LEFT)
161+
: null;
162+
$traceId = str_pad($span->traceId(), 32, '0', STR_PAD_LEFT);
163+
164+
$attributes = $span->attributes();
165+
if (empty($attributes)) {
166+
// force json_encode to render an empty object ("{}") instead of an empty array ("[]")
167+
$attributes = new \stdClass();
168+
}
169+
170+
$zipkinSpan = [
171+
'traceId' => $traceId,
172+
'name' => $span->name(),
173+
'parentId' => $parentSpanId,
174+
'id' => $spanId,
175+
'timestamp' => $startTime,
176+
'duration' => $endTime - $startTime,
177+
'debug' => $isDebug,
178+
'shared' => $isShared,
179+
'localEndpoint' => $this->localEndpoint,
180+
'tags' => $attributes,
181+
];
182+
183+
if (null !== ($kind = $this->spanKind($span))) {
184+
$zipkinSpan['kind'] = $kind;
185+
}
186+
187+
$zipkinSpans[] = $zipkinSpan;
188+
}
189+
190+
return $zipkinSpans;
191+
}
192+
193+
private function spanKind(SpanData $span)
194+
{
195+
$kind = self::KIND_MAP[$span->kind()];
196+
if ($kind !== null) {
197+
return $kind;
198+
}
199+
200+
if (strpos($span->name(), 'Sent.') === 0) {
201+
return self::KIND_CLIENT;
202+
}
203+
204+
if (strpos($span->name(), 'Recv.') === 0) {
205+
return self::KIND_SERVER;
206+
}
207+
208+
if ($span->timeEvents()) {
209+
foreach ($span->timeEvents() as $event) {
210+
if (!($event instanceof MessageEvent)) {
211+
continue;
212+
}
213+
214+
switch ($event->type()) {
215+
case MessageEvent::TYPE_SENT:
216+
return self::KIND_CLIENT;
217+
break;
218+
case MessageEvent::TYPE_RECEIVED:
219+
return self::KIND_SERVER;
220+
break;
221+
}
222+
}
223+
}
224+
225+
return null;
47226
}
48227
}

tests/unit/ZipkinExporterTest.php

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,162 @@
1818
namespace OpenCensus\Tests\Unit\Trace\Exporter;
1919

2020
use OpenCensus\Trace\Exporter\ZipkinExporter;
21+
use OpenCensus\Trace\MessageEvent;
22+
use OpenCensus\Trace\Span;
23+
use OpenCensus\Trace\SpanData;
2124
use PHPUnit\Framework\TestCase;
2225

26+
/**
27+
* @group trace
28+
*/
2329
class ZipkinExporterTest extends TestCase
2430
{
25-
public function testBasic()
31+
/**
32+
* @var SpanData[]
33+
*/
34+
private $spans;
35+
36+
public function setUp()
37+
{
38+
parent::setUp();
39+
$this->spans = array_map(function ($span) {
40+
return $span->spanData();
41+
}, [
42+
new Span([
43+
'traceId' => 'aaa',
44+
'name' => 'span',
45+
'startTime' => microtime(true),
46+
'endTime' => microtime(true) + 10
47+
])
48+
]);
49+
}
50+
51+
/**
52+
* http://zipkin.io/zipkin-api/#/paths/%252Fspans/post
53+
*/
54+
public function testFormatsTrace()
55+
{
56+
$exporter = new ZipkinExporter('myapp');
57+
$data = $exporter->convertSpans($this->spans);
58+
59+
$this->assertInternalType('array', $data);
60+
foreach ($data as $span) {
61+
$this->assertRegExp('/[0-9a-z]{16}/', $span['id']);
62+
$this->assertRegExp('/[0-9a-z]{32}/', $span['traceId']);
63+
$this->assertInternalType('string', $span['name']);
64+
$this->assertInternalType('int', $span['timestamp']);
65+
$this->assertInternalType('int', $span['duration']);
66+
67+
// make sure we have a JSON object, even when there is no tags
68+
$this->assertStringStartsWith('{', \json_encode($span['tags']));
69+
$this->assertStringEndsWith('}', \json_encode($span['tags']));
70+
71+
foreach ($span['tags'] as $key => $value) {
72+
$this->assertInternalType('string', $key);
73+
$this->assertInternalType('string', $value);
74+
}
75+
$this->assertFalse($span['shared']);
76+
$this->assertFalse($span['debug']);
77+
}
78+
}
79+
80+
/**
81+
* @dataProvider spanOptionsForKind
82+
*/
83+
public function testSpanKind($spanOpts, $kind)
84+
{
85+
$span = new Span($spanOpts);
86+
$span->setStartTime();
87+
$span->setEndTime();
88+
$exporter = new ZipkinExporter('myapp');
89+
$spans = $exporter->convertSpans([$span->spanData()]);
90+
91+
$this->assertEquals($kind, $spans[0]['kind']);
92+
}
93+
94+
public function spanOptionsForKind()
95+
{
96+
return [
97+
[['name' => 'Recv.Span1'], 'SERVER'],
98+
[['name' => 'Sent.Span2'], 'CLIENT'],
99+
[['name' => 'span3', 'timeEvents' => [new MessageEvent(MessageEvent::TYPE_RECEIVED, '')]], 'SERVER'],
100+
[['name' => 'span4', 'timeEvents' => [new MessageEvent(MessageEvent::TYPE_SENT, '')]], 'CLIENT'],
101+
[['kind' => Span::KIND_SERVER], 'SERVER'],
102+
[['kind' => Span::KIND_CLIENT], 'CLIENT'],
103+
[['kind' => Span::KIND_UNSPECIFIED], null]
104+
];
105+
}
106+
107+
public function testSpanDebug()
108+
{
109+
$exporter = new ZipkinExporter('myapp');
110+
$spans = $exporter->convertSpans($this->spans, [
111+
'HTTP_X_B3_FLAGS' => '1'
112+
]);
113+
114+
$this->assertCount(1, $spans);
115+
$this->assertTrue($spans[0]['debug']);
116+
}
117+
118+
public function testSpanShared()
119+
{
120+
$span = new Span(['parentSpanId' => 'abc']);
121+
$span->setStartTime();
122+
$span->setEndTime();
123+
124+
$exporter = new ZipkinExporter('myapp');
125+
$spans = $exporter->convertSpans([$span->spanData()]);
126+
127+
$this->assertCount(1, $spans);
128+
$this->assertTrue($spans[0]['shared']);
129+
}
130+
131+
public function testEmptyTrace()
132+
{
133+
$exporter = new ZipkinExporter('myapp');
134+
$spans = $exporter->convertSpans([]);
135+
$this->assertEmpty($spans);
136+
}
137+
138+
public function testSkipsIpv4()
139+
{
140+
$exporter = new ZipkinExporter('myapp');
141+
$spans = $exporter->convertSpans($this->spans);
142+
143+
$endpoint = $spans[0]['localEndpoint'];
144+
$this->assertArrayNotHasKey('ipv4', $endpoint);
145+
$this->assertArrayNotHasKey('ipv6', $endpoint);
146+
}
147+
148+
public function testSetsIpv4()
149+
{
150+
$exporter = new ZipkinExporter('myapp');
151+
$exporter->setLocalIpv4('1.2.3.4');
152+
$spans = $exporter->convertSpans($this->spans);
153+
154+
$endpoint = $spans[0]['localEndpoint'];
155+
$this->assertArrayHasKey('ipv4', $endpoint);
156+
$this->assertEquals('1.2.3.4', $endpoint['ipv4']);
157+
}
158+
159+
public function testSetsIpv6()
26160
{
27-
$this->markTestSkipped();
161+
$exporter = new ZipkinExporter('myapp');
162+
$exporter->setLocalIpv6('2001:db8:85a3::8a2e:370:7334');
163+
$spans = $exporter->convertSpans($this->spans);
164+
165+
$endpoint = $spans[0]['localEndpoint'];
166+
$this->assertArrayHasKey('ipv6', $endpoint);
167+
$this->assertEquals('2001:db8:85a3::8a2e:370:7334', $endpoint['ipv6']);
168+
}
169+
170+
public function testSetsLocalEndpointPort()
171+
{
172+
$exporter = new ZipkinExporter('myapp', null, ['SERVER_PORT' => "80"]);
173+
$spans = $exporter->convertSpans($this->spans);
174+
175+
$endpoint = $spans[0]['localEndpoint'];
176+
$this->assertArrayHasKey('port', $endpoint);
177+
$this->assertEquals(80, $endpoint['port']);
28178
}
29179
}

0 commit comments

Comments
 (0)