diff --git a/README.md b/README.md index fce9bf1e..01d876c2 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,7 @@ request. To do this, simply pass null to `openScope`. ## Instrumentation - [PSR18 HTTP Client](src/Zipkin/Instrumentation/Http/Client) +- [PSR15 HTTP Server](src/Zipkin/Instrumentation/Http/Server) ## Tests diff --git a/composer.json b/composer.json index 103b5fa0..3af9fab2 100644 --- a/composer.json +++ b/composer.json @@ -4,16 +4,21 @@ "description": "A Zipkin instrumentation for PHP", "keywords": [ "zipkin", + "distributed-tracing", "tracing", "openzipkin" ], - "license": "MIT", + "license": "Apache-2.0", "authors": [ { "name": "José Carlos Chávez", "email": "jcchavezs@gmail.com" } ], + "homepage": "https://github.com/openzipkin/zipkin-php", + "support": { + "issues": "https://github.com/openzipkin/zipkin-php/issues" + }, "require": { "php": "^7.1", "ext-curl": "*", @@ -21,11 +26,14 @@ "psr/log": "^1.0" }, "require-dev": { - "guzzlehttp/psr7": "^1.4", + "guzzlehttp/psr7": "^1.6", "jcchavezs/httptest": "~0.2", "phpstan/phpstan": "~0.12.28", "phpunit/phpunit": "~7.5.20", "psr/http-client": "^1.0", + "psr/http-server-middleware": "^1.0", + "middlewares/fast-route": "^1.2.1", + "middlewares/request-handler": "^1.4.0", "squizlabs/php_codesniffer": "3.*" }, "config": { @@ -60,7 +68,8 @@ "static-check": "phpstan analyse src --level 8" }, "suggest": { - "psr/http-client": "Allows to instrument HTTP clients following PSR18." + "psr/http-client": "Allows to instrument HTTP clients following PSR18.", + "psr/http-server-middleware": "Allows to instrument HTTP servers via middlewares following PSR15." }, "extra": { "branch-alias": { diff --git a/phpstan.neon b/phpstan.neon index 2be0b6da..708fce94 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -29,7 +29,16 @@ parameters: # In general types are desired but carrier is an special case message: '#has parameter \$carrier with no typehint specified#' path: src/Zipkin/* - - - # Somehow this started failing - message: '#Constant .* already defined#' - path: src/Zipkin \ No newline at end of file + - + message: '#Method .* with no typehint specified.#' + path: src/Zipkin/Instrumentation/Http/Server + - + message: '#Method .* with no typehint specified.#' + path: src/Zipkin/Instrumentation/Http/Client + #- + # # Somehow this started failing + # message: '#Constant .* already defined#' + # path: src/Zipkin + - + message: '#should return static#' + path: src/Zipkin/Instrumentation/Http/Client/Psr/Request.php diff --git a/src/Zipkin/Instrumentation/Http/Client/ClientTracing.php b/src/Zipkin/Instrumentation/Http/Client/ClientTracing.php index 440f0457..b33d7d61 100644 --- a/src/Zipkin/Instrumentation/Http/Client/ClientTracing.php +++ b/src/Zipkin/Instrumentation/Http/Client/ClientTracing.php @@ -5,6 +5,10 @@ namespace Zipkin\Instrumentation\Http\Client; use Zipkin\Tracing; +use Zipkin\Tracer; +use Zipkin\Span; +use Zipkin\Propagation\SamplingFlags; +use Zipkin\Instrumentation\Http\Client\Parser; /** * ClientTracing includes all the elements needed to instrument a @@ -23,25 +27,32 @@ class ClientTracing private $parser; /** - * function that decides to sample or not an unsampled - * request. The signature is: - * - *
- * function (RequestInterface $request): ?bool {}
- *
- *
- * @var callable|null
+ * @var callable
*/
- private $requestSampler;
+ private $nextSpanResolver;
+ /**
+ * @param Tracing $tracing
+ * @param Parser $parser HTTP client parser to obtain meaningful information from
+ * request and response and tag the span accordingly.
+ * @param callable(mixed):?bool $requestSampler function that decides to sample or not an unsampled
+ * request.
+ * @param callable(mixed):?SamplingFlags $traceContextObtainer function that obtains the context from the
+ * request. It is mostly used in event loop scenaries where the global scope can't be used.
+ */
public function __construct(
Tracing $tracing,
Parser $parser = null,
- callable $requestSampler = null
+ callable $requestSampler = null,
+ callable $traceContextObtainer = null
) {
$this->tracing = $tracing;
- $this->parser = $parser ?? new DefaultParser;
- $this->requestSampler = $requestSampler;
+ $this->parser = $parser ?? new NoopParser;
+ $this->nextSpanResolver = self::buildNextSpanResolver(
+ $tracing->getTracer(),
+ $requestSampler,
+ $traceContextObtainer
+ );
}
public function getTracing(): Tracing
@@ -49,20 +60,48 @@ public function getTracing(): Tracing
return $this->tracing;
}
+ public function getParser(): Parser
+ {
+ return $this->parser;
+ }
+
/**
- * @return callable with the signature:
- *
- * - * function (RequestInterface $request): ?bool - *+ * @return callable(mixed):Span the next span handler which creates an appropriate span based on the current scope, + * and the incoming request. */ - public function getRequestSampler(): ?callable + public function getNextSpanResolver(): callable { - return $this->requestSampler; + return $this->nextSpanResolver; } - public function getParser(): Parser - { - return $this->parser; + private static function buildNextSpanResolver( + Tracer $tracer, + ?callable $requestSampler, + ?callable $traceContextObtainer + ): callable { + return static function ($request) use ($tracer, $requestSampler, $traceContextObtainer): Span { + if ($traceContextObtainer !== null) { + // in this case, the trace context is meant to be obtained from the request + $traceContext = ($traceContextObtainer)($request); + + if ($requestSampler !== null) { + return $tracer->nextSpanWithSampler( + $requestSampler, + [$request], + $traceContext + ); + } + return $tracer->nextSpan($traceContext); + } + + if ($requestSampler !== null) { + return $tracer->nextSpanWithSampler( + $requestSampler, + [$request] + ); + } + + return $tracer->nextSpan(); + }; } } diff --git a/src/Zipkin/Instrumentation/Http/Client/DefaultParser.php b/src/Zipkin/Instrumentation/Http/Client/DefaultParser.php deleted file mode 100644 index 7ec3b2f9..00000000 --- a/src/Zipkin/Instrumentation/Http/Client/DefaultParser.php +++ /dev/null @@ -1,34 +0,0 @@ -getMethod(); - } - - public function request(RequestInterface $request, TraceContext $context, SpanCustomizer $span): void - { - $span->tag(Tags\HTTP_METHOD, $request->getMethod()); - $span->tag(Tags\HTTP_PATH, $request->getUri()->getPath() ?: "/"); - } - - public function response(ResponseInterface $response, TraceContext $context, SpanCustomizer $span): void - { - $span->tag(Tags\HTTP_STATUS_CODE, (string) $response->getStatusCode()); - if ($response->getStatusCode() > 399) { - $span->tag(Tags\ERROR, (string) $response->getStatusCode()); - } - } -} diff --git a/src/Zipkin/Instrumentation/Http/Client/NoopParser.php b/src/Zipkin/Instrumentation/Http/Client/NoopParser.php new file mode 100644 index 00000000..5b8dcbcd --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Client/NoopParser.php @@ -0,0 +1,29 @@ +delegate = $delegate; $this->injector = $tracing->getTracing()->getPropagation()->getInjector(new RequestHeaders()); - $this->tracer = $tracing->getTracing()->getTracer(); $this->parser = $tracing->getParser(); - $this->requestSampler = $tracing->getRequestSampler(); + $this->nextSpanResolver = $tracing->getNextSpanResolver(); } public function sendRequest(RequestInterface $request): ResponseInterface { - if ($this->requestSampler === null) { - $span = $this->tracer->nextSpan(); - } else { - $span = $this->tracer->nextSpanWithSampler( - $this->requestSampler, - [$request] - ); - } - + /** + * @var Span $span + */ + $span = ($this->nextSpanResolver)($request); ($this->injector)($span->getContext(), $request); if ($span->isNoop()) { diff --git a/src/Zipkin/Instrumentation/Http/Client/Psr/DefaultParser.php b/src/Zipkin/Instrumentation/Http/Client/Psr/DefaultParser.php new file mode 100644 index 00000000..0b3464ee --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Client/Psr/DefaultParser.php @@ -0,0 +1,48 @@ +getMethod(); + } + + public function request(/*RequestInterface */$request, TraceContext $context, SpanCustomizer $span): void + { + self::assertRequestType($request); + + $span->tag(Tags\HTTP_METHOD, $request->getMethod()); + $span->tag(Tags\HTTP_PATH, $request->getUri()->getPath() ?: "/"); + } + + public function response(/*ResponseInterface */$response, TraceContext $context, SpanCustomizer $span): void + { + self::assertResponseType($response); + + $span->tag(Tags\HTTP_STATUS_CODE, (string) $response->getStatusCode()); + if ($response->getStatusCode() > 399) { + $span->tag(Tags\ERROR, (string) $response->getStatusCode()); + } + } + + private static function assertRequestType(RequestInterface $request): void + { + } + + private static function assertResponseType(ResponseInterface $response): void + { + } +} diff --git a/src/Zipkin/Instrumentation/Http/Client/README.md b/src/Zipkin/Instrumentation/Http/Client/Psr/README.md similarity index 92% rename from src/Zipkin/Instrumentation/Http/Client/README.md rename to src/Zipkin/Instrumentation/Http/Client/Psr/README.md index 1316838a..52131f46 100644 --- a/src/Zipkin/Instrumentation/Http/Client/README.md +++ b/src/Zipkin/Instrumentation/Http/Client/Psr/README.md @@ -24,7 +24,7 @@ $tracing = TracingBuilder::create() ->havingLocalServiceName('my_service') ->build(); -$httpClientTracing = new ClientTracing($tracing); +$httpClientTracing = new ClientTracing($tracing, new DefaultParser); ... $httpClient = new ZipkinClient(new Client, $httpClientTracing); diff --git a/src/Zipkin/Instrumentation/Http/Client/RequestHeaders.php b/src/Zipkin/Instrumentation/Http/Client/Psr/RequestHeaders.php similarity index 86% rename from src/Zipkin/Instrumentation/Http/Client/RequestHeaders.php rename to src/Zipkin/Instrumentation/Http/Client/Psr/RequestHeaders.php index 374bfe03..1a68a188 100644 --- a/src/Zipkin/Instrumentation/Http/Client/RequestHeaders.php +++ b/src/Zipkin/Instrumentation/Http/Client/Psr/RequestHeaders.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Zipkin\Instrumentation\Http\Client; +namespace Zipkin\Instrumentation\Http\Client\Psr; use Zipkin\Propagation\RequestHeaders as BaseRequestHeaders; use Zipkin\Propagation\RemoteSetter; diff --git a/src/Zipkin/Instrumentation/Http/Client/Psr/TraceContextRequest.php b/src/Zipkin/Instrumentation/Http/Client/Psr/TraceContextRequest.php new file mode 100644 index 00000000..fb506821 --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Client/Psr/TraceContextRequest.php @@ -0,0 +1,133 @@ +delegate = $request; + $this->context = $context; + } + + public static function wrap(RequestInterface $request, TraceContext $context): self + { + return new self($request, $context); + } + + public static function obtainContext(RequestInterface $request): ?SamplingFlags + { + if ($request instanceof self) { + return $request->context; + } + + return null; + } + + public function getTraceContext(): TraceContext + { + return $this->context; + } + + public function getRequestTarget() + { + return $this->delegate->getRequestTarget(); + } + + public function withRequestTarget($requestTarget) + { + return new self($this->delegate->withRequestTarget($requestTarget), $this->context); + } + + public function getMethod() + { + return $this->delegate->getMethod(); + } + + public function withMethod($method) + { + return new self($this->delegate->withMethod($method), $this->context); + } + + public function getUri() + { + return $this->delegate->getUri(); + } + + public function withUri(UriInterface $uri, $preserveHost = false) + { + return new self($this->delegate->withUri($uri, $preserveHost), $this->context); + } + + public function getProtocolVersion() + { + return $this->delegate->getProtocolVersion(); + } + + public function withProtocolVersion($version) + { + return new self($this->delegate->withProtocolVersion($version), $this->context); + } + + public function getHeaders() + { + return $this->delegate->getHeaders(); + } + + public function hasHeader($name) + { + return $this->delegate->hasHeader($name); + } + + public function getHeader($name) + { + return $this->delegate->getHeader($name); + } + + public function getHeaderLine($name) + { + return $this->delegate->getHeaderLine($name); + } + + public function withHeader($name, $value) + { + return new self($this->delegate->withHeader($name, $value), $this->context); + } + + public function withAddedHeader($name, $value) + { + return new self($this->delegate->withAddedHeader($name, $value), $this->context); + } + + public function withoutHeader($name) + { + return new self($this->delegate->withoutHeader($name), $this->context); + } + public function getBody() + { + return $this->delegate->getBody(); + } + + public function withBody(StreamInterface $body) + { + return new self($this->delegate->withBody($body), $this->context); + } +} diff --git a/src/Zipkin/Instrumentation/Http/Server/NoopParser.php b/src/Zipkin/Instrumentation/Http/Server/NoopParser.php new file mode 100644 index 00000000..466b3af2 --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/NoopParser.php @@ -0,0 +1,29 @@ +getMethod(); + } + + public function request(/*ServerRequestInterface */$request, TraceContext $context, SpanCustomizer $span): void + { + self::assertRequestType($request); + + $span->tag(Tags\HTTP_METHOD, $request->getMethod()); + $span->tag(Tags\HTTP_PATH, $request->getUri()->getPath() ?: "/"); + } + + public function response(/*ResponseInterface */$response, TraceContext $context, SpanCustomizer $span): void + { + self::assertResponseType($response); + + $span->tag(Tags\HTTP_STATUS_CODE, (string) $response->getStatusCode()); + if ($response->getStatusCode() > 399) { + $span->tag(Tags\ERROR, (string) $response->getStatusCode()); + } + } + + private static function assertRequestType(ServerRequestInterface $request): void + { + } + + private static function assertResponseType(ResponseInterface $response): void + { + } +} diff --git a/src/Zipkin/Instrumentation/Http/Server/Psr/Middleware.php b/src/Zipkin/Instrumentation/Http/Server/Psr/Middleware.php new file mode 100644 index 00000000..d4f33141 --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/Psr/Middleware.php @@ -0,0 +1,85 @@ +tracer = $tracing->getTracing()->getTracer(); + $this->extractor = $tracing->getTracing()->getPropagation()->getExtractor(new RequestHeaders()); + $this->parser = $tracing->getParser(); + $this->nextSpanResolver = $tracing->getNextSpanResolver(); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $extractedContext = ($this->extractor)($request); + + /** + * @var Span $span + */ + $span = ($this->nextSpanResolver)($extractedContext, $request); + + $scopeCloser = $this->tracer->openScope($span); + + if ($span->isNoop()) { + try { + return $handler->handle($request); + } finally { + $span->finish(); + $scopeCloser(); + } + } + + $span->setKind(Kind\SERVER); + $spanCustomizer = new SpanCustomizerShield($span); + $span->setName($this->parser->spanName($request)); + $this->parser->request($request, $span->getContext(), $spanCustomizer); + + try { + $response = $handler->handle($request); + $this->parser->response($response, $span->getContext(), $spanCustomizer); + return $response; + } catch (Throwable $e) { + $span->setError($e); + throw $e; + } finally { + $span->finish(); + $scopeCloser(); + } + } +} diff --git a/src/Zipkin/Instrumentation/Http/Server/Psr/README.md b/src/Zipkin/Instrumentation/Http/Server/Psr/README.md new file mode 100644 index 00000000..902b8d8c --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/Psr/README.md @@ -0,0 +1,41 @@ +# Zipkin instrumentation for PSR15 HTTP Server + +This component contains the instrumentation for the standard [PSR15 HTTP servers](https://www.php-fig.org/psr/psr-15/). + +## Getting started + +Before using this library, make sure the interfaces for PSR15 HTTP server are installed: + +```bash +composer require psr/http-server-middleware +``` + +## Usage + +In this example we use [fast-route](https://github.com/middlewares/fast-route) and [request-handler](https://github.com/middlewares/request-handler) middlewares but any HTTP server middleware supporting PSR15 middlewares will work. + +```php +use Zipkin\Instrumentation\Http\Server\Middleware as ZipkinMiddleware; +use Zipkin\Instrumentation\Http\Server\ServerTracing; + +// Create the routing dispatcher +$fastRouteDispatcher = FastRoute\simpleDispatcher(function (FastRoute\RouteCollector $r) { + $r->get('/hello/{name}', HelloWorldController::class); +}); + +// Creates tracing component +$tracing = TracingBuilder::create() + ->havingLocalServiceName('my_service') + ->build(); + +$httpClientTracing = new ServerTracing($tracing); + +$dispatcher = new Dispatcher([ + new Middlewares\FastRoute($fastRouteDispatcher), + // ... + new ZipkinMiddleware($serverTracing), + new Middlewares\RequestHandler(), +]); + +$response = $dispatcher->dispatch(new ServerRequest('/hello/world')); +``` diff --git a/src/Zipkin/Instrumentation/Http/Server/Psr/RequestHeaders.php b/src/Zipkin/Instrumentation/Http/Server/Psr/RequestHeaders.php new file mode 100644 index 00000000..8b5fece0 --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/Psr/RequestHeaders.php @@ -0,0 +1,17 @@ +tracing = $tracing; + $this->parser = $parser ?? new NoopParser; + $this->nextSpanResolver = self::buildNextSpanResolver($tracing->getTracer(), $requestSampler); + } + + public function getTracing(): Tracing + { + return $this->tracing; + } + + /** + * @return Parser the server parser for enriching span information based on the request + */ + public function getParser(): Parser + { + return $this->parser; + } + + /** + * @return callable(TraceContext, $request): Span the next span handler which creates an appropriate span based + * on the extracted context. + */ + public function getNextSpanResolver(): callable + { + return $this->nextSpanResolver; + } + + private static function buildNextSpanResolver(Tracer $tracer, ?callable $requestSampler): callable + { + return static function (SamplingFlags $extractedContext, $request) use ($tracer, $requestSampler): Span { + if ($extractedContext instanceof TraceContext) { + return $tracer->joinSpan($extractedContext); + } + + if ($requestSampler === null) { + return $tracer->nextSpan($extractedContext); + } + + return $tracer->nextSpanWithSampler( + $requestSampler, + [$request], + $extractedContext + ); + }; + } +} diff --git a/src/Zipkin/Propagation/B3.php b/src/Zipkin/Propagation/B3.php index d5b6b224..dd05f154 100644 --- a/src/Zipkin/Propagation/B3.php +++ b/src/Zipkin/Propagation/B3.php @@ -21,23 +21,23 @@ final class B3 implements Propagation * 128 or 64-bit trace ID lower-hex encoded into 32 or 16 characters (required) */ private const TRACE_ID_NAME = 'X-B3-TraceId'; - + /** * 64-bit span ID lower-hex encoded into 16 characters (required) */ private const SPAN_ID_NAME = 'X-B3-SpanId'; - + /** * 64-bit parent span ID lower-hex encoded into 16 characters (absent on root span) */ private const PARENT_SPAN_ID_NAME = 'X-B3-ParentSpanId'; - + /** * '1' means report this span to the tracing system, '0' means do not. (absent means defer the * decision to the receiver of this header). */ private const SAMPLED_NAME = 'X-B3-Sampled'; - + /** * '1' implies sampled and is a request to override collection-tier sampling policy. */ @@ -78,7 +78,7 @@ final class B3 implements Propagation * Inject the single value context */ public const INJECT_SINGLE = 'single'; - + /** * Inject the single value context excluding the parent (e.g. for messaging) */ @@ -151,7 +151,7 @@ public function __construct( array $kindInjectors = [] ) { $this->logger = $logger ?: new NullLogger(); - + foreach ($kindInjectors as $kind => $injectorsNames) { if ($kind !== Kind\CLIENT && $kind !== Kind\PRODUCER && $kind !== self::DEFAULT_INJECTOR) { throw new InvalidArgumentException(sprintf( @@ -164,10 +164,11 @@ public function __construct( } if (array_key_exists(self::INJECT_SINGLE, $injectorsNames) && - array_key_exists(self::INJECT_SINGLE_NO_PARENT, $injectorsNames)) { + array_key_exists(self::INJECT_SINGLE_NO_PARENT, $injectorsNames) + ) { throw new InvalidArgumentException(sprintf( 'Both \"B3::INJECT_SINGLE\" and \"B3::INJECT_SINGLE_NO_PARENT\" ' . - 'can\'t be included for the same kind \"%d\".', + 'can\'t be included for the same kind \"%d\".', $kind )); } @@ -206,12 +207,7 @@ public function getInjector(Setter $setter): callable { $injectorKind = ($setter instanceof RemoteSetter) ? $setter->getKind() : self::DEFAULT_INJECTOR; - /** - * @param TraceContext $traceContext - * @param &$carrier - * @return void - */ - return function (SamplingFlags $traceContext, &$carrier) use ($setter, $injectorKind) { + return function (SamplingFlags $traceContext, &$carrier) use ($setter, $injectorKind): void { if ($traceContext->isEmpty()) { return; } @@ -271,7 +267,7 @@ private static function buildSingleValue(SamplingFlags $traceContext, bool $incl if ($traceContext instanceof TraceContext) { $value = $traceContext->getTraceId() - . '-' . $traceContext->getSpanId(); + . '-' . $traceContext->getSpanId(); if ($samplingBit !== null) { $value .= '-' . $samplingBit; @@ -380,7 +376,7 @@ public static function parseMultiValue(Getter $getter, $carrier): SamplingFlags } } } - + $traceId = $getter->get($carrier, self::TRACE_ID_NAME); if ($traceId === null) { return DefaultSamplingFlags::create($isSampled, $isDebug); diff --git a/src/Zipkin/Propagation/Propagation.php b/src/Zipkin/Propagation/Propagation.php index 97fe6dea..ca6f98de 100644 --- a/src/Zipkin/Propagation/Propagation.php +++ b/src/Zipkin/Propagation/Propagation.php @@ -40,7 +40,7 @@ public function getKeys(): array; * being passed by reference. * * @param Setter $setter invoked for each propagation key to add. - * @return callable + * @return callable(TraceContext,mixed):void */ public function getInjector(Setter $setter): callable; diff --git a/tests/Integration/Instrumentation/Http/Server/ServerTest.php b/tests/Integration/Instrumentation/Http/Server/ServerTest.php new file mode 100644 index 00000000..fc405111 --- /dev/null +++ b/tests/Integration/Instrumentation/Http/Server/ServerTest.php @@ -0,0 +1,93 @@ +havingReporter($reporter) + ->havingSampler(BinarySampler::createAsAlwaysSample()) + ->build(); + $tracer = $tracing->getTracer(); + + return [ + new ServerTracing($tracing, $parser), + static function () use ($tracer, $reporter): array { + $tracer->flush(); + return $reporter->flush(); + } + ]; + } + + public function testMiddleware() + { + $parser = new class() extends DefaultParser { + public function request($request, TraceContext $context, SpanCustomizer $span): void + { + assert($request instanceof ServerRequestInterface); + // This parser retrieves the user_id from the request and add + // is a tag. + $userId = $request->getAttribute('user_id'); + $span->tag('user_id', $userId); + parent::request($request, $context, $span); + } + }; + + list($serverTracing, $flusher) = self::createTracing($parser); + + $fastRouteDispatcher = simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/users/{user_id}', function ($request) { + return new Response(201); + }); + }); + + $request = Factory::createServerRequest('GET', '/users/abc123'); + + $response = Dispatcher::run([ + new Middlewares\FastRoute($fastRouteDispatcher), + new Middleware($serverTracing), + new Middlewares\RequestHandler(), + ], $request); + + $this->assertEquals(201, $response->getStatusCode()); + + $spans = ($flusher)(); + + $this->assertCount(1, $spans); + + $span = $spans[0]->toArray(); + + $this->assertEquals('GET', $span['name']); + $this->assertEquals([ + 'http.method' => 'GET', + 'http.path' => '/users/abc123', + 'http.status_code' => '201', + 'user_id' => 'abc123', + ], $span['tags']); + } +} diff --git a/tests/Unit/Instrumentation/Http/Client/ClientTest.php b/tests/Unit/Instrumentation/Http/Client/Psr/ClientTest.php similarity index 95% rename from tests/Unit/Instrumentation/Http/Client/ClientTest.php rename to tests/Unit/Instrumentation/Http/Client/Psr/ClientTest.php index bdd71425..736292e8 100644 --- a/tests/Unit/Instrumentation/Http/Client/ClientTest.php +++ b/tests/Unit/Instrumentation/Http/Client/Psr/ClientTest.php @@ -2,13 +2,14 @@ declare(strict_types=1); -namespace ZipkinTests\Instrumentation\Http\Client; +namespace ZipkinTests\Unit\Instrumentation\Http\Client\Psr; use Zipkin\TracingBuilder; use Zipkin\Samplers\BinarySampler; use Zipkin\Reporters\InMemory; +use Zipkin\Instrumentation\Http\Client\Psr\DefaultParser; +use Zipkin\Instrumentation\Http\Client\Psr\Client; use Zipkin\Instrumentation\Http\Client\ClientTracing; -use Zipkin\Instrumentation\Http\Client\Client; use RuntimeException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\RequestInterface; @@ -30,7 +31,7 @@ private static function createTracing(): array $tracer = $tracing->getTracer(); return [ - new ClientTracing($tracing), + new ClientTracing($tracing, new DefaultParser), static function () use ($tracer, $reporter): array { $tracer->flush(); return $reporter->flush(); diff --git a/tests/Unit/Instrumentation/Http/Client/TraceContextRequestTest.php b/tests/Unit/Instrumentation/Http/Client/TraceContextRequestTest.php new file mode 100644 index 00000000..adc23f54 --- /dev/null +++ b/tests/Unit/Instrumentation/Http/Client/TraceContextRequestTest.php @@ -0,0 +1,34 @@ +assertEquals('GET', $traceContextRequest->getMethod()); + $this->assertEquals('http://mytest/things', $traceContextRequest->getUri()->__toString()); + $this->assertTrue($context->isEqual($traceContextRequest->getTraceContext())); + } + + public function testWithMethodSuccess() + { + $request = new Request('GET', 'http://mytest/things'); + $context = TraceContext::createAsRoot(); + $traceContextRequest = TraceContextRequest::wrap($request, $context); + $traceContextRequestModified = $traceContextRequest->withMethod('POST'); + $this->assertNotEquals(spl_object_hash($traceContextRequest), spl_object_hash($traceContextRequestModified)); + $actualValue = $traceContextRequestModified->getMethod(); + $this->assertEquals('POST', $actualValue); + } +} diff --git a/tests/Unit/Instrumentation/Http/Server/Psr/ServerTest.php b/tests/Unit/Instrumentation/Http/Server/Psr/ServerTest.php new file mode 100644 index 00000000..b5159df9 --- /dev/null +++ b/tests/Unit/Instrumentation/Http/Server/Psr/ServerTest.php @@ -0,0 +1,119 @@ +havingReporter($reporter) + ->havingSampler(BinarySampler::createAsAlwaysSample()) + ->build(); + $tracer = $tracing->getTracer(); + + return [ + new ServerTracing($tracing, new DefaultParser()), + static function () use ($tracer, $reporter): array { + $tracer->flush(); + return $reporter->flush(); + } + ]; + } + + private static function createRequestHandler($response = null): RequestHandlerInterface + { + return new class($response) implements RequestHandlerInterface { + private $response; + private $lastRequest; + + public function __construct(?ResponseInterface $response) + { + $this->response = $response ?? new Psr7Response(); + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->lastRequest = $request; + + return $this->response; + } + + public function getLastRequest(): ?RequestInterface + { + return $this->lastRequest; + } + }; + } + + public function testMiddlewareHandlesRequestSuccessfully() + { + list($tracing, $flusher) = self::createTracing(); + $request = new ServerRequest('GET', 'http://mytest'); + + $handler = self::createRequestHandler(); + + $middleware = new Middleware($tracing); + $middleware->process($request, $handler); + + $this->assertSame($request, $handler->getLastRequest()); + + $spans = ($flusher)(); + + $this->assertCount(1, $spans); + + $span = $spans[0]->toArray(); + + $this->assertEquals('GET', $span['name']); + $this->assertEquals([ + 'http.method' => 'GET', + 'http.path' => '/', + 'http.status_code' => '200', + ], $span['tags']); + } + + public function testMiddlewareParsesRequestSuccessfullyWithNon2xx() + { + list($tracing, $flusher) = self::createTracing(); + $request = new ServerRequest('GET', 'http://mytest'); + + $handler = self::createRequestHandler(new Psr7Response(404)); + + $middleware = new Middleware($tracing); + $middleware->process($request, $handler); + + $this->assertSame($request, $handler->getLastRequest()); + + $spans = ($flusher)(); + + $this->assertCount(1, $spans); + + $span = $spans[0]->toArray(); + + $this->assertEquals('GET', $span['name']); + $this->assertEquals([ + 'http.method' => 'GET', + 'http.path' => '/', + 'http.status_code' => '404', + 'error' => '404' + ], $span['tags']); + } +}