From 097b6a8007dde2b389afcf64ec83b07fe651e47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Fri, 9 Feb 2018 08:50:05 -0500 Subject: [PATCH] Adds tracing flow. --- composer.json | 11 +- src/DDTrace/Encoder.php | 19 ++ src/DDTrace/Encoders/Json.php | 72 +++++++ src/DDTrace/Encoders/Noop.php | 18 ++ src/DDTrace/Propagator.php | 29 +++ src/DDTrace/Propagators/Noop.php | 25 +++ src/DDTrace/Propagators/TextMap.php | 52 +++++ src/DDTrace/Span.php | 204 ++++++++++-------- src/DDTrace/SpanContext.php | 107 +++++++++ src/DDTrace/{Meta.php => Tags.php} | 2 +- src/DDTrace/{MicroTime.php => Time.php} | 12 +- src/DDTrace/Tracer.php | 168 ++++++++++++++- src/DDTrace/Transport/Http.php | 83 +++++++ src/DDTrace/Transport/Noop.php | 2 +- src/DDTrace/Types.php | 17 ++ tests/DDTrace/Unit/Encoders/JsonTest.php | 33 +++ tests/{ => DDTrace}/Unit/SpanTest.php | 27 ++- .../Unit/TimeTest.php} | 10 +- 18 files changed, 757 insertions(+), 134 deletions(-) create mode 100644 src/DDTrace/Encoder.php create mode 100644 src/DDTrace/Encoders/Json.php create mode 100644 src/DDTrace/Encoders/Noop.php create mode 100644 src/DDTrace/Propagator.php create mode 100644 src/DDTrace/Propagators/Noop.php create mode 100644 src/DDTrace/Propagators/TextMap.php create mode 100644 src/DDTrace/SpanContext.php rename src/DDTrace/{Meta.php => Tags.php} (85%) rename src/DDTrace/{MicroTime.php => Time.php} (64%) create mode 100644 src/DDTrace/Transport/Http.php create mode 100644 src/DDTrace/Types.php create mode 100644 tests/DDTrace/Unit/Encoders/JsonTest.php rename tests/{ => DDTrace}/Unit/SpanTest.php (67%) rename tests/{Unit/MicroTimeTest.php => DDTrace/Unit/TimeTest.php} (73%) diff --git a/composer.json b/composer.json index 1ec6622b3cf..792d7b9c0ec 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,9 @@ "minimum-stability": "dev", "require": { "opentracing/opentracing": "1.0.0-beta2", - "symfony/polyfill": "~1.7.0" + "psr/log": "~1.0.2", + "symfony/polyfill": "~1.7.0", + "guzzlehttp/psr7": "^1.4@dev" }, "require-dev": { "phpunit/phpunit": "~5.7.19", @@ -29,13 +31,14 @@ "DDTrace\\": "./src/DDTrace/" }, "files": [ - "./src/DDTrace/Meta.php", - "./src/DDTrace/MicroTime.php" + "./src/DDTrace/Tags.php", + "./src/DDTrace/Time.php", + "./src/DDTrace/Types.php" ] }, "autoload-dev": { "psr-4": { - "DDTrace\\Tests\\": "./tests/" + "DDTrace\\Tests\\": "./tests/DdTrace/" } }, "scripts": { diff --git a/src/DDTrace/Encoder.php b/src/DDTrace/Encoder.php new file mode 100644 index 00000000000..10cc8ae32b2 --- /dev/null +++ b/src/DDTrace/Encoder.php @@ -0,0 +1,19 @@ +getStartTime() . '000', + '"duration":' . $span->getDuration() . '000', + ], json_encode(Json::spanToArray($span))); + } + + public function getContentType() + { + return 'application/json'; + } + + /** + * @param Span $span + * @return array + */ + private static function spanToArray(Span $span) + { + $arraySpan = [ + 'trace_id' => $span->getTraceId(), + 'span_id' => $span->getSpanId(), + 'name' => $span->getOperationName(), + 'resource' => $span->getResource(), + 'service' => $span->getService(), + 'start_micro' => 0, + 'error' => $span->hasError() ? 1 : 0, + ]; + + if ($span->getType() !== null) { + $arraySpan['type'] = $span->getType(); + } + + if ($span->isFinished()) { + $arraySpan['duration_micro'] = 0; + } + + if ($span->getParentId() !== null) { + $arraySpan['parent_id'] = $span->getParentId(); + } + + if (!empty($span->getAllTags())) { + $arraySpan['meta'] = $span->getAllTags(); + } + + return $arraySpan; + } +} diff --git a/src/DDTrace/Encoders/Noop.php b/src/DDTrace/Encoders/Noop.php new file mode 100644 index 00000000000..6b9c7db2a49 --- /dev/null +++ b/src/DDTrace/Encoders/Noop.php @@ -0,0 +1,18 @@ +getTraceId(); + $carrier[self::DEFAULT_PARENT_ID_HEADER] = $spanContext->getSpanId(); + + foreach ($spanContext as $key => $value) { + $carrier[self::DEFAULT_BAGGAGE_HEADER_PREFIX . $key] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function extract($carrier) + { + $traceId = null; + $spanId = null; + $baggageItems = []; + + foreach ($carrier as $key => $value) { + if ($key === self::DEFAULT_TRACE_ID_HEADER) { + $traceId = $value; + } elseif ($key === self::DEFAULT_PARENT_ID_HEADER) { + $spanId = $value; + } elseif (strpos(self::DEFAULT_BAGGAGE_HEADER_PREFIX, $key) === 0) { + $baggageItems[substr($key, strlen(self::DEFAULT_BAGGAGE_HEADER_PREFIX))] = $value; + } + } + + if ($traceId === null && $spanId === null) { + return null; + } + + return new SpanContext($traceId, $spanId, null, $baggageItems); + } +} diff --git a/src/DDTrace/Span.php b/src/DDTrace/Span.php index e5cbf26ad96..6e7e11634c8 100644 --- a/src/DDTrace/Span.php +++ b/src/DDTrace/Span.php @@ -4,38 +4,12 @@ use Exception; use InvalidArgumentException; +use OpenTracing\SpanContext as OpenTracingContext; use Throwable; +use OpenTracing\Span as OpenTracingSpan; -final class Span +final class Span implements OpenTracingSpan { - /** - * @var Tracer - */ - private $tracer; - - /** - * The unique integer (64-bit unsigned) ID of the trace containing this span. - * It is stored in hexadecimal representation. - * - * @var string - */ - private $traceId; - - /** - * The span integer ID of the parent span. - * - * @var string - */ - private $parentId; - - /** - * The span integer (64-bit unsigned) ID. - * It is stored in hexadecimal representation. - * - * @var string - */ - private $spanId; - /** * Name is the name of the operation being measured. Some examples * might be "http.handler", "fileserver.upload" or "video.decompress". @@ -43,7 +17,12 @@ final class Span * * @var string */ - private $name; + private $operationName; + + /** + * @var OpenTracingContext + */ + private $context; /** * Resource is a query to a service. A web application might use @@ -72,14 +51,14 @@ final class Span /** * Protocol associated with the span * - * @var string + * @var string|null */ private $type; /** * @var int */ - private $start; + private $startTime; /** * @var int|null @@ -89,31 +68,34 @@ final class Span /** * @var array */ - private $meta = []; + private $tags = []; /** * @var bool */ private $hasError = false; + + /** + * Span constructor. + * @param string $operationName + * @param SpanContext $context + * @param string $service + * @param string $resource + * @param int|null $start + */ public function __construct( - Tracer $tracer, - $name, + $operationName, + SpanContext $context, $service, $resource, - $traceId, - $spanId, - $parentId = null, $start = null ) { - $this->tracer = $tracer; - $this->name = (string) $name; - $this->service = $service; + $this->context = $context; + $this->operationName = (string) $operationName; + $this->service = (string) $service; $this->resource = (string) $resource; - $this->start = $start ?: MicroTime\now(); - $this->traceId = $traceId; - $this->spanId = $spanId; - $this->parentId = $parentId; + $this->startTime = $start ?: Time\now(); } /** @@ -121,7 +103,7 @@ public function __construct( */ public function getTraceId() { - return $this->traceId; + return $this->context->getTraceId(); } /** @@ -129,7 +111,7 @@ public function getTraceId() */ public function getSpanId() { - return $this->spanId; + return $this->context->getSpanId(); } /** @@ -137,24 +119,15 @@ public function getSpanId() */ public function getParentId() { - return $this->parentId; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; + return $this->context->getParentId(); } /** - * @param string $name - * @return void + * {@inheritdoc} */ - public function setName($name) + public function overwriteOperationName($operationName) { - $this->name = $name; + $this->operationName = $operationName; } /** @@ -174,7 +147,7 @@ public function getService() } /** - * @return string + * @return string|null */ public function getType() { @@ -184,9 +157,9 @@ public function getType() /** * @return int */ - public function getStart() + public function getStartTime() { - return $this->start; + return $this->startTime; } /** @@ -198,41 +171,46 @@ public function getDuration() } /** - * Adds an arbitrary meta field to the current Span. - * If the Span has been finished, it will not be modified by the method. - * - * @param string $key - * @param string $value - * @throws InvalidArgumentException + * {@inheritdoc} */ - public function setMeta($key, $value) + public function setTags(array $tags) { if ($this->isFinished()) { return; } - if ($key !== (string) $key) { - throw new InvalidArgumentException( - sprintf('First argument expected to be string, got %s', gettype($key)) - ); - } + foreach ($tags as $key => $value) { + if ($key !== (string) $key) { + throw new InvalidArgumentException( + sprintf('First argument expected to be string, got %s', gettype($key)) + ); + } - $this->meta[$key] = (string) $value; + $this->tags[$key] = (string) $value; + } } /** * @param string $key * @return string|null */ - public function getMeta($key) + public function getTag($key) { - if (array_key_exists($key, $this->meta)) { - return $this->meta[$key]; + if (array_key_exists($key, $this->tags)) { + return $this->tags[$key]; } return null; } + /** + * @return array + */ + public function getAllTags() + { + return $this->tags; + } + /** * Stores a Throwable object within the span meta. The error status is * updated and the error.Error() string is included with a default meta key. @@ -249,9 +227,12 @@ public function setError($e) if (($e instanceof Exception) || ($e instanceof Throwable)) { $this->hasError = true; - $this->setMeta(Meta\ERROR_MSG_KEY, $e->getMessage()); - $this->setMeta(Meta\ERROR_TYPE_KEY, get_class($e)); - $this->setMeta(Meta\ERROR_STACK_KEY, $e->getTraceAsString()); + $this->setTags([ + Tags\ERROR_MSG_KEY => $e->getMessage(), + Tags\ERROR_TYPE_KEY => get_class($e), + Tags\ERROR_STACK_KEY => $e->getTraceAsString(), + ]); + return; } @@ -266,23 +247,15 @@ public function hasError() } /** - * Finish closes this Span (but not its children) providing the duration - * of this part of the tracing session. This method is idempotent so - * calling this method multiple times is safe and doesn't update the - * current Span. Once a Span has been finished, methods that modify the Span - * will become no-ops. - * - * @param int|null $finish - * @return void + * {@inheritdoc} */ - public function finish($finish = null) + public function finish($finishTime = null, array $logRecords = []) { if ($this->isFinished()) { return; } - $this->duration = ($finish ?: MicroTime\now()) - $this->start; - $this->tracer->record($this); + $this->duration = ($finishTime ?: Time\now()) - $this->startTime; } /** @@ -295,8 +268,51 @@ public function finishWithError($e) $this->finish(); } - private function isFinished() + /** + * @return bool + */ + public function isFinished() { return $this->duration !== null; } + + /** + * {@inheritdoc} + */ + public function getOperationName() + { + return $this->operationName; + } + + /** + * {@inheritdoc} + */ + public function getContext() + { + return $this->context; + } + + /** + * {@inheritdoc} + */ + public function log(array $fields = [], $timestamp = null) + { + // TODO: Implement log() method. + } + + /** + * {@inheritdoc} + */ + public function addBaggageItem($key, $value) + { + $this->context = $this->context->withBaggageItem($key, $value); + } + + /** + * {@inheritdoc} + */ + public function getBaggageItem($key) + { + return $this->context->getBaggageItem($key); + } } diff --git a/src/DDTrace/SpanContext.php b/src/DDTrace/SpanContext.php new file mode 100644 index 00000000000..a8b39164767 --- /dev/null +++ b/src/DDTrace/SpanContext.php @@ -0,0 +1,107 @@ +traceId = $traceId; + $this->spanId = $spanId; + $this->parentId = $parentId; + $this->baggageItems = $baggageItems; + } + + public static function createAsChild(SpanContext $parentContext) + { + return new self( + $parentContext->traceId, + self::nextId(), + $parentContext->spanId, + $parentContext->baggageItems + ); + } + + public static function createAsRoot(array $baggageItems = []) + { + $nextId = self::nextId(); + + return new self( + $nextId, + $nextId, + null, + $baggageItems + ); + } + + public function getTraceId() + { + return $this->traceId; + } + + public function getSpanId() + { + return $this->spanId; + } + + public function getParentId() + { + return $this->parentId; + } + + public function getIterator() + { + return new ArrayIterator($this->baggageItems); + } + + public function getBaggageItem($key) + { + return array_key_exists($key, $this->baggageItems) + ? $this->baggageItems[$key] + : null; + } + + public function withBaggageItem($key, $value) + { + return new self( + $this->traceId, + $this->spanId, + $this->parentId, + array_merge($this->baggageItems, [$key => $value]) + ); + } + + private static function nextId() + { + return bin2hex(openssl_random_pseudo_bytes(8)); + } +} diff --git a/src/DDTrace/Meta.php b/src/DDTrace/Tags.php similarity index 85% rename from src/DDTrace/Meta.php rename to src/DDTrace/Tags.php index b8675713d5e..64ee75a3dcb 100644 --- a/src/DDTrace/Meta.php +++ b/src/DDTrace/Tags.php @@ -1,6 +1,6 @@ true, + /** + * Debug, when true, writes details to logs. + */ + 'debug' => false, + /** + * ServiceName specifies the name of this application. + */ + 'service_name' => PHP_SAPI, + /** GlobalTags holds a set of tags that will be automatically applied to + * all spans. + */ + 'global_tags' => [], + ]; + + /** + * Tracer constructor. + * @param Transport $transport + * @param Propagator[] $propagators + * @param array $config + */ + public function __construct(Transport $transport, array $propagators = [], array $config = []) { $this->transport = $transport; + $this->propagators = $propagators; + $this->config = array_merge($this->config, $config); } public static function noop() { - return new self(new Noop); + return new self( + new NoopTransport, + [ + Formats\BINARY => new NoopPropagator, + Formats\TEXT_MAP => new NoopPropagator, + Formats\HTTP_HEADERS => new NoopPropagator, + ], + ['enabled' => false] + ); + } + + /** + * {@inheritdoc} + */ + public function startSpan($operationName, $options = []) + { + if (!$this->config['enabled']) { + return NoopSpan::create(); + } + + if (!($options instanceof SpanOptions)) { + $options = SpanOptions::create($options); + } + + if (($context = $this->findChild($options->getReferences())) === null) { + $context = SpanContext::createAsRoot(); + } + + $span = new Span( + $operationName, + $context, + $this->config['service_name'], + $this->config['resource'] + ); + + $span->setTags($this->config['global_tags']); + + $this->record($span); + + return $span; + } + + /** + * @param array|Reference[] $references + * @return null|Reference + */ + private function findChild(array $references) + { + foreach ($references as $reference) { + if ($reference->isType(Reference::CHILD_OF)) { + return $reference; + } + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function inject(OpenTracingContext $spanContext, $format, &$carrier) + { + if (array_key_exists($format, $this->propagators)) { + $this->propagators[$format]->inject($spanContext, $carrier); + return; + } + + throw UnsupportedFormat::forFormat($format); } /** - * @param Span $span + * {@inheritdoc} */ - public function record(Span $span) + public function extract($format, $carrier) { - $this->traces[$span->getTraceId()][] = $span; + if (array_key_exists($format, $this->propagators)) { + return $this->propagators[$format]->extract($carrier); + } + + throw UnsupportedFormat::forFormat($format); } /** @@ -39,6 +155,40 @@ public function record(Span $span) */ public function flush() { - $this->transport->send($this->traces); + if (!$this->config['enabled']) { + return; + } + + $tracesToBeSent = []; + + foreach ($this->traces as $trace) { + $traceToBeSent = []; + + foreach ($trace as $span) { + if (!$span->isFinished()) { + $traceToBeSent = null; + break; + } + $tracesToBeSent[] = $span; + } + + if ($traceToBeSent === null) { + continue; + } + + $tracesToBeSent[] = $traceToBeSent; + unset($this->traces[$span->getTraceId()]); + } + + $this->transport->send($tracesToBeSent); + } + + private function record(Span $span) + { + if (!array_key_exists($span->getTraceId(), $this->traces)) { + $this->traces[$span->getTraceId()] = []; + } + + $this->traces[$span->getTraceId()][$span->getSpanId()] = $span; } } diff --git a/src/DDTrace/Transport/Http.php b/src/DDTrace/Transport/Http.php new file mode 100644 index 00000000000..b57605f99f5 --- /dev/null +++ b/src/DDTrace/Transport/Http.php @@ -0,0 +1,83 @@ +encoder = $encoder; + $this->config = array_merge([ + 'endpoint' => self::DEFAULT_ENDPOINT, + ]); + } + + public function send(array $traces) + { + $tracesPayload = $this->encoder->encodeTraces($traces); + + $headers = $this->headers + ['Content-Type' => $this->encoder->getContentType()]; + + $this->sendRequest($this->config['endpoint'], $headers, $tracesPayload); + } + + public function setHeader($key, $value) + { + $this->headers[(string) $key] = (string) $value; + } + + private function sendRequest($url, array $headers, $body) + { + $handle = curl_init($url); + curl_setopt($handle, CURLOPT_POST, 1); + curl_setopt($handle, CURLOPT_POSTFIELDS, $body); + curl_setopt($handle, CURLOPT_HTTPHEADER, array_merge([ + 'Content-Type: ' . $this->encoder->getContentType(), + 'Content-Length: ' . strlen($body), + ], $headers)); + + if (curl_exec($handle) === true) { + $statusCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); + curl_close($handle); + + if ($statusCode === 415) { + throw new RuntimeException('Reporting of spans failed, try with version 0.2'); + } + + if ($statusCode !== 200) { + throw new RuntimeException( + sprintf('Reporting of spans failed, status code %d', $statusCode) + ); + } + } else { + throw new RuntimeException(sprintf( + 'Reporting of spans failed: %s, error code %s', + curl_error($handle), + curl_errno($handle) + )); + } + } +} diff --git a/src/DDTrace/Transport/Noop.php b/src/DDTrace/Transport/Noop.php index 872a55710a6..b9b92644c76 100644 --- a/src/DDTrace/Transport/Noop.php +++ b/src/DDTrace/Transport/Noop.php @@ -5,7 +5,7 @@ use DDTrace\Transport; use GuzzleHttp\Psr7\Response; -class Noop implements Transport +final class Noop implements Transport { public function send(array $traces) { diff --git a/src/DDTrace/Types.php b/src/DDTrace/Types.php new file mode 100644 index 00000000000..bff1d412a0e --- /dev/null +++ b/src/DDTrace/Types.php @@ -0,0 +1,17 @@ +encodeTraces([[$span]]); + $this->assertEquals($expectedPayload, $encodedTrace); + } +} diff --git a/tests/Unit/SpanTest.php b/tests/DDTrace/Unit/SpanTest.php similarity index 67% rename from tests/Unit/SpanTest.php rename to tests/DDTrace/Unit/SpanTest.php index d3147a56765..6b0e8dbefa0 100644 --- a/tests/Unit/SpanTest.php +++ b/tests/DDTrace/Unit/SpanTest.php @@ -2,9 +2,9 @@ namespace DDTrace\Tests\Unit; -use DDTrace\Meta; +use DDTrace\SpanContext; +use DDTrace\Tags; use DDTrace\Span; -use DDTrace\Tracer; use Exception; use PHPUnit_Framework_TestCase; @@ -20,12 +20,12 @@ final class SpanTest extends PHPUnit_Framework_TestCase public function testCreateSpanSuccess() { $span = $this->createSpan(); - $span->setMeta(self::META_KEY, self::META_VALUE); + $span->setTags([self::META_KEY => self::META_VALUE]); - $this->assertSame(self::NAME, $span->getName()); + $this->assertSame(self::NAME, $span->getOperationName()); $this->assertSame(self::SERVICE, $span->getService()); $this->assertSame(self::RESOURCE, $span->getResource()); - $this->assertSame(self::META_VALUE, $span->getMeta(self::META_KEY)); + $this->assertSame(self::META_VALUE, $span->getTag(self::META_KEY)); } public function testSpanMetaRemainsImmutableAfterFinishing() @@ -33,8 +33,8 @@ public function testSpanMetaRemainsImmutableAfterFinishing() $span = $this->createSpan(); $span->finish(); - $span->setMeta(self::META_KEY, self::META_VALUE); - $this->assertNull($span->getMeta(self::META_KEY)); + $span->setTags([self::META_KEY => self::META_VALUE]); + $this->assertNull($span->getTag(self::META_KEY)); } public function testSpanErrorAddsExpectedMeta() @@ -43,8 +43,8 @@ public function testSpanErrorAddsExpectedMeta() $span->setError(new Exception(self::EXCEPTION_MESSAGE)); $this->assertTrue($span->hasError()); - $this->assertEquals($span->getMeta(Meta\ERROR_MSG_KEY), self::EXCEPTION_MESSAGE); - $this->assertEquals($span->getMeta(Meta\ERROR_TYPE_KEY), Exception::class); + $this->assertEquals($span->getTag(Tags\ERROR_MSG_KEY), self::EXCEPTION_MESSAGE); + $this->assertEquals($span->getTag(Tags\ERROR_TYPE_KEY), Exception::class); } public function testSpanErrorRemainsImmutableAfterFinishing() @@ -58,14 +58,13 @@ public function testSpanErrorRemainsImmutableAfterFinishing() private function createSpan() { - $tracer = Tracer::noop(); + $context = SpanContext::createAsRoot(); + $span = new Span( - $tracer, self::NAME, + $context, self::SERVICE, - self::RESOURCE, - 'abc123', - 'abc123' + self::RESOURCE ); return $span; diff --git a/tests/Unit/MicroTimeTest.php b/tests/DDTrace/Unit/TimeTest.php similarity index 73% rename from tests/Unit/MicroTimeTest.php rename to tests/DDTrace/Unit/TimeTest.php index 6217161391d..b0365b6068a 100644 --- a/tests/Unit/MicroTimeTest.php +++ b/tests/DDTrace/Unit/TimeTest.php @@ -2,13 +2,13 @@ namespace DDTrace\Tests; -use DDTrace\MicroTime; +use DDTrace\Time; -final class MicroTimeTest extends \PHPUnit_Framework_TestCase +final class TimeTest extends \PHPUnit_Framework_TestCase { public function testNowHasTheExpectedLength() { - $now = MicroTime\now(); + $now = Time\now(); $this->assertEquals(16, strlen((string) $now)); } @@ -17,13 +17,13 @@ public function testNowHasTheExpectedLength() */ public function testIsValidHasTheExpectedOutput($microtime, $isValid) { - $this->assertEquals($isValid, MicroTime\isValid($microtime)); + $this->assertEquals($isValid, Time\isValid($microtime)); } public function microtimeProvider() { return [ - [MicroTime\now(), true], + [Time\now(), true], [1234567890123456, true], [123456789012345, false], ['1234567890123456', false],