├── codecov.yml
├── .gitignore
├── .coveralls.yml
├── tests
├── Integration
│ ├── Instrumentation
│ │ ├── Mysqli
│ │ │ ├── access.cnf
│ │ │ ├── docker-compose.yaml
│ │ │ └── MysqliTest.php
│ │ └── Http
│ │ │ └── Server
│ │ │ └── MiddlewareTest.php
│ └── Reporters
│ │ └── Http
│ │ └── CurlFactoryTest.php
└── Unit
│ ├── InSpan
│ ├── Callables.php
│ └── Sumer.php
│ ├── DefaultErrorParserException.php
│ ├── Reporters
│ ├── NoopTest.php
│ ├── InMemoryTest.php
│ ├── LogTest.php
│ ├── HttpMockFactory.php
│ ├── Log
│ │ └── LogSerializerTest.php
│ ├── HttpTest.php
│ └── JsonV2SerializerTest.php
│ ├── NoopSpanTest.php
│ ├── Samplers
│ └── BinarySamplerTest.php
│ ├── Instrumentation
│ └── Http
│ │ ├── Client
│ │ ├── Psr18
│ │ │ ├── RequestTestCase.php
│ │ │ ├── ResponseTestCase.php
│ │ │ └── ClientTest.php
│ │ ├── BaseResponseTestCase.php
│ │ └── BaseRequestTestCase.php
│ │ └── Server
│ │ ├── Psr15
│ │ ├── RequestTestCase.php
│ │ ├── ResponseTestCase.php
│ │ └── MiddlewareTest.php
│ │ ├── BaseRequestTestCase.php
│ │ └── BaseResponseTestCase.php
│ ├── TimestampTest.php
│ ├── Recording
│ ├── SpanTest.php
│ └── SpanMapTest.php
│ ├── Propagation
│ ├── DefaultSamplingFlagsTest.php
│ ├── CurrentTraceContextTest.php
│ ├── ServerHeadersTest.php
│ ├── RequestHeadersTest.php
│ ├── ExtraFieldTest.php
│ ├── IdTest.php
│ └── MapTest.php
│ ├── DefaultErrorParserTest.php
│ ├── RecorderTest.php
│ ├── DefaultTracingTest.php
│ ├── SpanCustomizerShieldTest.php
│ ├── SpanCustomizerShieldSpan.php
│ ├── EndpointTest.php
│ ├── RealSpanTest.php
│ └── TracingBuilderTest.php
├── src
└── Zipkin
│ ├── Annotations.php
│ ├── Instrumentation
│ ├── Http
│ │ ├── Client
│ │ │ ├── Request.php
│ │ │ ├── Response.php
│ │ │ ├── Psr18
│ │ │ │ ├── Propagation
│ │ │ │ │ └── RequestHeaders.php
│ │ │ │ ├── README.md
│ │ │ │ ├── Response.php
│ │ │ │ ├── Request.php
│ │ │ │ └── Client.php
│ │ │ ├── HttpClientTracing.php
│ │ │ ├── HttpClientParser.php
│ │ │ └── DefaultHttpClientParser.php
│ │ ├── Server
│ │ │ ├── Psr15
│ │ │ │ ├── Propagation
│ │ │ │ │ └── RequestHeaders.php
│ │ │ │ ├── Response.php
│ │ │ │ ├── Request.php
│ │ │ │ ├── README.md
│ │ │ │ └── Middleware.php
│ │ │ ├── Response.php
│ │ │ ├── Request.php
│ │ │ ├── HttpServerParser.php
│ │ │ ├── HttpServerTracing.php
│ │ │ └── DefaultHttpServerParser.php
│ │ ├── Response.php
│ │ └── Request.php
│ └── README.md
│ ├── DefaultErrorParser.php
│ ├── Reporter.php
│ ├── ErrorParser.php
│ ├── Reporters
│ ├── Noop.php
│ ├── SpanSerializer.php
│ ├── InMemory.php
│ ├── Http
│ │ ├── ClientFactory.php
│ │ └── CurlFactory.php
│ ├── Log.php
│ ├── Log
│ │ └── LogSerializer.php
│ ├── Http.php
│ └── JsonV2Serializer.php
│ ├── Propagation
│ ├── Exceptions
│ │ ├── InvalidPropagationKey.php
│ │ ├── InvalidPropagationCarrier.php
│ │ └── InvalidTraceContextArgument.php
│ ├── Getter.php
│ ├── Setter.php
│ ├── RemoteSetter.php
│ ├── SamplingFlags.php
│ ├── Id.php
│ ├── RequestHeaders.php
│ ├── ServerHeaders.php
│ ├── CurrentTraceContext.php
│ ├── DefaultSamplingFlags.php
│ ├── Map.php
│ ├── ExtraField.php
│ └── Propagation.php
│ ├── Timestamp.php
│ ├── NoopSpanCustomizer.php
│ ├── Samplers
│ ├── BinarySampler.php
│ └── PercentageSampler.php
│ ├── Tracing.php
│ ├── Recording
│ ├── ReadbackSpan.php
│ └── SpanMap.php
│ ├── Sampler.php
│ ├── SpanCustomizerShield.php
│ ├── SpanName.php
│ ├── Kind.php
│ ├── SpanCustomizer.php
│ ├── DefaultTracing.php
│ ├── Endpoint.php
│ ├── Tags.php
│ ├── Span.php
│ ├── Recorder.php
│ ├── NoopSpan.php
│ ├── RealSpan.php
│ └── TracingBuilder.php
├── .php-cs-fixer.dist.php
├── phpunit.xml
├── .github
└── workflows
│ └── ci.yml
├── phpstan.neon
└── composer.json
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project: on
4 | patch: off
5 |
6 | comment: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | composer.lock
3 | /vendor/
4 | .phpunit.result.cache
5 | .php-cs-fixer.cache
6 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: github-actions
2 | coverage_clover: build/logs/clover.xml
3 | json_path: build/logs/coveralls-upload.json
4 |
--------------------------------------------------------------------------------
/tests/Integration/Instrumentation/Mysqli/access.cnf:
--------------------------------------------------------------------------------
1 | [mysqld]
2 | # makes it possible to connect to the server using `mysql` as host
3 | bind-address = 0.0.0.0
--------------------------------------------------------------------------------
/src/Zipkin/Annotations.php:
--------------------------------------------------------------------------------
1 | message = 'default error';
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Zipkin/DefaultErrorParser.php:
--------------------------------------------------------------------------------
1 | $e->getMessage()];
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/Unit/InSpan/Sumer.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(Reporter::class, $noopReporter);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Zipkin/Propagation/Setter.php:
--------------------------------------------------------------------------------
1 | in('src')
7 | ->in('tests')
8 | ;
9 |
10 | return (new PhpCsFixer\Config())
11 | ->setParallelConfig(ParallelConfigFactory::detect())
12 | ->setRules(
13 | [
14 | 'nullable_type_declaration_for_default_null_value' => true,
15 | ]
16 | )
17 | ->setFinder($finder)
18 | ;
--------------------------------------------------------------------------------
/tests/Unit/NoopSpanTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($span instanceof NoopSpan);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Zipkin/Propagation/RemoteSetter.php:
--------------------------------------------------------------------------------
1 | connect_errno) {
9 | printf("Connect failed: %s\n", $mysqli->connect_error);
10 | exit();
11 | }
12 |
13 | $mysqli->begin_transaction(MYSQLI_TRANS_START_READ_ONLY);
14 |
15 | $mysqli->query("SELECT first_name, last_name FROM actor");
16 | $mysqli->commit();
17 |
18 | $mysqli->close();
19 | ```
20 |
--------------------------------------------------------------------------------
/tests/Integration/Instrumentation/Mysqli/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "2.4"
2 |
3 | services:
4 | mysql:
5 | image: mysql:latest
6 | container_name: zipkin_php_mysql_test
7 | environment:
8 | - MYSQL_ROOT_PASSWORD=root
9 | - MYSQL_DATABASE=test
10 | - MYSQL_ALLOW_EMPTY_PASSWORD=yes
11 | volumes:
12 | - ./access.cnf:/etc/mysql/conf.d/access.cnf
13 | ports:
14 | - "3306:3306"
15 | healthcheck:
16 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
17 | timeout: 20s
18 | retries: 10
--------------------------------------------------------------------------------
/src/Zipkin/NoopSpanCustomizer.php:
--------------------------------------------------------------------------------
1 | assertTrue($sampler->isSampled('1'));
14 | }
15 |
16 | public function testNeverSample()
17 | {
18 | $sampler = BinarySampler::createAsNeverSample();
19 | $this->assertFalse($sampler->isSampled('1'));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Zipkin/Instrumentation/Http/Server/Response.php:
--------------------------------------------------------------------------------
1 | spans = [...$this->spans, ...$spans];
20 | }
21 |
22 | /**
23 | * @return array|Span[]
24 | */
25 | public function flush(): array
26 | {
27 | $spans = $this->spans;
28 | $this->spans = [];
29 | return $spans;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Zipkin/Reporters/Http/ClientFactory.php:
--------------------------------------------------------------------------------
1 |
13 | * $options = [
14 | * 'endpoint_url' => 'http://myzipkin:9411/api/v2/spans', // the reporting url for zipkin server
15 | * 'headers' => ['X-API-Key' => 'abc123'] // the additional headers to be included in the request
16 | * 'timeout' => 10, // the timeout for the request in seconds
17 | * ];
18 | *
19 | *
20 | * @return callable(string):void
21 | */
22 | public function build(array $options): callable;
23 | }
24 |
--------------------------------------------------------------------------------
/tests/Unit/Instrumentation/Http/Client/Psr18/RequestTestCase.php:
--------------------------------------------------------------------------------
1 | isSampled = $isSampled;
16 | }
17 |
18 | public static function createAsAlwaysSample(): self
19 | {
20 | return new self(true);
21 | }
22 |
23 | public static function createAsNeverSample(): self
24 | {
25 | return new self(false);
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function isSampled(string $traceId): bool
32 | {
33 | return $this->isSampled;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/Unit/Instrumentation/Http/Server/Psr15/RequestTestCase.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
22 | $this->serializer = $serializer ?? new JsonV2Serializer();
23 | }
24 |
25 | /**
26 | * @param Span[] $spans
27 | */
28 | public function report(array $spans): void
29 | {
30 | $this->logger->info($this->serializer->serialize($spans));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Unit/Reporters/InMemoryTest.php:
--------------------------------------------------------------------------------
1 | havingSampler(BinarySampler::createAsAlwaysSample())
17 | ->havingReporter($reporter)
18 | ->build();
19 |
20 | $span = $tracing->getTracer()->nextSpan();
21 | $span->start();
22 | $span->finish();
23 |
24 | $tracing->getTracer()->flush();
25 | $flushedSpans = $reporter->flush();
26 |
27 | $this->assertCount(1, $flushedSpans);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Zipkin/Propagation/Id.php:
--------------------------------------------------------------------------------
1 | 0 && \strlen($value) <= 32;
31 | }
32 |
33 | /**
34 | * @param string $value
35 | * @return bool
36 | */
37 | function isValidSpanId(string $value): bool
38 | {
39 | return \ctype_xdigit($value) &&
40 | \strlen($value) > 0 && \strlen($value) <= 16;
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Unit/TimestampTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(16, strlen((string) $now));
15 | }
16 |
17 | /**
18 | * @dataProvider timestampProvider
19 | */
20 | public function testIsValidProducesTheExpectedOutput($timestamp, $isValid)
21 | {
22 | $this->assertEquals($isValid, isValid($timestamp));
23 | }
24 |
25 | public function timestampProvider()
26 | {
27 | return [
28 | [now(), true],
29 | [-1, false],
30 | [1234567890123456, true],
31 | [123456789012345, false],
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Unit/Reporters/LogTest.php:
--------------------------------------------------------------------------------
1 | prophesize(LoggerInterface::class);
22 | $logger->info(Argument::that(function ($serialized) {
23 | return strlen($serialized) > 2;
24 | }))->shouldBeCalled();
25 |
26 | $span = Span::createFromContext(TraceContext::createAsRoot(), Endpoint::createAsEmpty());
27 |
28 | $reporter = new Log($logger->reveal());
29 | $reporter->report([$span]);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Zipkin/Tracing.php:
--------------------------------------------------------------------------------
1 | Conventionally associated with the key "http.status_code"
26 | */
27 | abstract public function getStatusCode(): int;
28 |
29 | /**
30 | * @return mixed the underlying response object or {@code null} if there is none.
31 | */
32 | abstract public function unwrap();
33 | }
34 |
--------------------------------------------------------------------------------
/src/Zipkin/Propagation/Exceptions/InvalidTraceContextArgument.php:
--------------------------------------------------------------------------------
1 | hasHeader($key) ? $carrier->getHeader($key)[0] : null;
21 | }
22 |
23 | /**
24 | * {@inheritdoc}
25 | *
26 | * @param RequestInterface $carrier
27 | * @throws \InvalidArgumentException for invalid header names or values.
28 | */
29 | public function put(&$carrier, string $key, string $value): void
30 | {
31 | $carrier = $carrier->withHeader(\strtolower($key), $value);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Zipkin/Recording/ReadbackSpan.php:
--------------------------------------------------------------------------------
1 | rate = $rate;
17 | }
18 |
19 | /**
20 | * @param float $rate
21 | * @return PercentageSampler
22 | * @throws InvalidArgumentException
23 | */
24 | public static function create(float $rate): self
25 | {
26 | if ($rate > 1 || $rate < 0) {
27 | throw new InvalidArgumentException(
28 | \sprintf('Invalid rate. Expected a value between 0 and 1, got %f', $rate)
29 | );
30 | }
31 | return new self($rate);
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function isSampled(string $traceId): bool
38 | {
39 | return (\mt_rand(0, 99) / 100) < $this->rate;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Zipkin/Sampler.php:
--------------------------------------------------------------------------------
1 | The route is associated with the request, but it may not be visible until response
18 | * processing. The reasons is that many server implementations process the request before they can
19 | * identify the route. Parsing should expect this and look at {@link HttpResponse#route()} as
20 | * needed.
21 | *
22 | * @see HTTP_ROUTE
23 | */
24 | public function getRoute(): ?string
25 | {
26 | return null;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
Unlike {@link #SERVER}, messaging spans never share a span ID. For example, the {@link 28 | * #PRODUCER} of this message is the {@link TraceContext#parentId()} of this span. 29 | */ 30 | const CONSUMER = 'CONSUMER'; 31 | -------------------------------------------------------------------------------- /tests/Unit/DefaultErrorParserTest.php: -------------------------------------------------------------------------------- 1 | parseTags($e); 24 | $this->assertEquals($expectedValue, $tags['error']); 25 | } 26 | 27 | public function throwables(): array 28 | { 29 | return [ 30 | 'known exception' => [new DefaultErrorParserException, 'default error'], 31 | 'std exception' => [new RuntimeException('runtime error'), 'runtime error'], 32 | 'anonymous throwable' => [ 33 | new class('anonymous error') extends RuntimeException { 34 | }, 35 | 'anonymous error', 36 | ], 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Unit/Instrumentation/Http/Client/Psr18/ResponseTestCase.php: -------------------------------------------------------------------------------- 1 | prophesize(Reporter::class); 22 | $recorder = new Recorder(Endpoint::createAsEmpty(), $reporter->reveal(), false); 23 | $this->assertNull($recorder->getTimestamp($context)); 24 | } 25 | 26 | public function testStartSuccess() 27 | { 28 | $context = TraceContext::createAsRoot(DefaultSamplingFlags::createAsEmpty()); 29 | $reporter = $this->prophesize(Reporter::class); 30 | $recorder = new Recorder(Endpoint::createAsEmpty(), $reporter->reveal(), false); 31 | $timestamp = now(); 32 | $recorder->start($context, $timestamp); 33 | $this->assertEquals($timestamp, $recorder->getTimestamp($context)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Server/HttpServerParser.php: -------------------------------------------------------------------------------- 1 | randomBool(); 24 | 25 | $tracing = new DefaultTracing( 26 | $localEndpoint, 27 | $reporter, 28 | $sampler, 29 | false, 30 | new CurrentTraceContext, 31 | $isNoop, 32 | new B3, 33 | true 34 | ); 35 | 36 | $this->assertInstanceOf(Tracing::class, $tracing); 37 | $this->assertInstanceOf(Tracer::class, $tracing->getTracer()); 38 | $this->assertInstanceOf(Propagation::class, $tracing->getPropagation()); 39 | } 40 | 41 | private function randomBool() 42 | { 43 | return (mt_rand(0, 1) === 1); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Client/Psr18/Request.php: -------------------------------------------------------------------------------- 1 | delegate = $delegate; 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function getMethod(): string 23 | { 24 | return $this->delegate->getMethod(); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getPath(): ?string 31 | { 32 | return $this->delegate->getUri()->getPath() ?: '/'; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getUrl(): string 39 | { 40 | return $this->delegate->getUri()->__toString(); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getHeader(string $name): ?string 47 | { 48 | return $this->delegate->getHeaderLine($name) ?: null; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function unwrap(): RequestInterface 55 | { 56 | return $this->delegate; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Server/Psr15/Request.php: -------------------------------------------------------------------------------- 1 | delegate = $delegate; 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function getMethod(): string 23 | { 24 | return $this->delegate->getMethod(); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getPath(): ?string 31 | { 32 | return $this->delegate->getUri()->getPath() ?: '/'; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getUrl(): string 39 | { 40 | return $this->delegate->getUri()->__toString(); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getHeader(string $name): ?string 47 | { 48 | return $this->delegate->getHeaderLine($name) ?: null; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function unwrap(): RequestInterface 55 | { 56 | return $this->delegate; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Server/HttpServerTracing.php: -------------------------------------------------------------------------------- 1 | tracing = $tracing; 33 | $this->parser = $parser ?? new DefaultHttpServerParser(); 34 | $this->requestSampler = $requestSampler; 35 | } 36 | 37 | public function getTracing(): Tracing 38 | { 39 | return $this->tracing; 40 | } 41 | 42 | /** 43 | * @return (callable(Request):?bool)|null 44 | */ 45 | public function getRequestSampler(): ?callable 46 | { 47 | return $this->requestSampler; 48 | } 49 | 50 | public function getParser(): HttpServerParser 51 | { 52 | return $this->parser; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Client/HttpClientTracing.php: -------------------------------------------------------------------------------- 1 | tracing = $tracing; 34 | $this->parser = $parser ?? new DefaultHttpClientParser(); 35 | $this->requestSampler = $requestSampler; 36 | } 37 | 38 | public function getTracing(): Tracing 39 | { 40 | return $this->tracing; 41 | } 42 | 43 | /** 44 | * @return (callable(Request):?bool)|null 45 | */ 46 | public function getRequestSampler(): ?callable 47 | { 48 | return $this->requestSampler; 49 | } 50 | 51 | public function getParser(): HttpClientParser 52 | { 53 | return $this->parser; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Client/HttpClientParser.php: -------------------------------------------------------------------------------- 1 | assertEquals($context, $currentTraceContext->getContext()); 16 | } 17 | 18 | /** 19 | * @dataProvider contextProvider 20 | */ 21 | public function testNewScopeSuccess($context1) 22 | { 23 | $currentTraceContext = new CurrentTraceContext($context1); 24 | $context2 = TraceContext::createAsRoot(); 25 | 26 | $scopeCloser = $currentTraceContext->createScopeAndRetrieveItsCloser($context2); 27 | $this->assertEquals($context2, $currentTraceContext->getContext()); 28 | 29 | $scopeCloser(); 30 | $this->assertEquals($context1, $currentTraceContext->getContext()); 31 | 32 | /** Verifies idempotency */ 33 | $scopeCloser(); 34 | $this->assertEquals($context1, $currentTraceContext->getContext()); 35 | } 36 | 37 | public function contextProvider() 38 | { 39 | return [ 40 | [ TraceContext::createAsRoot() ], 41 | [ null ] 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Server/Psr15/README.md: -------------------------------------------------------------------------------- 1 | # Zipkin instrumentation for PSR15 HTTP Server 2 | 3 | This component contains the instrumentation for the standard [PSR15 HTTP servers](https://www.php-fig.org/psr/psr-15/). 4 | 5 | ## Getting started 6 | 7 | Before using this library, make sure the interfaces for PSR15 HTTP server are installed: 8 | 9 | ```bash 10 | composer require psr/http-server-middleware 11 | ``` 12 | 13 | ## Usage 14 | 15 | 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. 16 | 17 | ```php 18 | use Zipkin\Instrumentation\Http\Server\Psr15\Middleware as ZipkinMiddleware; 19 | use Zipkin\Instrumentation\Http\Server\HttpServerTracing; 20 | 21 | // Create the routing dispatcher 22 | $fastRouteDispatcher = FastRoute\simpleDispatcher(function (FastRoute\RouteCollector $r) { 23 | $r->get('/hello/{name}', HelloWorldController::class); 24 | }); 25 | 26 | // Creates tracing component 27 | $tracing = TracingBuilder::create() 28 | ->havingLocalServiceName('my_service') 29 | ->build(); 30 | 31 | $httpClientTracing = new HttpServerTracing($tracing); 32 | 33 | $dispatcher = new Dispatcher([ 34 | new Middlewares\FastRoute($fastRouteDispatcher), 35 | // ... 36 | new ZipkinMiddleware($serverTracing), 37 | new Middlewares\RequestHandler(), 38 | ]); 39 | 40 | $response = $dispatcher->dispatch(new ServerRequest('/hello/world')); 41 | ``` 42 | -------------------------------------------------------------------------------- /src/Zipkin/SpanCustomizer.php: -------------------------------------------------------------------------------- 1 | This type is safer to expose directly to users than {@link Span}, as it has no hooks that 12 | * can affect the span lifecycle. 13 | */ 14 | interface SpanCustomizer 15 | { 16 | /** 17 | * Sets the string name for the logical operation this span represents. 18 | * 19 | * @param string $name 20 | * @return void 21 | */ 22 | public function setName(string $name): void; 23 | 24 | /** 25 | * Tags give your span context for search, viewing and analysis. For example, a key 26 | * "your_app.version" would let you lookup spans by version. A tag {@link Zipkin\Tags\SQL_QUERY} 27 | * isn't searchable, but it can help in debugging when viewing a trace. 28 | * 29 | * @param string $key Name used to lookup spans, such as "your_app.version". See {@link Zipkin\Tags} for 30 | * standard ones. 31 | * @param string $value value. 32 | * @return void 33 | */ 34 | public function tag(string $key, string $value): void; 35 | 36 | /** 37 | * Associates an event that explains latency with the current system time. 38 | * 39 | * @param string $value A short tag indicating the event, like "finagle.retry" 40 | * @param int|null $timestamp 41 | * @return void 42 | * @see Annotations 43 | */ 44 | public function annotate(string $value, ?int $timestamp = null): void; 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unit/Instrumentation/Http/Client/BaseResponseTestCase.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Response::class, $response); 42 | 43 | $this->assertEquals(202, $response->getStatusCode()); 44 | $this->assertSame($request, $response->getRequest()); 45 | $this->assertSame($delegateResponse, $response->unwrap()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Unit/Propagation/ServerHeadersTest.php: -------------------------------------------------------------------------------- 1 | getExtractor(new ServerHeaders); 20 | $extracted = $extractor($server); 21 | 22 | $this->assertTrue($extracted instanceof DefaultSamplingFlags); 23 | } 24 | 25 | public function testCorrectlyParsesHeaders() 26 | { 27 | $traceId = generateNextId(); 28 | $spanId = generateNextId(); 29 | $parentSpanId = generateNextId(); 30 | $server = [ 31 | 'HTTP_X_B3_TRACEID' => $traceId, 32 | 'HTTP_X_B3_SPANID' => $spanId, 33 | 'HTTP_X_B3_PARENTSPANID' => $parentSpanId, 34 | 'HTTP_X_B3_SAMPLED' => '1', 35 | ]; 36 | 37 | $propagation = new B3(); 38 | $extractor = $propagation->getExtractor(new ServerHeaders); 39 | $extracted = $extractor($server); 40 | 41 | $this->assertTrue($extracted instanceof TraceContext); 42 | $this->assertEquals($traceId, $extracted->getTraceId()); 43 | $this->assertEquals($spanId, $extracted->getSpanId()); 44 | $this->assertEquals($parentSpanId, $extracted->getParentId()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/Reporters/HttpMockFactory.php: -------------------------------------------------------------------------------- 1 | shouldFail = $shouldFail; 30 | } 31 | 32 | public static function createAsSuccess(): self 33 | { 34 | return new self(false); 35 | } 36 | 37 | public static function createAsFailing(): self 38 | { 39 | return new self(true); 40 | } 41 | 42 | /** 43 | * @param array $options 44 | * @return callable(string):void 45 | */ 46 | public function build(array $options): callable 47 | { 48 | $self = $this; 49 | 50 | return function (string $payload) use (&$self): void { 51 | if ($self->shouldFail) { 52 | throw new RuntimeException(self::ERROR_MESSAGE); 53 | } 54 | 55 | $self->calledTimes += 1; 56 | $self->content = $payload; 57 | }; 58 | } 59 | 60 | public function retrieveContent(): string 61 | { 62 | return $this->content; 63 | } 64 | 65 | public function calledTimes(): int 66 | { 67 | return $this->calledTimes; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Zipkin/Recording/SpanMap.php: -------------------------------------------------------------------------------- 1 | map[$contextHash] ?? null; 22 | } 23 | 24 | public function getOrCreate(TraceContext $context, Endpoint $endpoint): Span 25 | { 26 | $contextHash = self::getHash($context); 27 | 28 | if (!\array_key_exists($contextHash, $this->map)) { 29 | $this->map[$contextHash] = Span::createFromContext($context, $endpoint); 30 | } 31 | 32 | return $this->map[$contextHash]; 33 | } 34 | 35 | public function remove(TraceContext $context): ?Span 36 | { 37 | $contextHash = self::getHash($context); 38 | 39 | if (!\array_key_exists($contextHash, $this->map)) { 40 | return null; 41 | } 42 | 43 | $span = $this->map[$contextHash]; 44 | 45 | unset($this->map[$contextHash]); 46 | 47 | return $span; 48 | } 49 | 50 | /** 51 | * @return Span[] 52 | */ 53 | public function removeAll(): array 54 | { 55 | $spans = $this->map; 56 | $this->map = []; 57 | return \array_values($spans); 58 | } 59 | 60 | private static function getHash(TraceContext $context): int 61 | { 62 | return \crc32($context->getSpanId() . $context->getTraceId()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Zipkin/Propagation/CurrentTraceContext.php: -------------------------------------------------------------------------------- 1 | This type is an SPI, and intended to be used by implementors looking to change thread-local 11 | * storage, or integrate with other contexts such as logging (MDC). 12 | * 13 | *
It is part of the HTTP RFC 25 | * that an HTTP method is case-sensitive. Do not downcase results. 26 | * 27 | * @see HTTP_METHOD 28 | */ 29 | abstract public function getMethod(): string; 30 | 31 | /** 32 | * The absolute http path, without any query parameters. Ex. "/objects/abcd-ff" 33 | * 34 | *
Conventionally associated with the key "http.path" 35 | * 36 | *
{@code null} could mean not applicable to the HTTP method (ex CONNECT). 37 | * 38 | *
Conventionally associated with the key "http.url"
52 | *
53 | * @see HTTP_URL
54 | */
55 | abstract public function getUrl(): string;
56 |
57 | /**
58 | * Returns one value corresponding to the specified header, or null.
59 | */
60 | abstract public function getHeader(string $name): ?string;
61 |
62 | /**
63 | * @return mixed the underlying request object or {@code null} if there is none.
64 | */
65 | abstract public function unwrap();
66 | }
67 |
--------------------------------------------------------------------------------
/src/Zipkin/Propagation/Map.php:
--------------------------------------------------------------------------------
1 | offsetExists($lKey) ? $carrier->offsetGet($lKey) : null;
30 | }
31 |
32 | if (\is_array($carrier)) {
33 | if (empty($carrier)) {
34 | return null;
35 | }
36 |
37 | foreach ($carrier as $k => $value) {
38 | if (strtolower((string) $k) === $lKey) {
39 | return $value;
40 | }
41 | }
42 |
43 | return null;
44 | }
45 |
46 | throw InvalidPropagationCarrier::forCarrier($carrier);
47 | }
48 |
49 | /**
50 | * {@inheritdoc}
51 | * @param array|ArrayAccess $carrier
52 | */
53 | public function put(&$carrier, string $key, string $value): void
54 | {
55 | if ($key === '') {
56 | throw InvalidPropagationKey::forEmptyKey();
57 | }
58 |
59 | // Lowercasing the key was a first attempt to be compatible with the
60 | // getter when using the Map getter for HTTP headers.
61 | $lKey = \strtolower($key);
62 |
63 | if ($carrier instanceof ArrayAccess || \is_array($carrier)) {
64 | $carrier[$lKey] = $value;
65 | return;
66 | }
67 |
68 | throw InvalidPropagationCarrier::forCarrier($carrier);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/tests/Unit/Reporters/HttpTest.php:
--------------------------------------------------------------------------------
1 | start($now);
31 | $span->setName('test');
32 | $payload = sprintf(self::PAYLOAD, $context->getSpanId(), $context->getTraceId(), $now);
33 | $logger = $this->prophesize(LoggerInterface::class);
34 | $logger->error()->shouldNotBeCalled();
35 |
36 | $mockFactory = HttpMockFactory::createAsSuccess();
37 | $httpReporter = new Http([], $mockFactory, $logger->reveal());
38 | $httpReporter->report([$span]);
39 |
40 | $this->assertEquals($payload, $mockFactory->retrieveContent());
41 | }
42 |
43 | public function testHttpReporterFails()
44 | {
45 | $context = TraceContext::createAsRoot();
46 | $localEndpoint = Endpoint::createAsEmpty();
47 | $span = Span::createFromContext($context, $localEndpoint);
48 | $span->start(Timestamp\now());
49 | $logger = $this->prophesize(LoggerInterface::class);
50 | $logger->error(Argument::containingString(HttpMockFactory::ERROR_MESSAGE))->shouldBeCalled();
51 |
52 | $mockFactory = HttpMockFactory::createAsFailing();
53 | $httpReporter = new Http([], $mockFactory, $logger->reveal());
54 | $httpReporter->report([$span]);
55 | }
56 |
57 | public function testHttpReportsEmptySpansSuccess()
58 | {
59 | $logger = $this->prophesize(LoggerInterface::class);
60 | $logger->error()->shouldNotBeCalled();
61 |
62 | $mockFactory = HttpMockFactory::createAsFailing();
63 | $httpReporter = new Http([], $mockFactory, $logger->reveal());
64 | $httpReporter->report([]);
65 |
66 | $this->assertEquals(0, $mockFactory->calledTimes());
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Zipkin/Instrumentation/Http/Server/DefaultHttpServerParser.php:
--------------------------------------------------------------------------------
1 | getMethod()
27 | . ($request->getRoute() === null ? '' : ' ' . $request->getRoute());
28 | }
29 |
30 | /**
31 | * {@inhertidoc}
32 | */
33 | public function request(Request $request, TraceContext $context, SpanCustomizer $span): void
34 | {
35 | $span->setName($this->spanNameFromRequest($request));
36 | $span->tag(Tags\HTTP_METHOD, $request->getMethod());
37 | $span->tag(Tags\HTTP_PATH, $request->getPath() ?: '/');
38 | }
39 |
40 | /**
41 | * spanNameFromResponse returns an appropiate span name based on the response's request,
42 | * usually seeking for a better name than the HTTP method (e.g. GET /user/{user_id}).
43 | */
44 | protected function spanNameFromResponse(Response $response): ?string
45 | {
46 | if ($response->getRoute() === null || $response->getRequest() === null) {
47 | return null;
48 | }
49 |
50 | return $response->getRequest()->getMethod() . ' ' . $response->getRoute();
51 | }
52 |
53 |
54 | /**
55 | * {@inhertidoc}
56 | */
57 | public function response(Response $response, TraceContext $context, SpanCustomizer $span): void
58 | {
59 | $spanName = $this->spanNameFromResponse($response);
60 | if ($spanName !== null) {
61 | $span->setName($spanName);
62 | }
63 |
64 | $statusCode = $response->getStatusCode();
65 | if ($statusCode < 200 || $statusCode > 299) {
66 | $span->tag(Tags\HTTP_STATUS_CODE, (string) $statusCode);
67 | }
68 |
69 | if ($statusCode > 399) {
70 | $span->tag(Tags\ERROR, (string) $statusCode);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/Unit/Propagation/MapTest.php:
--------------------------------------------------------------------------------
1 | expectException(InvalidPropagationCarrier::class);
25 | $map->get($carrier, self::TEST_KEY);
26 | }
27 |
28 | public function testGetFromMapSuccessForArrayAccessCarrier()
29 | {
30 | $carrier = new ArrayObject([self::TEST_KEY => self::TEST_VALUE]);
31 | $map = new Map();
32 | $value = $map->get($carrier, self::TEST_KEY);
33 | $this->assertEquals(self::TEST_VALUE, $value);
34 | }
35 |
36 | public function testGetFromMapCaseInsensitiveSuccess()
37 | {
38 | $carrier = [self::TEST_KEY_INSENSITIVE => self::TEST_VALUE];
39 | $map = new Map();
40 | $value = $map->get($carrier, self::TEST_KEY);
41 | $this->assertEquals(self::TEST_VALUE, $value);
42 | }
43 |
44 | public function testGetFromMapCaseInsensitiveReturnsNull()
45 | {
46 | $carrier = new ArrayObject([self::TEST_KEY_INSENSITIVE => self::TEST_VALUE]);
47 | $map = new Map();
48 | $value = $map->get($carrier, self::TEST_KEY);
49 | $this->assertNull($value);
50 | }
51 |
52 | public function testPutToMapFailsDueToEmptyKey()
53 | {
54 | $carrier = new ArrayObject();
55 | $map = new Map();
56 | $this->expectException(InvalidPropagationKey::class);
57 | $map->put($carrier, self::TEST_EMPTY_KEY, self::TEST_VALUE);
58 | }
59 |
60 | public function testPutToMapFailsDueToInvalidCarrier()
61 | {
62 | $carrier = new stdClass();
63 | $map = new Map();
64 | $this->expectException(InvalidPropagationCarrier::class);
65 | $map->put($carrier, self::TEST_KEY, self::TEST_VALUE);
66 | }
67 |
68 | public function testPutToMapSuccess()
69 | {
70 | $carrier = new ArrayObject([self::TEST_KEY => self::TEST_VALUE]);
71 | $map = new Map();
72 | $map->put($carrier, self::TEST_KEY, self::TEST_VALUE);
73 | $value = $map->get($carrier, self::TEST_KEY);
74 | $this->assertEquals(self::TEST_VALUE, $value);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openzipkin/zipkin",
3 | "type": "library",
4 | "description": "A Zipkin instrumentation for PHP",
5 | "keywords": [
6 | "zipkin",
7 | "distributed-tracing",
8 | "tracing",
9 | "openzipkin"
10 | ],
11 | "license": "Apache-2.0",
12 | "authors": [
13 | {
14 | "name": "José Carlos Chávez",
15 | "email": "jcchavezs@gmail.com"
16 | }
17 | ],
18 | "homepage": "https://github.com/openzipkin/zipkin-php",
19 | "support": {
20 | "issues": "https://github.com/openzipkin/zipkin-php/issues"
21 | },
22 | "require": {
23 | "php": "^7.4 || ^8.0",
24 | "ext-curl": "*",
25 | "psr/http-message": "~1.0 || ~2.0",
26 | "psr/log": "^1.0 || ^2.0 || ^3.0"
27 | },
28 | "require-dev": {
29 | "ext-mysqli": "*",
30 | "friendsofphp/php-cs-fixer": "^3.75",
31 | "jcchavezs/httptest": "~0.2",
32 | "middlewares/fast-route": "^2.0",
33 | "middlewares/request-handler": "^2.0",
34 | "nyholm/psr7": "^1.4",
35 | "phpspec/prophecy-phpunit": "^2.0",
36 | "phpstan/phpstan": "^0.12.26",
37 | "phpunit/phpunit": "~9",
38 | "psr/http-client": "^1.0",
39 | "psr/http-server-middleware": "^1.0",
40 | "squizlabs/php_codesniffer": "3.*"
41 | },
42 | "config": {
43 | "sort-packages": true
44 | },
45 | "autoload": {
46 | "psr-4": {
47 | "Zipkin\\": "./src/Zipkin/"
48 | },
49 | "files": [
50 | "./src/Zipkin/Propagation/Id.php",
51 | "./src/Zipkin/Timestamp.php",
52 | "./src/Zipkin/Kind.php",
53 | "./src/Zipkin/Tags.php",
54 | "./src/Zipkin/Annotations.php",
55 | "./src/Zipkin/SpanName.php"
56 | ]
57 | },
58 | "autoload-dev": {
59 | "psr-4": {
60 | "ZipkinTests\\": "./tests/"
61 | },
62 | "files": [
63 | "./tests/Unit/InSpan/Callables.php"
64 | ]
65 | },
66 | "minimum-stability": "stable",
67 | "scripts": {
68 | "fix-lint": "phpcbf --standard=ZEND --standard=PSR2 --ignore=*/vendor/* ./",
69 | "lint": "phpcs --standard=ZEND --standard=PSR2 --ignore=*/vendor/* ./",
70 | "test": "phpunit tests",
71 | "test-unit": "phpunit tests/Unit",
72 | "test-integration": "phpunit tests/Integration",
73 | "static-check": "phpstan analyse src --level 8"
74 | },
75 | "suggest": {
76 | "ext-mysqli": "Allows to use mysqli instrumentation.",
77 | "psr/http-client": "Allows to instrument HTTP clients following PSR18.",
78 | "psr/http-server-middleware": "Allows to instrument HTTP servers via middlewares following PSR15."
79 | }
80 | }
--------------------------------------------------------------------------------
/tests/Unit/Recording/SpanMapTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(SpanMap::class, $spanMap);
18 | }
19 |
20 | public function testGetReturnsOrCreateOnNonExistingSpan()
21 | {
22 | $spanMap = new SpanMap;
23 | $context = TraceContext::createAsRoot(DefaultSamplingFlags::createAsEmpty());
24 | $endpoint = Endpoint::createAsEmpty();
25 | $span = $spanMap->getOrCreate($context, $endpoint);
26 | $this->assertInstanceOf(Span::class, $span);
27 | }
28 |
29 | public function testGetReturnsNullOnNonExistingSpan()
30 | {
31 | $spanMap = new SpanMap;
32 | $context = TraceContext::createAsRoot(DefaultSamplingFlags::createAsEmpty());
33 | $this->assertNull($spanMap->get($context));
34 | }
35 |
36 | public function testGetReturnsDifferentObjects()
37 | {
38 | $spanMap = new SpanMap;
39 | $endpoint = Endpoint::createAsEmpty();
40 | $rootSpan = TraceContext::createAsRoot(DefaultSamplingFlags::createAsEmpty());
41 | $recordedSpans = [];
42 | for ($i = 0; $i < 5; $i++) {
43 | $context = TraceContext::createFromParent($rootSpan);
44 | $recordedSpans[$i] = $spanMap->getOrCreate($context, $endpoint);
45 | }
46 | for ($i = 0; $i < count($recordedSpans); $i++) {
47 | for ($j = $i + 1; $j < count($recordedSpans); $j++) {
48 | $this->assertNotSame($recordedSpans[$i], $recordedSpans[$j]);
49 | }
50 | }
51 | }
52 |
53 | public function testRemoveReturnsEmptyAfterRemoval()
54 | {
55 | $spanMap = new SpanMap;
56 | $context = TraceContext::createAsRoot(DefaultSamplingFlags::createAsEmpty());
57 | $endpoint = Endpoint::createAsEmpty();
58 | $spanMap->getOrCreate($context, $endpoint);
59 | $spanMap->remove($context);
60 | $this->assertNull($spanMap->get($context));
61 | }
62 |
63 | public function testRemoveAllReturnsEmptyAfterRemoval()
64 | {
65 | $spanMap = new SpanMap;
66 | $contexts = [];
67 | $numberOfContexts = 3;
68 |
69 | for ($i = 0; $i < $numberOfContexts; $i++) {
70 | $contexts[$i] = TraceContext::createAsRoot();
71 | $endpoint = Endpoint::createAsEmpty();
72 | $spanMap->getOrCreate($contexts[$i], $endpoint);
73 | }
74 |
75 | $spanMap->removeAll();
76 |
77 | for ($i = 0; $i < $numberOfContexts; $i++) {
78 | $this->assertNull($spanMap->get($contexts[$i]));
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Zipkin/Instrumentation/Http/Client/Psr18/Client.php:
--------------------------------------------------------------------------------
1 | delegate = $client;
40 | $this->injector = $tracing->getTracing()->getPropagation()->getInjector(new RequestHeaders());
41 | $this->tracer = $tracing->getTracing()->getTracer();
42 | $this->parser = $tracing->getParser();
43 | $this->requestSampler = $tracing->getRequestSampler();
44 | }
45 |
46 | public function sendRequest(RequestInterface $request): ResponseInterface
47 | {
48 | $parsedRequest = new Request($request);
49 | if ($this->requestSampler === null) {
50 | $span = $this->tracer->nextSpan();
51 | } else {
52 | $span = $this->tracer->nextSpanWithSampler(
53 | $this->requestSampler,
54 | [$parsedRequest]
55 | );
56 | }
57 |
58 | ($this->injector)($span->getContext(), $request);
59 |
60 | if ($span->isNoop()) {
61 | try {
62 | return $this->delegate->sendRequest($request);
63 | } finally {
64 | $span->finish();
65 | }
66 | }
67 |
68 | $span->setKind(Kind\CLIENT);
69 | // If span is NOOP it does not make sense to add customizations
70 | // to it like tags or annotations.
71 | $spanCustomizer = new SpanCustomizerShield($span);
72 | $this->parser->request($parsedRequest, $span->getContext(), $spanCustomizer);
73 |
74 | try {
75 | $span->start();
76 | $response = $this->delegate->sendRequest($request);
77 | $this->parser->response(new Response($response, $parsedRequest), $span->getContext(), $spanCustomizer);
78 | return $response;
79 | } catch (Throwable $e) {
80 | $span->setError($e);
81 | throw $e;
82 | } finally {
83 | $span->finish();
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Integration/Instrumentation/Mysqli/MysqliTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped("Running the test on non-Linux systems might be problematic");
49 | }
50 |
51 | if (!extension_loaded("mysqli")) {
52 | $this->markTestSkipped("mysqli isn't loaded");
53 | }
54 |
55 | list($params, $closer) = self::launchMySQL();
56 |
57 | $reporter = new InMemory();
58 |
59 | $tracer = new Tracer(
60 | Endpoint::createAsEmpty(),
61 | $reporter,
62 | BinarySampler::createAsAlwaysSample(),
63 | false, // usesTraceId128bits
64 | new CurrentTraceContext(),
65 | false // isNoop
66 | );
67 |
68 | try {
69 | $mysqli = new Mysqli($tracer, [], ...$params);
70 |
71 | if ($mysqli->connect_errno) {
72 | $this->fail(
73 | sprintf('Failed to connect to MySQL: %s %s', $mysqli->connect_errno, $mysqli->connect_error)
74 | );
75 | }
76 |
77 | $res = $mysqli->query('SELECT 1');
78 | $this->assertEquals(1, $res->num_rows);
79 |
80 | $tracer->flush();
81 | $spans = $reporter->flush();
82 | $this->assertEquals(1, count($spans));
83 |
84 | $span = $spans[0];
85 | $this->assertEquals('sql/query', $span->getName());
86 | } finally {
87 | $closer();
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Zipkin/Reporters/Http/CurlFactory.php:
--------------------------------------------------------------------------------
1 | 'application/json',
52 | 'Content-Length' => \strlen($payload),
53 | 'b3' => '0',
54 | ];
55 | $additionalHeaders = $options['headers'] ?? [];
56 | $headers = \array_merge($additionalHeaders, $requiredHeaders);
57 | $formattedHeaders = \array_map(function ($key, $value) {
58 | return $key . ': ' . $value;
59 | }, \array_keys($headers), $headers);
60 | \curl_setopt($handle, CURLOPT_HTTPHEADER, $formattedHeaders);
61 |
62 | if (isset($options['timeout'])) {
63 | \curl_setopt($handle, CURLOPT_TIMEOUT, $options['timeout']);
64 | }
65 |
66 | if (\curl_exec($handle) !== false) {
67 | $statusCode = \curl_getinfo($handle, CURLINFO_HTTP_CODE);
68 | \curl_close($handle);
69 |
70 | if ($statusCode !== 202) {
71 | throw new RuntimeException(
72 | \sprintf('Reporting of spans failed, status code %d', $statusCode)
73 | );
74 | }
75 | } else {
76 | throw new RuntimeException(\sprintf(
77 | 'Reporting of spans failed: %s, error code %s',
78 | \curl_error($handle),
79 | \curl_errno($handle)
80 | ));
81 | }
82 | };
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Zipkin/Reporters/Http.php:
--------------------------------------------------------------------------------
1 | 'http://localhost:9411/api/v2/spans',
21 | ];
22 |
23 | private ClientFactory $clientFactory;
24 |
25 | private array $options;
26 |
27 | /**
28 | * logger is only meant to be used for development purposes. Enabling
29 | * an actual logger in production could cause a massive amount of data
30 | * that will flood the logs on failure.
31 | */
32 | private LoggerInterface $logger;
33 |
34 | private SpanSerializer $serializer;
35 |
36 | /**
37 | * @param array $options the options for HTTP call:
38 | *
39 | *
40 | * $options = [
41 | * 'endpoint_url' => 'http://myzipkin:9411/api/v2/spans', // the reporting url for zipkin server
42 | * 'headers' => ['X-API-Key' => 'abc123'] // the additional headers to be included in the request
43 | * 'timeout' => 10, // the timeout for the request in seconds
44 | * ];
45 | *
46 | *
47 | * @param ClientFactory $requesterFactory the factory for the client
48 | * that will do the HTTP call
49 | * @param LoggerInterface $logger the logger for output
50 | * @param SpanSerializer $serializer
51 | */
52 | public function __construct(
53 | array $options = [],
54 | ?ClientFactory $requesterFactory = null,
55 | ?LoggerInterface $logger = null,
56 | ?SpanSerializer $serializer = null
57 | ) {
58 | $this->options = \array_merge(self::DEFAULT_OPTIONS, $options);
59 | $this->clientFactory = $requesterFactory ?? CurlFactory::create();
60 | $this->logger = $logger ?? new NullLogger();
61 | $this->serializer = $serializer ?? new JsonV2Serializer();
62 | }
63 |
64 | /**
65 | * @param ReadbackSpan[] $spans
66 | * @return void
67 | */
68 | public function report(array $spans): void
69 | {
70 | if (\count($spans) === 0) {
71 | return;
72 | }
73 |
74 | $payload = $this->serializer->serialize($spans);
75 | if ($payload === false) {
76 | $this->logger->error(
77 | \sprintf('failed to encode spans with code %d', \json_last_error())
78 | );
79 | return;
80 | }
81 |
82 | $client = $this->clientFactory->build($this->options);
83 | try {
84 | $client($payload);
85 | } catch (RuntimeException $e) {
86 | $this->logger->error(\sprintf('failed to report spans: %s', $e->getMessage()));
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Zipkin/Propagation/ExtraField.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | private array $keyToName;
17 |
18 | /**
19 | * @var array
Used to filter against an http route.
18 | */ 19 | const HTTP_METHOD = 'http.method'; 20 | 21 | /** 22 | * The absolute http path, without any query parameters. Ex. "/objects/abcd-ff" 23 | * 24 | *Used as a filter or to clarify the request path for a given route. For example, the path for 25 | * a route "/objects/:objectId" could be "/objects/abdc-ff". This does not limit cardinality like 26 | * {@link #HTTP_ROUTE} can, so is not a good input to a span name.
27 | * 28 | *The Zipkin query api only supports equals filters. Dropping query parameters makes the 29 | * number of distinct URIs less. For example, one can query for the same resource, regardless of 30 | * signing parameters encoded in the query line. Dropping query parameters also limits the 31 | * security impact of this tag.
32 | * 33 | *Historical note: This was commonly expressed as "http.uri" in zipkin, even though it was most
34 | */ 35 | const HTTP_PATH = 'http.path'; 36 | 37 | /** 38 | * The route which a request matched or "" (empty string) if routing is supported, but there was 39 | * no match. Ex "/objects/{objectId}" 40 | * 41 | *Often used as a span name when known, with empty routes coercing to "not_found" or 42 | * "redirected" based on {@link #HTTP_STATUS_CODE}.
43 | * 44 | *Unlike {@link #HTTP_PATH}, this value is fixed cardinality, so is a safe input to a span 45 | * name function or a metrics dimension. Different formats are possible. For example, the 46 | * following are all valid route templates: "/objects" "/objects/:objectId" "/objects/*"
47 | */ 48 | const HTTP_ROUTE = 'http.route'; 49 | 50 | /** 51 | * The entire URL, including the scheme, host and query parameters if available. Ex. 52 | * "https://mybucket.s3.amazonaws.com/objects/abcd-ff?X-Amz-Algorithm=AWS4-HMAC-SHA256..." 53 | * 54 | *Combined with {@linkplain #HTTP_METHOD}, you can understand the fully-qualified request
55 | * line. 56 | * 57 | *This is optional as it may include private data or be of considerable length.
58 | */ 59 | const HTTP_URL = 'http.url'; 60 | 61 | /** 62 | * The HTTP status code, when not in 2xx range. Ex. "503" 63 | * 64 | *Used to filter for error status.
65 | */ 66 | const HTTP_STATUS_CODE = 'http.status_code'; 67 | 68 | /** 69 | * The size of the non-empty HTTP request body, in bytes. Ex. "16384" 70 | * 71 | *Large uploads can exceed limits or contribute directly to latency.
72 | */ 73 | const HTTP_REQUEST_SIZE = 'http.request.size'; 74 | 75 | /** 76 | * The size of the non-empty HTTP response body, in bytes. Ex. "16384" 77 | * 78 | *Large downloads can exceed limits or contribute directly to latency.
79 | */ 80 | const HTTP_RESPONSE_SIZE = 'http.response.size'; 81 | 82 | /** 83 | * The query executed for SQL call. Ex. "select * from customers where id = ?" 84 | * 85 | *Used to understand the complexity of a request
86 | */ 87 | const SQL_QUERY = 'sql.query'; 88 | 89 | /** 90 | * The value of "lc" is the component or namespace of a local span. 91 | */ 92 | const LOCAL_COMPONENT = 'lc'; 93 | 94 | const ERROR = 'error'; 95 | -------------------------------------------------------------------------------- /src/Zipkin/Propagation/Propagation.php: -------------------------------------------------------------------------------- 1 | Propagation example: Http 13 | * 14 | * When using http, the carrier of propagated data on both the client (injector) and server 15 | * (extractor) side is usually an http request. Propagation is usually implemented via library- 16 | * specific request interceptors, where the client-side injects span identifiers and the server-side 17 | * extracts them. 18 | */ 19 | interface Propagation 20 | { 21 | /** 22 | * The propagation fields defined. If your carrier is reused, you should delete the fields here 23 | * before calling {@link Setter#put(object, string, string)}. 24 | * 25 | *For example, if the carrier is a single-use or immutable request object, you don't need to
26 | * clear fields as they could not have been set before. If it is a mutable, retryable object,
27 | * successive calls should clear these fields first.
28 | *
29 | * @return array|string[]
30 | */
31 | // The use cases of this are:
32 | // * allow pre-allocation of fields, especially in systems like gRPC Metadata
33 | // * allow a single-pass over an iterator (ex OpenTracing has no getter in TextMap)
34 | public function getKeys(): array;
35 |
36 | /**
37 | * Returns a injector as a callable having the signature function(TraceContext $context, &$carrier): void
38 | *
39 | * The injector replaces a propagated field with the given value so it is very important the carrier is
40 | * being passed by reference.
41 | *
42 | * @param Setter $setter invoked for each propagation key to add.
43 | * @return callable(TraceContext,mixed):void
44 | */
45 | public function getInjector(Setter $setter): callable;
46 |
47 | /**
48 | * Returns the extractor as a callable having the signature function($carrier): TraceContext|SamplingFlags
49 | * - return SamplingFlags being empty if the context does not hold traceId, not debug nor sampling decision
50 | * - return SamplingFlags if the context does not contain a spanId.
51 | * - return TraceContext if the context contains a traceId and a spanId.
52 | *
53 | * @param Getter $getter invoked for each propagation key to get.
54 | * @return callable(mixed):SamplingFlags
55 | */
56 | public function getExtractor(Getter $getter): callable;
57 |
58 | /**
59 | * Does the propagation implementation support sharing client and server span IDs. For example,
60 | * should an RPC server span share the same identifiers extracted from an incoming request?
61 | *
62 | * In usual B3 Propagation, the
63 | * parent span ID is sent across the wire so that the client and server can share the same
64 | * identifiers. Other propagation formats, like trace-context
65 | * only propagate the calling trace and span ID, with an assumption that the receiver always
66 | * starts a new child span. When join is supported, you can assume that when {@link
67 | * TraceContex::getParentId() the parent span ID} is null, you've been propagated a root span. When
68 | * join is not supported, you must always fork a new child.
69 | */
70 | public function supportsJoin(): bool;
71 | }
72 |
--------------------------------------------------------------------------------
/src/Zipkin/Instrumentation/Http/Server/Psr15/Middleware.php:
--------------------------------------------------------------------------------
1 | tracer = $tracing->getTracing()->getTracer();
43 | $this->extractor = $tracing->getTracing()->getPropagation()->getExtractor(new RequestHeaders());
44 | $this->parser = $tracing->getParser();
45 | $this->requestSampler = $tracing->getRequestSampler();
46 | }
47 |
48 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
49 | {
50 | $extractedContext = ($this->extractor)($request);
51 |
52 | $span = $this->nextSpan($extractedContext, $request);
53 | $scopeCloser = $this->tracer->openScope($span);
54 |
55 | if ($span->isNoop()) {
56 | try {
57 | return $handler->handle($request);
58 | } finally {
59 | $span->finish();
60 | $scopeCloser();
61 | }
62 | }
63 |
64 | $parsedRequest = new Request($request);
65 |
66 | $span->setKind(Kind\SERVER);
67 | $spanCustomizer = new SpanCustomizerShield($span);
68 | $this->parser->request($parsedRequest, $span->getContext(), $spanCustomizer);
69 |
70 | try {
71 | $response = $handler->handle($request);
72 | $this->parser->response(new Response($response, $parsedRequest), $span->getContext(), $spanCustomizer);
73 | return $response;
74 | } catch (Throwable $e) {
75 | $span->setError($e);
76 | throw $e;
77 | } finally {
78 | $span->finish();
79 | $scopeCloser();
80 | }
81 | }
82 |
83 | private function nextSpan(?SamplingFlags $extractedContext, ServerRequestInterface $request): Span
84 | {
85 | if ($extractedContext instanceof TraceContext) {
86 | return $this->tracer->joinSpan($extractedContext);
87 | }
88 |
89 | $extractedContext ??= DefaultSamplingFlags::createAsEmpty();
90 | if ($this->requestSampler === null) {
91 | return $this->tracer->nextSpan($extractedContext);
92 | }
93 |
94 | return $this->tracer->nextSpanWithSampler(
95 | $this->requestSampler,
96 | [$request],
97 | $extractedContext
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/Unit/RealSpanTest.php:
--------------------------------------------------------------------------------
1 | prophesize(Recorder::class);
27 | $span = new RealSpan($context, $recorder->reveal());
28 | $this->assertEquals($context, $span->getContext());
29 | }
30 |
31 | public function testStartSuccess()
32 | {
33 | $context = TraceContext::createAsRoot();
34 | $recorder = $this->prophesize(Recorder::class);
35 | $recorder->start($context, self::TEST_START_TIMESTAMP)->shouldBeCalled();
36 | $span = new RealSpan($context, $recorder->reveal());
37 | $span->start(self::TEST_START_TIMESTAMP);
38 | }
39 |
40 | public function testSetNameSuccess()
41 | {
42 | $context = TraceContext::createAsRoot();
43 | $recorder = $this->prophesize(Recorder::class);
44 | $recorder->setName($context, self::TEST_NAME)->shouldBeCalled();
45 | $span = new RealSpan($context, $recorder->reveal());
46 | $span->setName(self::TEST_NAME);
47 | }
48 |
49 | public function testSetKindSuccess()
50 | {
51 | $context = TraceContext::createAsRoot();
52 | $recorder = $this->prophesize(Recorder::class);
53 | $recorder->setKind($context, self::TEST_KIND)->shouldBeCalled();
54 | $span = new RealSpan($context, $recorder->reveal());
55 | $span->setKind(self::TEST_KIND);
56 | }
57 |
58 | public function testSetRemoteEndpointSuccess()
59 | {
60 | $context = TraceContext::createAsRoot();
61 | $remoteEndpoint = Endpoint::createAsEmpty();
62 | $recorder = $this->prophesize(Recorder::class);
63 | $recorder->setRemoteEndpoint($context, $remoteEndpoint)->shouldBeCalled();
64 | $span = new RealSpan($context, $recorder->reveal());
65 | $span->setRemoteEndpoint($remoteEndpoint);
66 | }
67 |
68 | public function testAnnotateSuccess()
69 | {
70 | $timestamp = now();
71 | $value = WIRE_SEND;
72 | $context = TraceContext::createAsRoot();
73 | $recorder = $this->prophesize(Recorder::class);
74 | $recorder->annotate($context, $timestamp, $value)->shouldBeCalled();
75 | $span = new RealSpan($context, $recorder->reveal());
76 | $span->annotate($value, $timestamp);
77 | }
78 |
79 | public function testAnnotateFailsDueToInvalidTimestamp()
80 | {
81 | $this->expectException(InvalidArgumentException::class);
82 | $timestamp = -1;
83 | $value = WIRE_SEND;
84 | $context = TraceContext::createAsRoot();
85 | $recorder = $this->prophesize(Recorder::class);
86 | $recorder->annotate($context, $timestamp, $value)->shouldNotBeCalled();
87 | $span = new RealSpan($context, $recorder->reveal());
88 | $this->expectException(InvalidArgumentException::class);
89 | $span->annotate($value, $timestamp);
90 | }
91 |
92 | public function testStartRealSpanFailsDueToInvalidTimestamp()
93 | {
94 | $this->expectException(InvalidArgumentException::class);
95 | $context = TraceContext::createAsRoot();
96 | $recorder = $this->prophesize(Recorder::class);
97 | $span = new RealSpan($context, $recorder->reveal());
98 | $span->start(-1);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Zipkin/Span.php:
--------------------------------------------------------------------------------
1 | endpoint = $endpoint;
28 | $this->reporter = $reporter;
29 | $this->noop = $isNoop;
30 | $this->spanMap = new SpanMap();
31 | }
32 |
33 | public static function createAsNoop(): self
34 | {
35 | return new self(Endpoint::createAsEmpty(), new Noop(), true);
36 | }
37 |
38 | public function getTimestamp(TraceContext $context): ?int
39 | {
40 | $span = $this->spanMap->get($context);
41 |
42 | if ($span !== null && $span->getTimestamp() !== null) {
43 | return $span->getTimestamp();
44 | }
45 |
46 | return null;
47 | }
48 |
49 | public function start(TraceContext $context, int $timestamp): void
50 | {
51 | $span = $this->spanMap->getOrCreate($context, $this->endpoint);
52 | $span->start($timestamp);
53 | }
54 |
55 | public function setName(TraceContext $context, string $name): void
56 | {
57 | if ($this->noop) {
58 | return;
59 | }
60 |
61 | $span = $this->spanMap->getOrCreate($context, $this->endpoint);
62 | $span->setName($name);
63 | }
64 |
65 | public function setKind(TraceContext $context, string $kind): void
66 | {
67 | if ($this->noop) {
68 | return;
69 | }
70 |
71 | $span = $this->spanMap->getOrCreate($context, $this->endpoint);
72 | $span->setKind($kind);
73 | }
74 |
75 | /**
76 | * @param TraceContext $context
77 | * @param int $timestamp
78 | * @param string $value
79 | * @throws \InvalidArgumentException
80 | * @return void
81 | */
82 | public function annotate(TraceContext $context, int $timestamp, string $value): void
83 | {
84 | if ($this->noop) {
85 | return;
86 | }
87 |
88 | $span = $this->spanMap->getOrCreate($context, $this->endpoint);
89 | $span->annotate($timestamp, $value);
90 | }
91 |
92 | public function tag(TraceContext $context, string $key, string $value): void
93 | {
94 | if ($this->noop) {
95 | return;
96 | }
97 |
98 | $span = $this->spanMap->getOrCreate($context, $this->endpoint);
99 | $span->tag($key, $value);
100 | }
101 |
102 | public function setError(TraceContext $context, Throwable $e): void
103 | {
104 | if ($this->noop) {
105 | return;
106 | }
107 |
108 | $span = $this->spanMap->getOrCreate($context, $this->endpoint);
109 | $span->setError($e);
110 | }
111 |
112 | public function setRemoteEndpoint(TraceContext $context, Endpoint $remoteEndpoint): void
113 | {
114 | if ($this->noop) {
115 | return;
116 | }
117 |
118 | $span = $this->spanMap->getOrCreate($context, $this->endpoint);
119 | $span->setRemoteEndpoint($remoteEndpoint);
120 | }
121 |
122 | public function finish(TraceContext $context, int $finishTimestamp): void
123 | {
124 | $span = $this->spanMap->get($context);
125 |
126 | if ($span !== null) {
127 | $span->finish($finishTimestamp);
128 | }
129 | }
130 |
131 | public function abandon(TraceContext $context): void
132 | {
133 | $this->spanMap->remove($context);
134 | }
135 |
136 | public function flush(TraceContext $context): void
137 | {
138 | $span = $this->spanMap->remove($context);
139 |
140 | if ($span !== null && !$this->noop) {
141 | $span->finish();
142 | $this->reporter->report([$span]);
143 | }
144 | }
145 |
146 | public function flushAll(): void
147 | {
148 | $this->reporter->report($this->spanMap->removeAll());
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/Zipkin/NoopSpan.php:
--------------------------------------------------------------------------------
1 | context = $context;
17 | }
18 |
19 | /**
20 | * When true, no recording is done and nothing is reported to zipkin. However, this span should
21 | * still be injected into outgoing requests. Use this flag to avoid performing expensive
22 | * computation.
23 | */
24 | public function isNoop(): bool
25 | {
26 | return true;
27 | }
28 |
29 | public function getContext(): TraceContext
30 | {
31 | return $this->context;
32 | }
33 |
34 | /**
35 | * Starts the span with an implicit timestamp.
36 | *
37 | * Spans can be modified before calling start. For example, you can add tags to the span and
38 | * set its name without lock contention.
39 | */
40 | public function start(?int $timestamp = null): void
41 | {
42 | }
43 |
44 | /**
45 | * Sets the string name for the logical operation this span represents.
46 | */
47 | public function setName(string $name): void
48 | {
49 | }
50 |
51 | /**
52 | * The kind of span is optional. When set, it affects how a span is reported. For example, if the
53 | * kind is {@link Zipkin\Kind\SERVER}, the span's start timestamp is implicitly annotated as "sr"
54 | * and that plus its duration as "ss".
55 | */
56 | public function setKind(string $kind): void
57 | {
58 | }
59 |
60 | /**
61 | * Tags give your span context for search, viewing and analysis. For example, a key
62 | * "your_app.version" would let you lookup spans by version. A tag {@link Zipkin\Tags\SQL_QUERY}
63 | * isn't searchable, but it can help in debugging when viewing a trace.
64 | *
65 | * @param string $key Name used to lookup spans, such as "your_app.version". See {@link Zipkin\Tags} for
66 | * standard ones.
67 | * @param string $value
68 | * @return void
69 | */
70 | public function tag(string $key, string $value): void
71 | {
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | public function setError(Throwable $e): void
78 | {
79 | }
80 |
81 |
82 | /**
83 | * Associates an event that explains latency with the current system time.
84 | *
85 | * @param string $value A short tag indicating the event, like "finagle.retry"
86 | * @param int $timestamp
87 | * @return void
88 | * @see Annotations
89 | */
90 | public function annotate(string $value, ?int $timestamp = null): void
91 | {
92 | }
93 |
94 | /**
95 | * For a client span, this would be the server's address.
96 | *
97 | * It is often expensive to derive a remote address: always check {@link #isNoop()} first!
98 | */
99 | public function setRemoteEndpoint(Endpoint $remoteEndpoint): void
100 | {
101 | }
102 |
103 | /**
104 | * Throws away the current span without reporting it.
105 | */
106 | public function abandon(): void
107 | {
108 | }
109 |
110 | /**
111 | * Like {@link #finish()}, except with a given timestamp in microseconds.
112 | *
113 | * {@link zipkin.Span#duration Zipkin's span duration} is derived by subtracting the start
114 | * timestamp from this, and set when appropriate.
115 | */
116 | public function finish(?int $timestamp = null): void
117 | {
118 | }
119 |
120 | /**
121 | * Reports the span, even if unfinished. Most users will not call this method.
122 | *
123 | * This primarily supports two use cases: one-way spans and orphaned spans.
124 | * For example, a one-way span can be modeled as a span where one tracer calls start and another
125 | * calls finish. In order to report that span from its origin, flush must be called.
126 | *
127 | * Another example is where a user didn't call finish within a deadline or before a shutdown
128 | * occurs. By flushing, you can report what was in progress.
129 | */
130 | public function flush(): void
131 | {
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/tests/Unit/Reporters/JsonV2SerializerTest.php:
--------------------------------------------------------------------------------
1 | start($startTime);
20 | $span->setName('Test');
21 | $span->setKind('CLIENT');
22 | $remoteEndpoint = Endpoint::create('SERVICE2', null, '2001:0db8:85a3:0000:0000:8a2e:0370:7334', 3302);
23 | $span->setRemoteEndpoint($remoteEndpoint);
24 | $span->tag('test_key', 'test_value');
25 | $span->annotate($startTime + 100, 'test_annotation');
26 | $span->setError(new \RuntimeException('test_error'));
27 | $span->finish($startTime + 1000);
28 | $serializer = new JsonV2Serializer();
29 | $serializedSpans = $serializer->serialize([$span]);
30 |
31 | $expectedSerialization = '[{'
32 | . '"id":"186f11b67460db4d","traceId":"186f11b67460db4d","timestamp":1594044779509687,"name":"test",'
33 | . '"duration":1000,"localEndpoint":{"serviceName":"service1","ipv4":"192.168.0.11","port":3301},'
34 | . '"kind":"CLIENT",'
35 | . '"remoteEndpoint":{"serviceName":"service2","ipv6":"2001:0db8:85a3:0000:0000:8a2e:0370:7334","port":3302}'
36 | . ',"annotations":[{"value":"test_annotation","timestamp":1594044779509787}],'
37 | . '"tags":{"test_key":"test_value","error":"test_error"}'
38 | . '}]';
39 | $this->assertEquals($expectedSerialization, $serializedSpans);
40 | }
41 |
42 | public function testErrorTagIsNotClobberedBySpanError()
43 | {
44 | $context = TraceContext::create('186f11b67460db4d', '186f11b67460db4d');
45 | $localEndpoint = Endpoint::create('service1', '192.168.0.11', null, 3301);
46 | $span = Span::createFromContext($context, $localEndpoint);
47 | $startTime = 1594044779509688;
48 | $span->start($startTime);
49 | $span->setName('test');
50 | $span->tag('test_key', 'test_value');
51 | $span->tag('error', 'priority_error');
52 | $span->setError(new \RuntimeException('test_error'));
53 | $span->finish($startTime + 1000);
54 | $serializer = new JsonV2Serializer();
55 | $serializedSpans = $serializer->serialize([$span]);
56 |
57 | $expectedSerialization = '[{'
58 | . '"id":"186f11b67460db4d","traceId":"186f11b67460db4d","timestamp":1594044779509688,"name":"test",'
59 | . '"duration":1000,"localEndpoint":{"serviceName":"service1","ipv4":"192.168.0.11","port":3301},'
60 | . '"tags":{"test_key":"test_value","error":"priority_error"}'
61 | . '}]';
62 | $this->assertEquals($expectedSerialization, $serializedSpans);
63 | }
64 |
65 | public function testStringValuesAreEscapedAndSerializedCorrectly()
66 | {
67 | $jsonValue = '{"name":"Kurt"}';
68 | $mutilineValue = << For example, here's how to batch send spans via http:
70 | *
71 | * See https://github.com/openzipkin/zipkin-reporter-java
78 | *
79 | * @param Reporter $reporter
80 | */
81 | public function havingReporter(Reporter $reporter): self
82 | {
83 | $this->reporter = $reporter;
84 | return $this;
85 | }
86 |
87 | /**
88 | * Sampler is responsible for deciding if a particular trace should be "sampled", i.e. whether
89 | * the overhead of tracing will occur and/or if a trace will be reported to Zipkin.
90 | *
91 | * @param Sampler $sampler
92 | */
93 | public function havingSampler(Sampler $sampler): self
94 | {
95 | $this->sampler = $sampler;
96 | return $this;
97 | }
98 |
99 | /**
100 | * When true, new root spans will have 128-bit trace IDs. Defaults to false (64-bit)
101 | *
102 | * @param bool $usesTraceId128bits
103 | */
104 | public function havingTraceId128bits(bool $usesTraceId128bits): self
105 | {
106 | $this->usesTraceId128bits = $usesTraceId128bits;
107 | return $this;
108 | }
109 |
110 | /**
111 | * @param CurrentTraceContext $currentTraceContext
112 | */
113 | public function havingCurrentTraceContext(CurrentTraceContext $currentTraceContext): self
114 | {
115 | $this->currentTraceContext = $currentTraceContext;
116 | return $this;
117 | }
118 |
119 | /**
120 | * Set true to drop data and only return {@link Span#isNoop() noop spans} regardless of sampling
121 | * policy. This allows operators to stop tracing in risk scenarios.
122 | *
123 | * @param bool $isNoop
124 | */
125 | public function beingNoop(bool $isNoop = true): self
126 | {
127 | $this->isNoop = $isNoop;
128 | return $this;
129 | }
130 |
131 | public function havingPropagation(Propagation $propagation): self
132 | {
133 | $this->propagation = $propagation;
134 | return $this;
135 | }
136 |
137 | /**
138 | * True means the tracing system supports sharing a span ID between a {@link Span\Kind\CLIENT}
139 | * and {@link Span\Kind\SERVER} span. Defaults to true.
140 | *
141 | * Set this to false when the tracing system requires the opposite. For example, if
142 | * ultimately spans are sent to Amazon X-Ray or Google Stackdriver Trace, you should set this to
143 | * false.
144 | *
145 | * This is implicitly set to false when {@link Propagation::supportsJoin()} is false,
146 | * as in that case, sharing IDs isn't possible anyway.
147 | */
148 | public function supportingJoin(bool $supportsJoin): self
149 | {
150 | $this->supportsJoin = $supportsJoin;
151 | return $this;
152 | }
153 |
154 | /**
155 | * True means that spans will always be recorded, even if the current trace is not sampled.
156 | * Defaults to False.
157 | *
158 | * This has the side effect that your reporter will receive all spans, irrespective of the
159 | * sampling decision. Use this if you want to have some custom smart logic in the reporter
160 | * that needs to have access to both sampled and unsampled traces.
161 | *
162 | * @param bool $alwaysReportSpans
163 | * @return $this
164 | */
165 | public function alwaysReportingSpans(bool $alwaysReportSpans): self
166 | {
167 | $this->alwaysReportSpans = $alwaysReportSpans;
168 | return $this;
169 | }
170 |
171 | /**
172 | * @return DefaultTracing
173 | */
174 | public function build(): Tracing
175 | {
176 | $localEndpoint = $this->localEndpoint;
177 | if ($this->localEndpoint === null) {
178 | $localEndpoint = Endpoint::createFromGlobals()->withServiceName($this->localServiceName);
179 | }
180 |
181 | $reporter = $this->reporter ?? new Log(new NullLogger());
182 | $sampler = $this->sampler ?? BinarySampler::createAsNeverSample();
183 | $propagation = $this->propagation ?? new B3();
184 | $currentTraceContext = $this->currentTraceContext ?: new CurrentTraceContext();
185 |
186 | return new DefaultTracing(
187 | $localEndpoint,
188 | $reporter,
189 | $sampler,
190 | $this->usesTraceId128bits,
191 | $currentTraceContext,
192 | $this->isNoop,
193 | $propagation,
194 | $this->supportsJoin && $propagation->supportsJoin(),
195 | $this->alwaysReportSpans
196 | );
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
{@code
72 | * reporter = AsyncReporter.v2(URLConnectionSender.json("http://localhost:9411/api/v2/spans"));
73 | *
74 | * tracingBuilder.spanReporter(reporter);
75 | * }
76 | *
77 | *