From 9986886e07533d619728797fd69f489d572d0f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Wed, 24 Jun 2020 17:38:35 +0200 Subject: [PATCH 1/8] feat(http-server): adds middleware and request handler instrumentation. --- .../Http/Server/DefaultParser.php | 39 ++++++++ .../Http/Server/Middleware.php | 88 +++++++++++++++++ .../Instrumentation/Http/Server/Parser.php | 53 +++++++++++ .../Http/Server/RequestHandler.php | 94 +++++++++++++++++++ .../Http/Server/RequestHeaders.php | 17 ++++ .../Http/Server/ServerTracing.php | 68 ++++++++++++++ 6 files changed, 359 insertions(+) create mode 100644 src/Zipkin/Instrumentation/Http/Server/DefaultParser.php create mode 100644 src/Zipkin/Instrumentation/Http/Server/Middleware.php create mode 100644 src/Zipkin/Instrumentation/Http/Server/Parser.php create mode 100644 src/Zipkin/Instrumentation/Http/Server/RequestHandler.php create mode 100644 src/Zipkin/Instrumentation/Http/Server/RequestHeaders.php create mode 100644 src/Zipkin/Instrumentation/Http/Server/ServerTracing.php diff --git a/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php b/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php new file mode 100644 index 00000000..0f5f068a --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php @@ -0,0 +1,39 @@ +getMethod(); + } + + public function request(ServerRequestInterface $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()); + } + } + + public function error(Throwable $e, TraceContext $context, SpanCustomizer $span): void + { + $span->tag(Tags\ERROR, $e->getMessage()); + } +} diff --git a/src/Zipkin/Instrumentation/Http/Server/Middleware.php b/src/Zipkin/Instrumentation/Http/Server/Middleware.php new file mode 100644 index 00000000..054568f6 --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/Middleware.php @@ -0,0 +1,88 @@ +tracer = $tracing->getTracing()->getTracer(); + $this->extractor = $tracing->getTracing()->getPropagation()->getExtractor(new RequestHeaders()); + $this->parser = $tracing->getParser(); + $this->requestSampler = $tracing->getRequestSampler(); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $extractedContext = ($this->extractor)($request); + + if ($extractedContext instanceof TraceContext) { + $span = $this->tracer->joinSpan($extractedContext); + } elseif ($this->requestSampler === null) { + $span = $this->tracer->nextSpan($extractedContext); + } else { + $span = $this->tracer->nextSpanWithSampler( + $this->requestSampler, + [$request], + $extractedContext + ); + } + + $spanCustomizer = null; + if (!$span->isNoop()) { + $span->setKind(Kind\SERVER); + // If span is NOOP it does not make sense to add customizations + // to it like tags or annotations. + $spanCustomizer = new SpanCustomizerShield($span); + $span->setName($this->parser->spanName($request)); + $this->parser->request($request, $span->getContext(), $spanCustomizer); + } + + try { + $response = $handler->handle($request); + if ($spanCustomizer !== null) { + $this->parser->response($response, $span->getContext(), $spanCustomizer); + } + return $response; + } catch (Throwable $e) { + if ($spanCustomizer !== null) { + $this->parser->error($e, $span->getContext(), $spanCustomizer); + } + throw $e; + } finally { + $span->finish(); + } + } +} diff --git a/src/Zipkin/Instrumentation/Http/Server/Parser.php b/src/Zipkin/Instrumentation/Http/Server/Parser.php new file mode 100644 index 00000000..c0e13374 --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/Parser.php @@ -0,0 +1,53 @@ +delegate = $delegate; + $this->tracer = $tracing->getTracing()->getTracer(); + $this->extractor = $tracing->getTracing()->getPropagation()->getExtractor(new RequestHeaders()); + $this->parser = $tracing->getParser(); + $this->requestSampler = $tracing->getRequestSampler(); + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $extractedContext = ($this->extractor)($request); + + if ($extractedContext instanceof TraceContext) { + $span = $this->tracer->joinSpan($extractedContext); + } elseif ($this->requestSampler === null) { + $span = $this->tracer->nextSpan($extractedContext); + } else { + $span = $this->tracer->nextSpanWithSampler( + $this->requestSampler, + [$request], + $extractedContext + ); + } + + $spanCustomizer = null; + if (!$span->isNoop()) { + $span->setKind(Kind\SERVER); + // If span is NOOP it does not make sense to add customizations + // to it like tags or annotations. + $spanCustomizer = new SpanCustomizerShield($span); + $span->setName($this->parser->spanName($request)); + $this->parser->request($request, $span->getContext(), $spanCustomizer); + } + + try { + $response = $this->delegate->handle($request); + if ($spanCustomizer !== null) { + $this->parser->response($response, $span->getContext(), $spanCustomizer); + } + return $response; + } catch (Throwable $e) { + if ($spanCustomizer !== null) { + $this->parser->error($e, $span->getContext(), $spanCustomizer); + } + throw $e; + } finally { + $span->finish(); + } + } +} diff --git a/src/Zipkin/Instrumentation/Http/Server/RequestHeaders.php b/src/Zipkin/Instrumentation/Http/Server/RequestHeaders.php new file mode 100644 index 00000000..2cc6c213 --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/RequestHeaders.php @@ -0,0 +1,17 @@ + + * function (RequestInterface $request): ?bool {} + * + * + * @var callable|null + */ + private $requestSampler; + + public function __construct( + Tracing $tracing, + Parser $parser = null, + callable $requestSampler = null + ) { + $this->tracing = $tracing; + $this->parser = $parser ?? new DefaultParser; + $this->requestSampler = $requestSampler; + } + + public function getTracing(): Tracing + { + return $this->tracing; + } + + /** + * @return callable|null with the signature: + * + *
+     * function (RequestInterface $request): ?bool
+     * 
+ */ + public function getRequestSampler(): ?callable + { + return $this->requestSampler; + } + + public function getParser(): Parser + { + return $this->parser; + } +} From dd5de9986d8282061139771fa2624e44b6bf2e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Wed, 24 Jun 2020 21:53:22 +0200 Subject: [PATCH 2/8] tests(http-server): adds integration test for the Middleware. --- composer.json | 15 ++- .../Http/Server/Middleware.php | 1 - .../Instrumentation/Http/Server/Parser.php | 2 +- .../Instrumentation/Http/Server/README.md | 41 ++++++ .../Http/Server/ServerTest.php | 92 +++++++++++++ .../Http/Client/ClientTest.php | 2 +- .../Http/Server/ServerTest.php | 121 ++++++++++++++++++ 7 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 src/Zipkin/Instrumentation/Http/Server/README.md create mode 100644 tests/Integration/Instrumentation/Http/Server/ServerTest.php create mode 100644 tests/Unit/Instrumentation/Http/Server/ServerTest.php diff --git a/composer.json b/composer.json index 103b5fa0..d3538db6 100644 --- a/composer.json +++ b/composer.json @@ -7,13 +7,17 @@ "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 +25,15 @@ "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", + "psr/http-server-handler": "^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": "Allows to instrument HTTP servers following PSR15." }, "extra": { "branch-alias": { diff --git a/src/Zipkin/Instrumentation/Http/Server/Middleware.php b/src/Zipkin/Instrumentation/Http/Server/Middleware.php index 054568f6..3da48e08 100644 --- a/src/Zipkin/Instrumentation/Http/Server/Middleware.php +++ b/src/Zipkin/Instrumentation/Http/Server/Middleware.php @@ -59,7 +59,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $extractedContext ); } - $spanCustomizer = null; if (!$span->isNoop()) { $span->setKind(Kind\SERVER); diff --git a/src/Zipkin/Instrumentation/Http/Server/Parser.php b/src/Zipkin/Instrumentation/Http/Server/Parser.php index c0e13374..d55b1f9c 100644 --- a/src/Zipkin/Instrumentation/Http/Server/Parser.php +++ b/src/Zipkin/Instrumentation/Http/Server/Parser.php @@ -22,7 +22,7 @@ interface Parser * usually the HTTP method is enough (e.g GET or POST) but ideally * the http.route is desired (e.g. /user/{user_id}). */ - public function spanName(RequestInterface $request): string; + public function spanName(ServerRequestInterface $request): string; /** * request parses the incoming data related to a request in order to add diff --git a/src/Zipkin/Instrumentation/Http/Server/README.md b/src/Zipkin/Instrumentation/Http/Server/README.md new file mode 100644 index 00000000..902b8d8c --- /dev/null +++ b/src/Zipkin/Instrumentation/Http/Server/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/tests/Integration/Instrumentation/Http/Server/ServerTest.php b/tests/Integration/Instrumentation/Http/Server/ServerTest.php new file mode 100644 index 00000000..ac84750d --- /dev/null +++ b/tests/Integration/Instrumentation/Http/Server/ServerTest.php @@ -0,0 +1,92 @@ +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(ServerRequestInterface $request, TraceContext $context, SpanCustomizer $span): void + { + // 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/ClientTest.php index bdd71425..7586c8c6 100644 --- a/tests/Unit/Instrumentation/Http/Client/ClientTest.php +++ b/tests/Unit/Instrumentation/Http/Client/ClientTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ZipkinTests\Instrumentation\Http\Client; +namespace ZipkinTests\Unit\Instrumentation\Http\Client; use Zipkin\TracingBuilder; use Zipkin\Samplers\BinarySampler; diff --git a/tests/Unit/Instrumentation/Http/Server/ServerTest.php b/tests/Unit/Instrumentation/Http/Server/ServerTest.php new file mode 100644 index 00000000..0e3a500e --- /dev/null +++ b/tests/Unit/Instrumentation/Http/Server/ServerTest.php @@ -0,0 +1,121 @@ +havingReporter($reporter) + ->havingSampler(BinarySampler::createAsAlwaysSample()) + ->build(); + $tracer = $tracing->getTracer(); + + return [ + new ServerTracing($tracing), + static function () use ($tracer, $reporter): array { + $tracer->flush(); + return $reporter->flush(); + } + ]; + } + + public function testMiddlewareHandlesRequestSuccessfully() + { + list($tracing, $flusher) = self::createTracing(); + $request = new ServerRequest('GET', 'http://mytest'); + + $handler = new class() implements RequestHandlerInterface { + private $lastRequest; + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->lastRequest = $request; + + return new Psr7Response(); + } + + public function getLastRequest(): ?RequestInterface + { + return $this->lastRequest; + } + }; + + $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 = new class() implements RequestHandlerInterface { + private $lastRequest; + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->lastRequest = $request; + + return new Psr7Response(404); + } + + public function getLastRequest(): ?RequestInterface + { + return $this->lastRequest; + } + }; + + $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']); + } +} From 02ccf75dd3e11cda0e6dc539534539af3a8f3844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Wed, 24 Jun 2020 21:58:16 +0200 Subject: [PATCH 3/8] docs(http-server): improves documentation. --- README.md | 1 + composer.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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 d3538db6..bbe7f80d 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,8 @@ }, "suggest": { "psr/http-client": "Allows to instrument HTTP clients following PSR18.", - "psr/http-server": "Allows to instrument HTTP servers following PSR15." + "psr/http-server-middleware": "Allows to instrument HTTP servers following PSR15.", + "psr/http-server-handler": "Allows to instrument HTTP servers following PSR15." }, "extra": { "branch-alias": { From 18b1e712be057179dec3d9231ff3abcfbc758d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Wed, 24 Jun 2020 22:02:09 +0200 Subject: [PATCH 4/8] chore(http-server): makes linter happy. --- src/Zipkin/Instrumentation/Http/Server/Middleware.php | 2 +- src/Zipkin/Instrumentation/Http/Server/RequestHandler.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Zipkin/Instrumentation/Http/Server/Middleware.php b/src/Zipkin/Instrumentation/Http/Server/Middleware.php index 3da48e08..d8c26c1c 100644 --- a/src/Zipkin/Instrumentation/Http/Server/Middleware.php +++ b/src/Zipkin/Instrumentation/Http/Server/Middleware.php @@ -32,7 +32,7 @@ final class Middleware implements MiddlewareInterface private $parser; /** - * @var callable + * @var callable|null */ private $requestSampler; diff --git a/src/Zipkin/Instrumentation/Http/Server/RequestHandler.php b/src/Zipkin/Instrumentation/Http/Server/RequestHandler.php index 2378f9a2..879b365f 100644 --- a/src/Zipkin/Instrumentation/Http/Server/RequestHandler.php +++ b/src/Zipkin/Instrumentation/Http/Server/RequestHandler.php @@ -37,7 +37,7 @@ final class RequestHandler implements RequestHandlerInterface private $parser; /** - * @var callable + * @var callable|null */ private $requestSampler; From eb76e0838351fc261603084172fd578a70987cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Fri, 26 Jun 2020 10:19:04 +0200 Subject: [PATCH 5/8] chore(http-server): removes RequestHandler as middleware is enough for tracing. --- composer.json | 5 +- .../Http/Server/Middleware.php | 31 +++--- .../Http/Server/RequestHandler.php | 94 ------------------- .../Http/Server/ServerTest.php | 39 ++++---- 4 files changed, 37 insertions(+), 132 deletions(-) delete mode 100644 src/Zipkin/Instrumentation/Http/Server/RequestHandler.php diff --git a/composer.json b/composer.json index bbe7f80d..3af9fab2 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "description": "A Zipkin instrumentation for PHP", "keywords": [ "zipkin", + "distributed-tracing", "tracing", "openzipkin" ], @@ -31,7 +32,6 @@ "phpunit/phpunit": "~7.5.20", "psr/http-client": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/http-server-handler": "^1.0", "middlewares/fast-route": "^1.2.1", "middlewares/request-handler": "^1.4.0", "squizlabs/php_codesniffer": "3.*" @@ -69,8 +69,7 @@ }, "suggest": { "psr/http-client": "Allows to instrument HTTP clients following PSR18.", - "psr/http-server-middleware": "Allows to instrument HTTP servers following PSR15.", - "psr/http-server-handler": "Allows to instrument HTTP servers following PSR15." + "psr/http-server-middleware": "Allows to instrument HTTP servers via middlewares following PSR15." }, "extra": { "branch-alias": { diff --git a/src/Zipkin/Instrumentation/Http/Server/Middleware.php b/src/Zipkin/Instrumentation/Http/Server/Middleware.php index d8c26c1c..87280eb2 100644 --- a/src/Zipkin/Instrumentation/Http/Server/Middleware.php +++ b/src/Zipkin/Instrumentation/Http/Server/Middleware.php @@ -59,29 +59,32 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $extractedContext ); } - $spanCustomizer = null; - if (!$span->isNoop()) { - $span->setKind(Kind\SERVER); - // If span is NOOP it does not make sense to add customizations - // to it like tags or annotations. - $spanCustomizer = new SpanCustomizerShield($span); - $span->setName($this->parser->spanName($request)); - $this->parser->request($request, $span->getContext(), $spanCustomizer); + $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); - if ($spanCustomizer !== null) { - $this->parser->response($response, $span->getContext(), $spanCustomizer); - } + $this->parser->response($response, $span->getContext(), $spanCustomizer); return $response; } catch (Throwable $e) { - if ($spanCustomizer !== null) { - $this->parser->error($e, $span->getContext(), $spanCustomizer); - } + $this->parser->error($e, $span->getContext(), $spanCustomizer); throw $e; } finally { $span->finish(); + $scopeCloser(); } } } diff --git a/src/Zipkin/Instrumentation/Http/Server/RequestHandler.php b/src/Zipkin/Instrumentation/Http/Server/RequestHandler.php deleted file mode 100644 index 879b365f..00000000 --- a/src/Zipkin/Instrumentation/Http/Server/RequestHandler.php +++ /dev/null @@ -1,94 +0,0 @@ -delegate = $delegate; - $this->tracer = $tracing->getTracing()->getTracer(); - $this->extractor = $tracing->getTracing()->getPropagation()->getExtractor(new RequestHeaders()); - $this->parser = $tracing->getParser(); - $this->requestSampler = $tracing->getRequestSampler(); - } - - public function handle(ServerRequestInterface $request): ResponseInterface - { - $extractedContext = ($this->extractor)($request); - - if ($extractedContext instanceof TraceContext) { - $span = $this->tracer->joinSpan($extractedContext); - } elseif ($this->requestSampler === null) { - $span = $this->tracer->nextSpan($extractedContext); - } else { - $span = $this->tracer->nextSpanWithSampler( - $this->requestSampler, - [$request], - $extractedContext - ); - } - - $spanCustomizer = null; - if (!$span->isNoop()) { - $span->setKind(Kind\SERVER); - // If span is NOOP it does not make sense to add customizations - // to it like tags or annotations. - $spanCustomizer = new SpanCustomizerShield($span); - $span->setName($this->parser->spanName($request)); - $this->parser->request($request, $span->getContext(), $spanCustomizer); - } - - try { - $response = $this->delegate->handle($request); - if ($spanCustomizer !== null) { - $this->parser->response($response, $span->getContext(), $spanCustomizer); - } - return $response; - } catch (Throwable $e) { - if ($spanCustomizer !== null) { - $this->parser->error($e, $span->getContext(), $spanCustomizer); - } - throw $e; - } finally { - $span->finish(); - } - } -} diff --git a/tests/Unit/Instrumentation/Http/Server/ServerTest.php b/tests/Unit/Instrumentation/Http/Server/ServerTest.php index 0e3a500e..f37d356c 100644 --- a/tests/Unit/Instrumentation/Http/Server/ServerTest.php +++ b/tests/Unit/Instrumentation/Http/Server/ServerTest.php @@ -38,19 +38,22 @@ static function () use ($tracer, $reporter): array { ]; } - public function testMiddlewareHandlesRequestSuccessfully() + private static function createRequestHandler($response = null): RequestHandlerInterface { - list($tracing, $flusher) = self::createTracing(); - $request = new ServerRequest('GET', 'http://mytest'); - - $handler = new class() implements 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 new Psr7Response(); + return $this->response; } public function getLastRequest(): ?RequestInterface @@ -58,6 +61,14 @@ 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); @@ -83,21 +94,7 @@ public function testMiddlewareParsesRequestSuccessfullyWithNon2xx() list($tracing, $flusher) = self::createTracing(); $request = new ServerRequest('GET', 'http://mytest'); - $handler = new class() implements RequestHandlerInterface { - private $lastRequest; - - public function handle(ServerRequestInterface $request): ResponseInterface - { - $this->lastRequest = $request; - - return new Psr7Response(404); - } - - public function getLastRequest(): ?RequestInterface - { - return $this->lastRequest; - } - }; + $handler = self::createRequestHandler(new Psr7Response(404)); $middleware = new Middleware($tracing); $middleware->process($request, $handler); From 4fc98ca4e8a3a164e22c659bd1b5600d39d5e2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Mon, 6 Jul 2020 17:11:15 +0200 Subject: [PATCH 6/8] chore: uses span error API. --- src/Zipkin/Instrumentation/Http/Server/DefaultParser.php | 5 ----- src/Zipkin/Instrumentation/Http/Server/Middleware.php | 2 +- src/Zipkin/Instrumentation/Http/Server/Parser.php | 7 ------- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php b/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php index 0f5f068a..1c429206 100644 --- a/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php +++ b/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php @@ -31,9 +31,4 @@ public function response(ResponseInterface $response, TraceContext $context, Spa $span->tag(Tags\ERROR, (string) $response->getStatusCode()); } } - - public function error(Throwable $e, TraceContext $context, SpanCustomizer $span): void - { - $span->tag(Tags\ERROR, $e->getMessage()); - } } diff --git a/src/Zipkin/Instrumentation/Http/Server/Middleware.php b/src/Zipkin/Instrumentation/Http/Server/Middleware.php index 87280eb2..b9da73b6 100644 --- a/src/Zipkin/Instrumentation/Http/Server/Middleware.php +++ b/src/Zipkin/Instrumentation/Http/Server/Middleware.php @@ -80,7 +80,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->parser->response($response, $span->getContext(), $spanCustomizer); return $response; } catch (Throwable $e) { - $this->parser->error($e, $span->getContext(), $spanCustomizer); + $span->setError($e); throw $e; } finally { $span->finish(); diff --git a/src/Zipkin/Instrumentation/Http/Server/Parser.php b/src/Zipkin/Instrumentation/Http/Server/Parser.php index d55b1f9c..5f9a1df7 100644 --- a/src/Zipkin/Instrumentation/Http/Server/Parser.php +++ b/src/Zipkin/Instrumentation/Http/Server/Parser.php @@ -43,11 +43,4 @@ public function request(ServerRequestInterface $request, TraceContext $context, * payload can be added. */ public function response(ResponseInterface $response, TraceContext $context, SpanCustomizer $span): void; - - /** - * error parses the exception when doing a HTTP call, usually it is good enough to tag - * the throwable message but depending on the wrapping client, one might want to enrich - * the error with meaningful information from the exception. - */ - public function error(Throwable $e, TraceContext $context, SpanCustomizer $span): void; } From eb5bee4d690deaf9e482b5bec9beb58ea062ff16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Tue, 7 Jul 2020 19:47:17 +0200 Subject: [PATCH 7/8] chore: creates nextSpanHandler in ServerTracing. --- .../Http/Server/Middleware.php | 20 ++----- .../Http/Server/ServerTracing.php | 54 +++++++++++++------ 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/Zipkin/Instrumentation/Http/Server/Middleware.php b/src/Zipkin/Instrumentation/Http/Server/Middleware.php index b9da73b6..5d7320e3 100644 --- a/src/Zipkin/Instrumentation/Http/Server/Middleware.php +++ b/src/Zipkin/Instrumentation/Http/Server/Middleware.php @@ -6,7 +6,6 @@ use Zipkin\Tracer; use Zipkin\SpanCustomizerShield; -use Zipkin\Propagation\TraceContext; use Zipkin\Kind; use Throwable; use Psr\Http\Server\RequestHandlerInterface; @@ -32,33 +31,24 @@ final class Middleware implements MiddlewareInterface private $parser; /** - * @var callable|null + * @var callable */ - private $requestSampler; + private $nextSpanHandler; public function __construct(ServerTracing $tracing) { $this->tracer = $tracing->getTracing()->getTracer(); $this->extractor = $tracing->getTracing()->getPropagation()->getExtractor(new RequestHeaders()); $this->parser = $tracing->getParser(); - $this->requestSampler = $tracing->getRequestSampler(); + $this->nextSpanHandler = $tracing->getNextSpanHandler(); } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $extractedContext = ($this->extractor)($request); - if ($extractedContext instanceof TraceContext) { - $span = $this->tracer->joinSpan($extractedContext); - } elseif ($this->requestSampler === null) { - $span = $this->tracer->nextSpan($extractedContext); - } else { - $span = $this->tracer->nextSpanWithSampler( - $this->requestSampler, - [$request], - $extractedContext - ); - } + $span = ($this->nextSpanHandler)($extractedContext, $request); + $scopeCloser = $this->tracer->openScope($span); if ($span->isNoop()) { diff --git a/src/Zipkin/Instrumentation/Http/Server/ServerTracing.php b/src/Zipkin/Instrumentation/Http/Server/ServerTracing.php index 72e98adc..c8d4d6c6 100644 --- a/src/Zipkin/Instrumentation/Http/Server/ServerTracing.php +++ b/src/Zipkin/Instrumentation/Http/Server/ServerTracing.php @@ -5,6 +5,10 @@ namespace Zipkin\Instrumentation\Http\Server; use Zipkin\Tracing; +use Zipkin\Tracer; +use Zipkin\Span; +use Zipkin\Propagation\TraceContext; +use Zipkin\Propagation\SamplingFlags; /** * Tracing includes all the elements needed to trace a HTTP server @@ -23,17 +27,21 @@ class ServerTracing private $parser; /** - * function that decides to sample or not an unsampled + * @var callable + */ + private $nextSpanHandler; + + /** + * @param Tracing $tracing + * @param Parser $parser HTTP server parser to obtain meaningful information from + * request and response and tag the span accordingly. + * @param callable $requestSampler function that decides to sample or not an unsampled * request. The signature is: * *
      * function (RequestInterface $request): ?bool {}
      * 
- * - * @var callable|null */ - private $requestSampler; - public function __construct( Tracing $tracing, Parser $parser = null, @@ -41,7 +49,7 @@ public function __construct( ) { $this->tracing = $tracing; $this->parser = $parser ?? new DefaultParser; - $this->requestSampler = $requestSampler; + $this->nextSpanHandler = self::buildNextSpanHandler($tracing->getTracer(), $requestSampler); } public function getTracing(): Tracing @@ -49,20 +57,32 @@ public function getTracing(): Tracing return $this->tracing; } - /** - * @return callable|null with the signature: - * - *
-     * function (RequestInterface $request): ?bool
-     * 
- */ - public function getRequestSampler(): ?callable + public function getParser(): Parser { - return $this->requestSampler; + return $this->parser; } - public function getParser(): Parser + public function getNextSpanHandler(): callable { - return $this->parser; + return $this->nextSpanHandler; + } + + private static function buildNextSpanHandler(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 + ); + }; } } From bd755907936740d231e560823104bb09a9b36263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Ch=C3=A1vez?= Date: Sun, 12 Jul 2020 23:38:52 +0200 Subject: [PATCH 8/8] chore(http-server): decouples client/server tracing from PSR implementation and consolidates nextSpanResolver. --- phpstan.neon | 17 ++- .../Http/Client/ClientTracing.php | 83 ++++++++--- .../Http/Client/DefaultParser.php | 34 ----- .../Http/Client/NoopParser.php | 29 ++++ .../Instrumentation/Http/Client/Parser.php | 18 +-- .../Http/Client/{ => Psr}/Client.php | 34 ++--- .../Http/Client/Psr/DefaultParser.php | 48 +++++++ .../Http/Client/{ => Psr}/README.md | 2 +- .../Http/Client/{ => Psr}/RequestHeaders.php | 2 +- .../Http/Client/Psr/TraceContextRequest.php | 133 ++++++++++++++++++ .../Http/Server/DefaultParser.php | 34 ----- .../Http/Server/NoopParser.php | 29 ++++ .../Instrumentation/Http/Server/Parser.php | 15 +- .../Http/Server/Psr/DefaultParser.php | 48 +++++++ .../Http/Server/{ => Psr}/Middleware.php | 15 +- .../Http/Server/{ => Psr}/README.md | 0 .../Http/Server/{ => Psr}/RequestHeaders.php | 2 +- .../Http/Server/ServerTracing.php | 27 ++-- src/Zipkin/Propagation/B3.php | 28 ++-- src/Zipkin/Propagation/Propagation.php | 2 +- .../Http/Server/ServerTest.php | 7 +- .../Http/Client/{ => Psr}/ClientTest.php | 7 +- .../Http/Client/TraceContextRequestTest.php | 34 +++++ .../Http/Server/{ => Psr}/ServerTest.php | 7 +- 24 files changed, 479 insertions(+), 176 deletions(-) delete mode 100644 src/Zipkin/Instrumentation/Http/Client/DefaultParser.php create mode 100644 src/Zipkin/Instrumentation/Http/Client/NoopParser.php rename src/Zipkin/Instrumentation/Http/Client/{ => Psr}/Client.php (75%) create mode 100644 src/Zipkin/Instrumentation/Http/Client/Psr/DefaultParser.php rename src/Zipkin/Instrumentation/Http/Client/{ => Psr}/README.md (92%) rename src/Zipkin/Instrumentation/Http/Client/{ => Psr}/RequestHeaders.php (86%) create mode 100644 src/Zipkin/Instrumentation/Http/Client/Psr/TraceContextRequest.php delete mode 100644 src/Zipkin/Instrumentation/Http/Server/DefaultParser.php create mode 100644 src/Zipkin/Instrumentation/Http/Server/NoopParser.php create mode 100644 src/Zipkin/Instrumentation/Http/Server/Psr/DefaultParser.php rename src/Zipkin/Instrumentation/Http/Server/{ => Psr}/Middleware.php (82%) rename src/Zipkin/Instrumentation/Http/Server/{ => Psr}/README.md (100%) rename src/Zipkin/Instrumentation/Http/Server/{ => Psr}/RequestHeaders.php (86%) rename tests/Unit/Instrumentation/Http/Client/{ => Psr}/ClientTest.php (95%) create mode 100644 tests/Unit/Instrumentation/Http/Client/TraceContextRequestTest.php rename tests/Unit/Instrumentation/Http/Server/{ => Psr}/ServerTest.php (93%) 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/DefaultParser.php b/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php deleted file mode 100644 index 1c429206..00000000 --- a/src/Zipkin/Instrumentation/Http/Server/DefaultParser.php +++ /dev/null @@ -1,34 +0,0 @@ -getMethod(); - } - - public function request(ServerRequestInterface $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/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/Middleware.php b/src/Zipkin/Instrumentation/Http/Server/Psr/Middleware.php similarity index 82% rename from src/Zipkin/Instrumentation/Http/Server/Middleware.php rename to src/Zipkin/Instrumentation/Http/Server/Psr/Middleware.php index 5d7320e3..d4f33141 100644 --- a/src/Zipkin/Instrumentation/Http/Server/Middleware.php +++ b/src/Zipkin/Instrumentation/Http/Server/Psr/Middleware.php @@ -2,11 +2,13 @@ declare(strict_types=1); -namespace Zipkin\Instrumentation\Http\Server; +namespace Zipkin\Instrumentation\Http\Server\Psr; use Zipkin\Tracer; use Zipkin\SpanCustomizerShield; +use Zipkin\Span; use Zipkin\Kind; +use Zipkin\Instrumentation\Http\Server\ServerTracing; use Throwable; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\MiddlewareInterface; @@ -31,23 +33,26 @@ final class Middleware implements MiddlewareInterface private $parser; /** - * @var callable + * @var callable(SamplingFlags,mixed):Span */ - private $nextSpanHandler; + private $nextSpanResolver; public function __construct(ServerTracing $tracing) { $this->tracer = $tracing->getTracing()->getTracer(); $this->extractor = $tracing->getTracing()->getPropagation()->getExtractor(new RequestHeaders()); $this->parser = $tracing->getParser(); - $this->nextSpanHandler = $tracing->getNextSpanHandler(); + $this->nextSpanResolver = $tracing->getNextSpanResolver(); } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $extractedContext = ($this->extractor)($request); - $span = ($this->nextSpanHandler)($extractedContext, $request); + /** + * @var Span $span + */ + $span = ($this->nextSpanResolver)($extractedContext, $request); $scopeCloser = $this->tracer->openScope($span); diff --git a/src/Zipkin/Instrumentation/Http/Server/README.md b/src/Zipkin/Instrumentation/Http/Server/Psr/README.md similarity index 100% rename from src/Zipkin/Instrumentation/Http/Server/README.md rename to src/Zipkin/Instrumentation/Http/Server/Psr/README.md diff --git a/src/Zipkin/Instrumentation/Http/Server/RequestHeaders.php b/src/Zipkin/Instrumentation/Http/Server/Psr/RequestHeaders.php similarity index 86% rename from src/Zipkin/Instrumentation/Http/Server/RequestHeaders.php rename to src/Zipkin/Instrumentation/Http/Server/Psr/RequestHeaders.php index 2cc6c213..8b5fece0 100644 --- a/src/Zipkin/Instrumentation/Http/Server/RequestHeaders.php +++ b/src/Zipkin/Instrumentation/Http/Server/Psr/RequestHeaders.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Zipkin\Instrumentation\Http\Server; +namespace Zipkin\Instrumentation\Http\Server\Psr; use Zipkin\Propagation\RequestHeaders as BaseRequestHeaders; use Zipkin\Propagation\RemoteSetter; diff --git a/src/Zipkin/Instrumentation/Http/Server/ServerTracing.php b/src/Zipkin/Instrumentation/Http/Server/ServerTracing.php index c8d4d6c6..0f339c94 100644 --- a/src/Zipkin/Instrumentation/Http/Server/ServerTracing.php +++ b/src/Zipkin/Instrumentation/Http/Server/ServerTracing.php @@ -29,18 +29,14 @@ class ServerTracing /** * @var callable */ - private $nextSpanHandler; + private $nextSpanResolver; /** * @param Tracing $tracing * @param Parser $parser HTTP server parser to obtain meaningful information from * request and response and tag the span accordingly. - * @param callable $requestSampler function that decides to sample or not an unsampled - * request. The signature is: - * - *
-     * function (RequestInterface $request): ?bool {}
-     * 
+ * @param callable(mixed):?bool $requestSampler function that decides to sample or not an unsampled + * request. */ public function __construct( Tracing $tracing, @@ -48,8 +44,8 @@ public function __construct( callable $requestSampler = null ) { $this->tracing = $tracing; - $this->parser = $parser ?? new DefaultParser; - $this->nextSpanHandler = self::buildNextSpanHandler($tracing->getTracer(), $requestSampler); + $this->parser = $parser ?? new NoopParser; + $this->nextSpanResolver = self::buildNextSpanResolver($tracing->getTracer(), $requestSampler); } public function getTracing(): Tracing @@ -57,17 +53,24 @@ 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; } - public function getNextSpanHandler(): callable + /** + * @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->nextSpanHandler; + return $this->nextSpanResolver; } - private static function buildNextSpanHandler(Tracer $tracer, ?callable $requestSampler): callable + private static function buildNextSpanResolver(Tracer $tracer, ?callable $requestSampler): callable { return static function (SamplingFlags $extractedContext, $request) use ($tracer, $requestSampler): Span { if ($extractedContext instanceof TraceContext) { 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 index ac84750d..fc405111 100644 --- a/tests/Integration/Instrumentation/Http/Server/ServerTest.php +++ b/tests/Integration/Instrumentation/Http/Server/ServerTest.php @@ -12,9 +12,9 @@ use Zipkin\Propagation\TraceContext; use Zipkin\Instrumentation\Http\Server\ServerTracing; +use Zipkin\Instrumentation\Http\Server\Psr\Middleware; +use Zipkin\Instrumentation\Http\Server\Psr\DefaultParser; use Zipkin\Instrumentation\Http\Server\Parser; -use Zipkin\Instrumentation\Http\Server\Middleware; -use Zipkin\Instrumentation\Http\Server\DefaultParser; use RingCentral\Psr7\Response; use Psr\Http\Message\ServerRequestInterface; use PHPUnit\Framework\TestCase; @@ -47,8 +47,9 @@ static function () use ($tracer, $reporter): array { public function testMiddleware() { $parser = new class() extends DefaultParser { - public function request(ServerRequestInterface $request, TraceContext $context, SpanCustomizer $span): void + 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'); 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 7586c8c6..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\Unit\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/ServerTest.php b/tests/Unit/Instrumentation/Http/Server/Psr/ServerTest.php similarity index 93% rename from tests/Unit/Instrumentation/Http/Server/ServerTest.php rename to tests/Unit/Instrumentation/Http/Server/Psr/ServerTest.php index f37d356c..b5159df9 100644 --- a/tests/Unit/Instrumentation/Http/Server/ServerTest.php +++ b/tests/Unit/Instrumentation/Http/Server/Psr/ServerTest.php @@ -2,13 +2,14 @@ declare(strict_types=1); -namespace ZipkinTests\Unit\Instrumentation\Http\Server; +namespace ZipkinTests\Unit\Instrumentation\Http\Server\Psr; use Zipkin\TracingBuilder; use Zipkin\Samplers\BinarySampler; use Zipkin\Reporters\InMemory; use Zipkin\Instrumentation\Http\Server\ServerTracing; -use Zipkin\Instrumentation\Http\Server\Middleware; +use Zipkin\Instrumentation\Http\Server\Psr\Middleware; +use Zipkin\Instrumentation\Http\Server\Psr\DefaultParser; use RingCentral\Psr7\Response as Psr7Response; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Message\ServerRequestInterface; @@ -30,7 +31,7 @@ private static function createTracing(): array $tracer = $tracing->getTracer(); return [ - new ServerTracing($tracing), + new ServerTracing($tracing, new DefaultParser()), static function () use ($tracer, $reporter): array { $tracer->flush(); return $reporter->flush();