├── .coveralls.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── codecov.yml ├── composer.json ├── phpstan.neon ├── phpunit.xml ├── src └── Zipkin │ ├── Annotations.php │ ├── DefaultErrorParser.php │ ├── DefaultTracing.php │ ├── Endpoint.php │ ├── ErrorParser.php │ ├── Instrumentation │ ├── Http │ │ ├── Client │ │ │ ├── DefaultHttpClientParser.php │ │ │ ├── HttpClientParser.php │ │ │ ├── HttpClientTracing.php │ │ │ ├── Psr18 │ │ │ │ ├── Client.php │ │ │ │ ├── Propagation │ │ │ │ │ └── RequestHeaders.php │ │ │ │ ├── README.md │ │ │ │ ├── Request.php │ │ │ │ └── Response.php │ │ │ ├── Request.php │ │ │ └── Response.php │ │ ├── Request.php │ │ ├── Response.php │ │ └── Server │ │ │ ├── DefaultHttpServerParser.php │ │ │ ├── HttpServerParser.php │ │ │ ├── HttpServerTracing.php │ │ │ ├── Psr15 │ │ │ ├── Middleware.php │ │ │ ├── Propagation │ │ │ │ └── RequestHeaders.php │ │ │ ├── README.md │ │ │ ├── Request.php │ │ │ └── Response.php │ │ │ ├── Request.php │ │ │ └── Response.php │ ├── Mysqli │ │ └── Mysqli.php │ └── README.md │ ├── Kind.php │ ├── NoopSpan.php │ ├── NoopSpanCustomizer.php │ ├── Propagation │ ├── B3.php │ ├── CurrentTraceContext.php │ ├── DefaultSamplingFlags.php │ ├── Exceptions │ │ ├── InvalidPropagationCarrier.php │ │ ├── InvalidPropagationKey.php │ │ └── InvalidTraceContextArgument.php │ ├── ExtraField.php │ ├── Getter.php │ ├── Id.php │ ├── Map.php │ ├── Propagation.php │ ├── RemoteSetter.php │ ├── RequestHeaders.php │ ├── SamplingFlags.php │ ├── ServerHeaders.php │ ├── Setter.php │ └── TraceContext.php │ ├── RealSpan.php │ ├── Recorder.php │ ├── Recording │ ├── ReadbackSpan.php │ ├── Span.php │ └── SpanMap.php │ ├── Reporter.php │ ├── Reporters │ ├── Http.php │ ├── Http │ │ ├── ClientFactory.php │ │ └── CurlFactory.php │ ├── InMemory.php │ ├── JsonV2Serializer.php │ ├── Log.php │ ├── Log │ │ └── LogSerializer.php │ ├── Noop.php │ └── SpanSerializer.php │ ├── Sampler.php │ ├── Samplers │ ├── BinarySampler.php │ └── PercentageSampler.php │ ├── Span.php │ ├── SpanCustomizer.php │ ├── SpanCustomizerShield.php │ ├── SpanName.php │ ├── Tags.php │ ├── Timestamp.php │ ├── Tracer.php │ ├── Tracing.php │ └── TracingBuilder.php └── tests ├── Integration ├── Instrumentation │ ├── Http │ │ └── Server │ │ │ └── MiddlewareTest.php │ └── Mysqli │ │ ├── MysqliTest.php │ │ ├── access.cnf │ │ └── docker-compose.yaml └── Reporters │ └── Http │ └── CurlFactoryTest.php └── Unit ├── DefaultErrorParserException.php ├── DefaultErrorParserTest.php ├── DefaultTracingTest.php ├── EndpointTest.php ├── InSpan ├── Callables.php └── Sumer.php ├── Instrumentation └── Http │ ├── Client │ ├── BaseRequestTestCase.php │ ├── BaseResponseTestCase.php │ └── Psr18 │ │ ├── ClientTest.php │ │ ├── RequestTestCase.php │ │ └── ResponseTestCase.php │ └── Server │ ├── BaseRequestTestCase.php │ ├── BaseResponseTestCase.php │ └── Psr15 │ ├── MiddlewareTest.php │ ├── RequestTestCase.php │ └── ResponseTestCase.php ├── NoopSpanTest.php ├── Propagation ├── B3Test.php ├── CurrentTraceContextTest.php ├── DefaultSamplingFlagsTest.php ├── ExtraFieldTest.php ├── IdTest.php ├── MapTest.php ├── RequestHeadersTest.php ├── ServerHeadersTest.php └── TraceContextTest.php ├── RealSpanTest.php ├── RecorderTest.php ├── Recording ├── SpanMapTest.php └── SpanTest.php ├── Reporters ├── HttpMockFactory.php ├── HttpTest.php ├── InMemoryTest.php ├── JsonV2SerializerTest.php ├── Log │ └── LogSerializerTest.php ├── LogTest.php └── NoopTest.php ├── Samplers └── BinarySamplerTest.php ├── SpanCustomizerShieldSpan.php ├── SpanCustomizerShieldTest.php ├── TimestampTest.php ├── TracerTest.php └── TracingBuilderTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: github-actions 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - "**/*.md" 8 | - "LICENSE" 9 | pull_request: 10 | jobs: 11 | test: 12 | name: Zipkin PHP (PHP version ${{ matrix.php-versions }} on ${{ matrix.operating-system }}) 13 | runs-on: ${{ matrix.operating-system }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | operating-system: [ubuntu-latest, windows-latest, macos-latest] 18 | php-versions: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | - name: Setup PHP, with composer and extensions 23 | uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php 24 | with: 25 | php-version: ${{ matrix.php-versions }} 26 | coverage: xdebug #optional 27 | extensions: mysql 28 | - name: Get composer cache directory 29 | id: composer-cache 30 | run: echo "dir=$(composer config cache-files-dir | tr -d '\n' | tr -d '\r')" >> $GITHUB_OUTPUT 31 | - name: Cache composer dependencies 32 | uses: actions/cache@v3 33 | with: 34 | path: ${{ steps.composer-cache.outputs.dir }} 35 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 36 | restore-keys: ${{ runner.os }}-composer- 37 | - name: Install Composer dependencies 38 | run: | 39 | composer install --no-progress --prefer-dist --optimize-autoloader 40 | - name: Run lint 41 | if: matrix.operating-system != 'windows-latest' 42 | run: composer lint 43 | - name: Run static check 44 | run: composer static-check 45 | - name: Run tests 46 | run: composer test -- --coverage-clover=build/logs/clover.xml 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | /vendor/ 4 | .phpunit.result.cache 5 | .php-cs-fixer.cache 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.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 | ; -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: on 4 | patch: off 5 | 6 | comment: true -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | checkGenericClassInNonGenericObjectType: false 3 | treatPhpDocTypesAsCertain: false 4 | reportUnmatchedIgnoredErrors: false 5 | bootstrapFiles: 6 | - vendor/autoload.php 7 | checkMissingIterableValueType: false 8 | ignoreErrors: 9 | - 10 | # if openssl_random_pseudo_bytes we want to fail 11 | message: '#Parameter \#1 \$data of function bin2hex expects string, string\|false given#' 12 | path: src/Zipkin/Propagation/Id.php 13 | - 14 | # This is probably a mistake in the logic of PHPStan as $localEndpoint is always being overrided 15 | message: '#Parameter \#1 \$localEndpoint of class Zipkin\\DefaultTracing constructor expects Zipkin\\Endpoint, Zipkin\\Endpoint\|null given#' 16 | path: src/Zipkin/TracingBuilder.php 17 | - 18 | # This avoids false positive in quirky HTTP reporter constructor 19 | message: '#Zipkin\\Reporters\\Http\:\:\_\_construct\(\)#' 20 | path: src/Zipkin/Reporters/Http.php 21 | - 22 | # This avoids false positive in quirky HTTP reporter constructor 23 | message: '#Strict comparison using \=\=\=#' 24 | path: src/Zipkin/Reporters/Http.php 25 | - 26 | # This avoids false positive for parameter type mismatch 27 | message: '#Parameter \#2 ...\$arrays of function array_merge expects array, array|Zipkin\\Reporters\\Http\\ClientFactory given#' 28 | path: src/Zipkin/Reporters/Http.php 29 | - 30 | # If we specify a type for $fn the it is impossible to justify the casting to string or array 31 | message: '#Function Zipkin\\SpanName\\generateSpanName\(\) has parameter \$fn with no typehint specified.#' 32 | path: src/Zipkin/SpanName.php 33 | - 34 | # In general types are desired but carrier is an special case 35 | message: '#has parameter \$carrier with no typehint specified#' 36 | path: src/Zipkin/* 37 | - 38 | # SpanCustomizer is definitively not null 39 | message: '#Parameter \#3 \$ of callable callable\(.*, Zipkin\\Propagation\\TraceContext, Zipkin\\SpanCustomizer\): void expects Zipkin\\SpanCustomizer, Zipkin\\SpanCustomizerShield\|null given.#' 40 | path: src/Zipkin/Tracer 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 24 | 25 | 26 | ./src 27 | 28 | 29 | ./vendor 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Zipkin/Annotations.php: -------------------------------------------------------------------------------- 1 | $e->getMessage()]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Zipkin/DefaultTracing.php: -------------------------------------------------------------------------------- 1 | tracer = new Tracer( 32 | $localEndpoint, 33 | $reporter, 34 | $sampler, 35 | $usesTraceId128bits, 36 | $currentTraceContext, 37 | $isNoop, 38 | $supportsJoin, 39 | $alwaysReportSpans 40 | ); 41 | 42 | $this->propagation = $propagation; 43 | $this->isNoop = $isNoop; 44 | } 45 | 46 | public function getTracer(): Tracer 47 | { 48 | return $this->tracer; 49 | } 50 | 51 | public function getPropagation(): Propagation 52 | { 53 | return $this->propagation; 54 | } 55 | 56 | /** 57 | * When true, no recording is done and nothing is reported to zipkin. However, trace context is 58 | * still injected into outgoing requests. 59 | * 60 | * @see Span#isNoop() 61 | */ 62 | public function isNoop(): bool 63 | { 64 | return $this->isNoop; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Zipkin/Endpoint.php: -------------------------------------------------------------------------------- 1 | serviceName = $serviceName; 29 | $this->ipv4 = $ipv4; 30 | $this->ipv6 = $ipv6; 31 | $this->port = $port; 32 | } 33 | 34 | public static function create( 35 | string $serviceName, 36 | ?string $ipv4 = null, 37 | ?string $ipv6 = null, 38 | ?int $port = null 39 | ): self { 40 | if ($ipv4 !== null && \filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) { 41 | throw new InvalidArgumentException( 42 | \sprintf('Invalid IPv4. Expected something in the range 0.0.0.0 and 255.255.255.255, got %s', $ipv4) 43 | ); 44 | } 45 | 46 | if ($ipv6 !== null && \filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false) { 47 | throw new InvalidArgumentException( 48 | \sprintf('Invalid IPv6 %s', $ipv6) 49 | ); 50 | } 51 | 52 | if ($port !== null) { 53 | if ($port > 65535) { 54 | throw new InvalidArgumentException( 55 | \sprintf('Invalid port. Expected a number between 0 and 65535, got %d', $port) 56 | ); 57 | } 58 | } 59 | 60 | return new self($serviceName, $ipv4, $ipv6, $port); 61 | } 62 | 63 | public static function createFromGlobals(): self 64 | { 65 | return new self( 66 | PHP_SAPI, 67 | \array_key_exists('REMOTE_ADDR', $_SERVER) ? $_SERVER['REMOTE_ADDR'] : null, 68 | null, 69 | \array_key_exists('REMOTE_PORT', $_SERVER) ? (int) $_SERVER['REMOTE_PORT'] : null 70 | ); 71 | } 72 | 73 | public static function createAsEmpty(): self 74 | { 75 | return new self('', null, null, null); 76 | } 77 | 78 | public function getServiceName(): string 79 | { 80 | return $this->serviceName; 81 | } 82 | 83 | public function getIpv4(): ?string 84 | { 85 | return $this->ipv4; 86 | } 87 | 88 | public function getIpv6(): ?string 89 | { 90 | return $this->ipv6; 91 | } 92 | 93 | public function getPort(): ?int 94 | { 95 | return $this->port; 96 | } 97 | 98 | public function withServiceName(string $serviceName): Endpoint 99 | { 100 | return new self($serviceName, $this->ipv4, $this->ipv6, $this->port); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Zipkin/ErrorParser.php: -------------------------------------------------------------------------------- 1 | getMethod(); 28 | } 29 | 30 | /** 31 | * {@inhertidoc} 32 | */ 33 | public function request(Request $request, TraceContext $context, SpanCustomizer $span): void 34 | { 35 | $span->setName($this->spanName($request)); 36 | $span->tag(Tags\HTTP_METHOD, $request->getMethod()); 37 | $span->tag(Tags\HTTP_PATH, $request->getPath() ?: "/"); 38 | } 39 | 40 | /** 41 | * {@inhertidoc} 42 | */ 43 | public function response(Response $response, TraceContext $context, SpanCustomizer $span): void 44 | { 45 | $statusCode = $response->getStatusCode(); 46 | if ($statusCode < 200 || $statusCode > 299) { 47 | $span->tag(Tags\HTTP_STATUS_CODE, (string) $statusCode); 48 | } 49 | 50 | if ($statusCode > 399) { 51 | $span->tag(Tags\ERROR, (string) $statusCode); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Client/HttpClientParser.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/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 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Client/Psr18/Propagation/RequestHeaders.php: -------------------------------------------------------------------------------- 1 | havingLocalServiceName('my_service') 25 | ->build(); 26 | 27 | $httpClientTracing = new HttpClientTracing($tracing); 28 | ... 29 | 30 | $httpClient = new ZipkinClient(new Client, $httpClientTracing); 31 | $request = new Request('POST', 'http://myurl.test'); 32 | $response = $httpClient->sendRequest($request); 33 | ``` 34 | -------------------------------------------------------------------------------- /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/Client/Psr18/Response.php: -------------------------------------------------------------------------------- 1 | delegate = $delegate; 22 | $this->request = $request; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function getRequest(): ?ClientRequest 29 | { 30 | return $this->request; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getStatusCode(): int 37 | { 38 | return $this->delegate->getStatusCode(); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function unwrap(): ResponseInterface 45 | { 46 | return $this->delegate; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Client/Request.php: -------------------------------------------------------------------------------- 1 | Conventionally associated with the key "http.method" 22 | * 23 | *

Note

24 | *

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 | *

Implementation notes

39 | * Some HTTP client abstractions, return the input as opposed to 40 | * the absolute path. One common problem is a path requested as "", not "/". When that's the case, 41 | * normalize "" to "/". This ensures values are consistent with wire-level clients and behaviour 42 | * consistent with RFC 7230 Section 2.7.3. 43 | * 44 | * @see HTTP_PATH 45 | */ 46 | abstract public function getPath(): ?string; 47 | 48 | /** 49 | * The entire URL, including the scheme, host and query parameters if available. 50 | * 51 | *

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/Instrumentation/Http/Response.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/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 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Server/HttpServerParser.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/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 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Server/Psr15/Propagation/RequestHeaders.php: -------------------------------------------------------------------------------- 1 | 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/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/Psr15/Response.php: -------------------------------------------------------------------------------- 1 | delegate = $delegate; 22 | $this->request = $request; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function getRequest(): ?ServerRequest 29 | { 30 | return $this->request; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getStatusCode(): int 37 | { 38 | return $this->delegate->getStatusCode(); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function unwrap(): ResponseInterface 45 | { 46 | return $this->delegate; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Server/Request.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 | -------------------------------------------------------------------------------- /src/Zipkin/Instrumentation/Http/Server/Response.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 | -------------------------------------------------------------------------------- /src/Zipkin/Kind.php: -------------------------------------------------------------------------------- 1 | Unlike {@link #CLIENT}, messaging spans never share a span ID. For example, the {@link 17 | * #CONSUMER} of the same message has {@link TraceContext#parentId()} set to this span's {@link 18 | * TraceContext#spanId()}. 19 | */ 20 | const PRODUCER = 'PRODUCER'; 21 | 22 | /** 23 | * When present, {@link Tracer#start()} is the moment a consumer received a message from an 24 | * origin. A duration between {@link Tracer#start()} and {@link Tracer#finish()} may imply a processing backlog. 25 | * while {@link #remoteEndpoint(Endpoint)} indicates the origin, such as a broker. 26 | * 27 | *

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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Zipkin/NoopSpanCustomizer.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 | *

Design

14 | * 15 | * This design was inspired by com.google.instrumentation.trace.ContextUtils, 16 | * com.google.inject.servlet.RequestScoper and com.github.kristofa.brave.CurrentSpan 17 | */ 18 | 19 | final class CurrentTraceContext 20 | { 21 | private ?TraceContext $context; 22 | 23 | public function __construct(?TraceContext $currentContext = null) 24 | { 25 | $this->context = $currentContext; 26 | } 27 | 28 | /** 29 | * Returns the current span context in scope or null if there isn't one. 30 | */ 31 | public function getContext(): ?TraceContext 32 | { 33 | return $this->context; 34 | } 35 | 36 | /** 37 | * Sets the current span in scope until the returned callable is called. It is a programming 38 | * error to drop or never close the result. 39 | * 40 | * @param TraceContext|null $currentContext 41 | * @return callable():void The scope closed 42 | */ 43 | public function createScopeAndRetrieveItsCloser(?TraceContext $currentContext = null): callable 44 | { 45 | $previous = $this->context; 46 | $self = $this; 47 | $this->context = $currentContext; 48 | 49 | return static function () use ($previous, $self): void { 50 | $self->context = $previous; 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Zipkin/Propagation/DefaultSamplingFlags.php: -------------------------------------------------------------------------------- 1 | isSampled = $isDebug ?: $isSampled; 16 | $this->isDebug = $isDebug; 17 | } 18 | 19 | public static function create(?bool $isSampled, bool $isDebug = false): self 20 | { 21 | return new self($isSampled, $isDebug); 22 | } 23 | 24 | public static function createAsEmpty(): self 25 | { 26 | return new self(SamplingFlags::EMPTY_SAMPLED, SamplingFlags::EMPTY_DEBUG); 27 | } 28 | 29 | public static function createAsSampled(): self 30 | { 31 | return new self(true, false); 32 | } 33 | 34 | public static function createAsNotSampled(): self 35 | { 36 | return new self(false, false); 37 | } 38 | 39 | public static function createAsDebug(): self 40 | { 41 | return new self(true, true); 42 | } 43 | 44 | public function isSampled(): ?bool 45 | { 46 | return $this->isSampled; 47 | } 48 | 49 | public function isDebug(): bool 50 | { 51 | return $this->isDebug; 52 | } 53 | 54 | public function isEmpty(): bool 55 | { 56 | return $this->isSampled === self::EMPTY_SAMPLED && 57 | $this->isDebug === self::EMPTY_DEBUG; 58 | } 59 | 60 | public function isEqual(SamplingFlags $samplingFlags): bool 61 | { 62 | return $this->isDebug === $samplingFlags->isDebug() 63 | && $this->isSampled === $samplingFlags->isSampled(); 64 | } 65 | 66 | /** 67 | * @return DefaultSamplingFlags 68 | */ 69 | public function withSampled(bool $isSampled): SamplingFlags 70 | { 71 | return new self($isSampled, false); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Zipkin/Propagation/Exceptions/InvalidPropagationCarrier.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private array $keyToName; 17 | 18 | /** 19 | * @var array 20 | */ 21 | private array $nameToKey; 22 | 23 | /** 24 | * @param Propagation $delegate 25 | * @param array $keyToNameMap 26 | */ 27 | public function __construct(Propagation $delegate, array $keyToNameMap) 28 | { 29 | $this->delegate = $delegate; 30 | $this->keyToName = $keyToNameMap; 31 | $this->nameToKey = array_flip($keyToNameMap); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getKeys(): array 38 | { 39 | return array_values($this->nameToKey); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getInjector(Setter $setter): callable 46 | { 47 | $delegateInjector = $this->delegate->getInjector($setter); 48 | 49 | /** 50 | * @param TraceContext $traceContext 51 | * @param $carrier 52 | * @return void 53 | */ 54 | return function (TraceContext $traceContext, &$carrier) use ($setter, $delegateInjector): void { 55 | foreach ($traceContext->getExtra() as $name => $value) { 56 | if (!array_key_exists($name, $this->nameToKey)) { 57 | continue; 58 | } 59 | 60 | $setter->put($carrier, $this->nameToKey[$name], $value); 61 | } 62 | 63 | $delegateInjector($traceContext, $carrier); 64 | }; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function getExtractor(Getter $getter): callable 71 | { 72 | $delegateExtractor = $this->delegate->getExtractor($getter); 73 | 74 | /** 75 | * @param $carrier 76 | * @return SamplingFlags 77 | */ 78 | return function ($carrier) use ($getter, $delegateExtractor): SamplingFlags { 79 | $traceContext = $delegateExtractor($carrier); 80 | if (!($traceContext instanceof TraceContext)) { 81 | return $traceContext; 82 | } 83 | 84 | $extra = []; 85 | foreach ($this->keyToName as $key => $name) { 86 | $value = $getter->get($carrier, $key); 87 | 88 | if ($value === null) { 89 | continue; 90 | } 91 | 92 | $extra[$name] = $value; 93 | } 94 | 95 | return $traceContext->withExtra($extra); 96 | }; 97 | } 98 | 99 | public function supportsJoin(): bool 100 | { 101 | return $this->delegate->supportsJoin(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Zipkin/Propagation/Getter.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Propagation/RemoteSetter.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/Propagation/SamplingFlags.php: -------------------------------------------------------------------------------- 1 | tracing->getPropagation()->getExtractor(new ServerHeaders); 16 | * $extractedContext = $extractor($_SERVER); 17 | */ 18 | final class ServerHeaders implements Getter 19 | { 20 | /** 21 | * {@inheritdoc} 22 | * 23 | * @param mixed $carrier 24 | * @param string $key 25 | * @return string|null 26 | */ 27 | public function get($carrier, string $key): ?string 28 | { 29 | // Headers in $_SERVER are always uppercased, with any - replaced with an _ 30 | $key = strtoupper($key); 31 | $key = str_replace('-', '_', $key); 32 | 33 | return $carrier['HTTP_' . $key] ?? null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Zipkin/Propagation/Setter.php: -------------------------------------------------------------------------------- 1 | traceContext = $context; 22 | $this->recorder = $recorder; 23 | } 24 | 25 | /** 26 | * When true, no recording is done and nothing is reported to zipkin. However, this span should 27 | * still be injected into outgoing requests. Use this flag to avoid performing expensive 28 | * computation. 29 | * 30 | * @return bool 31 | */ 32 | public function isNoop(): bool 33 | { 34 | return false; 35 | } 36 | 37 | /** 38 | * @return TraceContext 39 | */ 40 | public function getContext(): TraceContext 41 | { 42 | return $this->traceContext; 43 | } 44 | 45 | /** 46 | * Starts the span with an implicit timestamp. 47 | * 48 | * Spans can be modified before calling start. For example, you can add tags to the span and 49 | * set its name without lock contention. 50 | * 51 | * @param int $timestamp 52 | * @return void 53 | * @throws \InvalidArgumentException 54 | */ 55 | public function start(?int $timestamp = null): void 56 | { 57 | if ($timestamp !== null && !isValid($timestamp)) { 58 | throw new InvalidArgumentException( 59 | \sprintf('Invalid timestamp. Expected int, got %s', $timestamp) 60 | ); 61 | } 62 | 63 | $this->recorder->start($this->traceContext, $timestamp ?: now()); 64 | } 65 | 66 | /** 67 | * Sets the string name for the logical operation this span represents. 68 | * 69 | * @param string $name 70 | * @return void 71 | */ 72 | public function setName(string $name): void 73 | { 74 | $this->recorder->setName($this->traceContext, $name); 75 | } 76 | 77 | /** 78 | * The kind of span is optional. When set, it affects how a span is reported. For example, if the 79 | * kind is {@link Zipkin\Kind\SERVER}, the span's start timestamp is implicitly annotated as "sr" 80 | * and that plus its duration as "ss". 81 | * 82 | * @param string $kind 83 | * @return void 84 | */ 85 | public function setKind(string $kind): void 86 | { 87 | $this->recorder->setKind($this->traceContext, $kind); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | * 93 | * @param string $key Name used to lookup spans, such as "your_app.version". See {@link Zipkin\Tags} for 94 | * standard ones. 95 | * @param string $value. 96 | * @return void 97 | */ 98 | public function tag(string $key, string $value): void 99 | { 100 | $this->recorder->tag($this->traceContext, $key, $value); 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | */ 106 | public function setError(Throwable $e): void 107 | { 108 | $this->recorder->setError($this->traceContext, $e); 109 | } 110 | 111 | /** 112 | * Associates an event that explains latency with the current system time. 113 | * 114 | * @param string $value A short tag indicating the event, like "finagle.retry" 115 | * @param int|null $timestamp 116 | * @return void 117 | * @throws \InvalidArgumentException 118 | * @see Zipkin\Annotations 119 | */ 120 | public function annotate(string $value, ?int $timestamp = null): void 121 | { 122 | if ($timestamp !== null && !isValid($timestamp)) { 123 | throw new InvalidArgumentException( 124 | \sprintf('Valid timestamp represented microtime expected, got \'%s\'', $timestamp) 125 | ); 126 | } 127 | 128 | $this->recorder->annotate($this->traceContext, $timestamp ?: now(), $value); 129 | } 130 | 131 | /** 132 | * For a client span, this would be the server's address. 133 | * 134 | * It is often expensive to derive a remote address: always check {@link #isNoop()} first! 135 | * 136 | * @param Endpoint $remoteEndpoint 137 | * @return void 138 | */ 139 | public function setRemoteEndpoint(Endpoint $remoteEndpoint): void 140 | { 141 | $this->recorder->setRemoteEndpoint($this->traceContext, $remoteEndpoint); 142 | } 143 | 144 | /** 145 | * Throws away the current span without reporting it. 146 | * 147 | * @return void 148 | */ 149 | public function abandon(): void 150 | { 151 | $this->recorder->abandon($this->traceContext); 152 | } 153 | 154 | /** 155 | * Like {@link #finish()}, except with a given timestamp in microseconds. 156 | * 157 | * {@link zipkin.Span#duration Zipkin's span duration} is derived by subtracting the start 158 | * timestamp from this, and set when appropriate. 159 | * 160 | * @param int|null $timestamp 161 | * @return void 162 | * @throws \InvalidArgumentException 163 | */ 164 | public function finish(?int $timestamp = null): void 165 | { 166 | if ($timestamp !== null && !isValid($timestamp)) { 167 | throw new InvalidArgumentException('Invalid timestamp'); 168 | } 169 | 170 | $this->recorder->finish($this->traceContext, $timestamp ?? now()); 171 | } 172 | 173 | /** 174 | * Reports the span, even if unfinished. Most users will not call this method. 175 | * 176 | * This primarily supports two use cases: one-way spans and orphaned spans. 177 | * For example, a one-way span can be modeled as a span where one flusher calls start and another 178 | * calls finish. In order to report that span from its origin, flush must be called. 179 | * 180 | * Another example is where a user didn't call finish within a deadline or before a shutdown 181 | * occurs. By flushing, you can report what was in progress. 182 | * 183 | * @return void 184 | */ 185 | public function flush(): void 186 | { 187 | $this->recorder->flush($this->traceContext); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Zipkin/Recorder.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/Recording/ReadbackSpan.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/Reporter.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/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 | -------------------------------------------------------------------------------- /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/InMemory.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/JsonV2Serializer.php: -------------------------------------------------------------------------------- 1 | errorParser = $errorParser ?? new DefaultErrorParser(); 19 | } 20 | 21 | /** 22 | * @param ReadbackSpan[]|array $spans 23 | */ 24 | public function serialize(array $spans): string 25 | { 26 | $spansAsArray = array_map([self::class, 'serializeSpan'], $spans); 27 | 28 | return '[' . implode(',', $spansAsArray) . ']'; 29 | } 30 | 31 | private static function serializeEndpoint(Endpoint $endpoint): string 32 | { 33 | $endpointStr = '{"serviceName":"' . \strtolower(self::escapeString($endpoint->getServiceName())) . '"'; 34 | 35 | if ($endpoint->getIpv4() !== null) { 36 | $endpointStr .= ',"ipv4":"' . $endpoint->getIpv4() . '"'; 37 | } 38 | 39 | if ($endpoint->getIpv6() !== null) { 40 | $endpointStr .= ',"ipv6":"' . $endpoint->getIpv6() . '"'; 41 | } 42 | 43 | if ($endpoint->getPort() !== null) { 44 | $endpointStr .= ',"port":' . $endpoint->getPort(); 45 | } 46 | 47 | return $endpointStr . '}'; 48 | } 49 | 50 | private static function escapeString(string $s): string 51 | { 52 | $encodedString = \json_encode($s); 53 | return $encodedString === false ? $s : \mb_substr($encodedString, 1, -1); 54 | } 55 | 56 | private function serializeSpan(ReadbackSpan $span): string 57 | { 58 | $spanStr = 59 | '{"id":"' . $span->getSpanId() . '"' 60 | . ',"traceId":"' . $span->getTraceId() . '"' 61 | . ',"timestamp":' . $span->getTimestamp(); 62 | 63 | if ($span->getName() !== null) { 64 | $spanStr .= ',"name":"' . \strtolower(self::escapeString($span->getName())) . '"'; 65 | } 66 | 67 | if ($span->getDuration() !== null) { 68 | $spanStr .= ',"duration":' . $span->getDuration(); 69 | } 70 | 71 | if (null !== ($localEndpoint = $span->getLocalEndpoint())) { 72 | $spanStr .= ',"localEndpoint":' . self::serializeEndpoint($localEndpoint); 73 | } 74 | 75 | if ($span->getParentId() !== null) { 76 | $spanStr .= ',"parentId":"' . $span->getParentId() . '"'; 77 | } 78 | 79 | if ($span->isDebug()) { 80 | $spanStr .= ',"debug":true'; 81 | } 82 | 83 | if ($span->isShared()) { 84 | $spanStr .= ',"shared":true'; 85 | } 86 | 87 | if ($span->getKind() !== null) { 88 | $spanStr .= ',"kind":"' . $span->getKind() . '"'; 89 | } 90 | 91 | if (null !== ($remoteEndpoint = $span->getRemoteEndpoint())) { 92 | $spanStr .= ',"remoteEndpoint":' . self::serializeEndpoint($remoteEndpoint); 93 | } 94 | 95 | if (!empty($span->getAnnotations())) { 96 | $spanStr .= ',"annotations":['; 97 | $firstIteration = true; 98 | foreach ($span->getAnnotations() as $annotation) { 99 | if ($firstIteration) { 100 | $firstIteration = false; 101 | } else { 102 | $spanStr .= ','; 103 | } 104 | $spanStr .= '{"value":"' . self::escapeString($annotation['value']) 105 | . '","timestamp":' . $annotation['timestamp'] . '}'; 106 | } 107 | $spanStr .= ']'; 108 | } 109 | 110 | if ($span->getError() === null) { 111 | $tags = $span->getTags(); 112 | } else { 113 | $tags = $span->getTags() + $this->errorParser->parseTags($span->getError()); 114 | } 115 | 116 | if (!empty($tags)) { 117 | $spanStr .= ',"tags":{'; 118 | $firstIteration = true; 119 | foreach ($tags as $key => $value) { 120 | if ($firstIteration) { 121 | $firstIteration = false; 122 | } else { 123 | $spanStr .= ','; 124 | } 125 | $spanStr .= '"' . $key . '":"' . self::escapeString($value) . '"'; 126 | } 127 | $spanStr .= '}'; 128 | } 129 | 130 | return $spanStr . '}'; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Zipkin/Reporters/Log.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 | -------------------------------------------------------------------------------- /src/Zipkin/Reporters/Log/LogSerializer.php: -------------------------------------------------------------------------------- 1 | getName()); 26 | $serialized[] = sprintf('TraceID: %s', $span->getTraceId()); 27 | $serialized[] = sprintf('SpanID: %s', $span->getSpanId()); 28 | if (!is_null($parentID = $span->getParentId())) { 29 | $serialized[] = sprintf('StartTime: %s', $parentID); 30 | } 31 | $serialized[] = sprintf('Timestamp: %s', $span->getTimestamp()); 32 | $serialized[] = sprintf('Duration: %s', $span->getDuration()); 33 | $serialized[] = sprintf('Kind: %s', $span->getKind()); 34 | 35 | $serialized[] = sprintf('LocalEndpoint: %s', $span->getLocalEndpoint()->getServiceName()); 36 | 37 | if (\count($tags = $span->getTags()) > 0) { 38 | $serialized[] = 'Tags:'; 39 | 40 | foreach ($tags as $key => $value) { 41 | $serialized[] = sprintf(' %s: %s', $key, $value); 42 | } 43 | } 44 | 45 | if (\count($annotations = $span->getAnnotations()) > 0) { 46 | $serialized[] = 'Annotations:'; 47 | 48 | foreach ($annotations as $annotation) { 49 | $serialized[] = sprintf(' - timestamp: %s', $annotation['timestamp']); 50 | $serialized[] = sprintf(' value: %s', $annotation['value']); 51 | } 52 | } 53 | 54 | if (!is_null($remoteEndpoint = $span->getRemoteEndpoint())) { 55 | $serialized[] = sprintf('RemoteEndpoint: %s', $remoteEndpoint->getServiceName()); 56 | } 57 | 58 | return implode(PHP_EOL, $serialized); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Zipkin/Reporters/Noop.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 | -------------------------------------------------------------------------------- /src/Zipkin/Samplers/PercentageSampler.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/Span.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 | -------------------------------------------------------------------------------- /src/Zipkin/SpanCustomizerShield.php: -------------------------------------------------------------------------------- 1 | delegate = $span; 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function setName(string $name): void 25 | { 26 | $this->delegate->setName($name); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function tag(string $key, string $value): void 33 | { 34 | $this->delegate->tag($key, $value); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function annotate(string $value, ?int $timestamp = null): void 41 | { 42 | $this->delegate->annotate($value, $timestamp); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Zipkin/SpanName.php: -------------------------------------------------------------------------------- 1 | method call style 19 | if (\gettype($fn[0]) === 'string') { // static class 20 | $name = $fn[0] . '::' . $fn[1]; 21 | } elseif (\strpos(\get_class($fn[0]), 'class@anonymous') !== 0) { 22 | $name = \get_class($fn[0]) . '::' . $fn[1]; // non anonymous class 23 | } else { 24 | $name = $fn[1]; // anonymous class, hence we use the method 25 | } 26 | } elseif ($fnType === 'object' && !($fn instanceof Closure)) { // invokable 27 | $fnClass = \get_class($fn); 28 | if (\strpos($fnClass, 'class@anonymous') !== 0) { 29 | $name = $fnClass; 30 | } 31 | } 32 | $namePieces = \explode("\\", $name); 33 | return $namePieces[\count($namePieces) - 1]; 34 | } 35 | -------------------------------------------------------------------------------- /src/Zipkin/Tags.php: -------------------------------------------------------------------------------- 1 | Used to filter by host as opposed to ip address.

11 | */ 12 | const HTTP_HOST = 'http.host'; 13 | 14 | /** 15 | * The HTTP method, or verb, such as "GET" or "POST". 16 | * 17 | *

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/Timestamp.php: -------------------------------------------------------------------------------- 1 | localServiceName = $localServiceName; 50 | return $this; 51 | } 52 | 53 | /** 54 | * @param Endpoint $endpoint Endpoint of the local service being traced. Defaults to site local. 55 | */ 56 | public function havingLocalEndpoint(Endpoint $endpoint): self 57 | { 58 | $this->localEndpoint = $endpoint; 59 | return $this; 60 | } 61 | 62 | /** 63 | * Controls how spans are reported. Defaults to logging, but often an {@link AsyncReporter} 64 | * which batches spans before sending to Zipkin. 65 | * 66 | * The {@link AsyncReporter} includes a {@link Sender}, which is a driver for transports like 67 | * http, kafka and scribe. 68 | * 69 | *

For example, here's how to batch send spans via http: 70 | * 71 | *

{@code
 72 |      * reporter = AsyncReporter.v2(URLConnectionSender.json("http://localhost:9411/api/v2/spans"));
 73 |      *
 74 |      * tracingBuilder.spanReporter(reporter);
 75 |      * }
76 | * 77 | *

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 | -------------------------------------------------------------------------------- /tests/Integration/Instrumentation/Http/Server/MiddlewareTest.php: -------------------------------------------------------------------------------- 1 | havingReporter($reporter) 35 | ->havingSampler(BinarySampler::createAsAlwaysSample()) 36 | ->build(); 37 | $tracer = $tracing->getTracer(); 38 | 39 | return [ 40 | new HttpServerTracing($tracing, $parser), 41 | static function () use ($tracer, $reporter): array { 42 | $tracer->flush(); 43 | return $reporter->flush(); 44 | } 45 | ]; 46 | } 47 | 48 | public function testMiddlewareRecordsRequestSuccessfully() 49 | { 50 | $parser = new class() extends DefaultHttpServerParser { 51 | public function request(Request $request, TraceContext $context, SpanCustomizer $span): void 52 | { 53 | // This parser retrieves the user_id from the request and add 54 | // is a tag. 55 | $userId = $request->unwrap()->getAttribute('user_id'); 56 | $span->tag('user_id', $userId); 57 | parent::request($request, $context, $span); 58 | } 59 | }; 60 | 61 | list($serverTracing, $flusher) = self::createTracing($parser); 62 | 63 | $fastRouteDispatcher = simpleDispatcher(function (RouteCollector $r) { 64 | $r->addRoute('GET', '/users/{user_id}', function ($request) { 65 | return new Response(201); 66 | }); 67 | }); 68 | 69 | $request = Factory::createServerRequest('GET', '/users/abc123'); 70 | 71 | $response = Dispatcher::run([ 72 | new Middlewares\FastRoute($fastRouteDispatcher), 73 | new Middleware($serverTracing), 74 | new Middlewares\RequestHandler(), 75 | ], $request); 76 | 77 | $this->assertEquals(201, $response->getStatusCode()); 78 | 79 | $spans = ($flusher)(); 80 | 81 | $this->assertCount(1, $spans); 82 | 83 | /** 84 | * @var ReadbackSpan $span 85 | */ 86 | $span = $spans[0]; 87 | 88 | $this->assertEquals('GET', $span->getName()); 89 | $this->assertEquals([ 90 | 'http.method' => 'GET', 91 | 'http.path' => '/users/abc123', 92 | 'user_id' => 'abc123', 93 | ], $span->getTags()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /tests/Integration/Reporters/Http/CurlFactoryTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('The pcntl extension is not available.'); 21 | } 22 | } 23 | 24 | public function testHttpReportingSuccess() 25 | { 26 | $t = $this; 27 | 28 | $server = HttpTestServer::create( 29 | function (RequestInterface $request, ResponseInterface &$response) use ($t) { 30 | $t->assertEquals('POST', $request->getMethod()); 31 | $t->assertEquals('application/json', $request->getHeader('Content-Type')[0]); 32 | $t->assertEquals('0', $request->getHeader('b3')[0]); 33 | $response = $response->withStatus(202); 34 | } 35 | ); 36 | 37 | $server->start(); 38 | 39 | try { 40 | $curlClient = CurlFactory::create()->build([ 41 | 'endpoint_url' => $server->getUrl(), 42 | ]); 43 | 44 | $curlClient(\json_encode([])); 45 | $this->assertTrue(true); 46 | } finally { 47 | $server->stop(); 48 | } 49 | } 50 | 51 | public function testHttpReportingSuccessWithExtraHeader() 52 | { 53 | $t = $this; 54 | 55 | $server = HttpTestServer::create( 56 | function (RequestInterface $request, ResponseInterface &$response) use ($t) { 57 | $t->assertEquals('POST', $request->getMethod()); 58 | $t->assertEquals('application/json', $request->getHeader('Content-Type')[0]); 59 | $t->assertEquals('user@example.com', $request->getHeader('From')[0]); 60 | $response = $response->withStatus(202); 61 | } 62 | ); 63 | 64 | $server->start(); 65 | 66 | try { 67 | $curlClient = CurlFactory::create()->build([ 68 | 'endpoint_url' => $server->getUrl(), 69 | 'headers' => ['From' => 'user@example.com', 'Content-Type' => 'test'], 70 | ]); 71 | 72 | $curlClient(\json_encode([])); 73 | $this->assertTrue(true); 74 | } finally { 75 | $server->stop(); 76 | } 77 | } 78 | 79 | public function testHttpReportingFailsDueToInvalidStatusCode() 80 | { 81 | $server = HttpTestServer::create( 82 | function (RequestInterface $request, ResponseInterface &$response) { 83 | $response = $response->withStatus(404); 84 | } 85 | ); 86 | 87 | $server->start(); 88 | 89 | try { 90 | $this->expectException(RuntimeException::class); 91 | $this->expectExceptionMessage('Reporting of spans failed'); 92 | 93 | $curlClient = CurlFactory::create()->build([ 94 | 'endpoint_url' => $server->getUrl(), 95 | ]); 96 | 97 | $curlClient(''); 98 | 99 | $server->stop(); 100 | 101 | $this->fail('Runtime exception expected'); 102 | } finally { 103 | $server->stop(); 104 | } 105 | } 106 | 107 | public function testHttpReportingFailsDueToUnreachableUrl() 108 | { 109 | $this->expectException(RuntimeException::class); 110 | $this->expectExceptionMessage('Reporting of spans failed'); 111 | 112 | $curlClient = CurlFactory::create()->build([ 113 | 'endpoint_url' => 'invalid_url', 114 | ]); 115 | 116 | $curlClient(''); 117 | } 118 | 119 | public function testHttpReportingSilentlySendTracesSuccess() 120 | { 121 | $t = $this; 122 | 123 | $server = HttpTestServer::create( 124 | function (RequestInterface $request, ResponseInterface &$response) use ($t) { 125 | $t->assertEquals('POST', $request->getMethod()); 126 | $t->assertEquals('application/json', $request->getHeader('Content-Type')[0]); 127 | $response = $response->withStatus(202); 128 | $response->getBody()->write('Accepted'); 129 | } 130 | ); 131 | 132 | $server->start(); 133 | 134 | try { 135 | $curlClient = CurlFactory::create()->build([ 136 | 'endpoint_url' => $server->getUrl(), 137 | ]); 138 | 139 | ob_start(); 140 | $curlClient(\json_encode([])); 141 | } finally { 142 | $server->stop(); 143 | $output = ob_get_clean(); 144 | $this->assertEmpty($output); 145 | } 146 | } 147 | 148 | public function testHttpReportingSilentlySendTracesFailure() 149 | { 150 | $server = HttpTestServer::create( 151 | function (RequestInterface $request, ResponseInterface &$response) { 152 | $response = $response->withStatus(404); 153 | $response->getBody()->write('Not Found'); 154 | } 155 | ); 156 | 157 | $server->start(); 158 | 159 | try { 160 | $this->expectException(RuntimeException::class); 161 | $this->expectExceptionMessage('Reporting of spans failed'); 162 | 163 | $curlClient = CurlFactory::create()->build([ 164 | 'endpoint_url' => $server->getUrl(), 165 | ]); 166 | 167 | ob_start(); 168 | $curlClient(''); 169 | } finally { 170 | $server->stop(); 171 | $output = ob_get_clean(); 172 | $this->assertEmpty($output); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/Unit/DefaultErrorParserException.php: -------------------------------------------------------------------------------- 1 | message = 'default error'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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/DefaultTracingTest.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 | -------------------------------------------------------------------------------- /tests/Unit/EndpointTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 14 | $this->expectExceptionMessage( 15 | 'Invalid IPv4. Expected something in the range 0.0.0.0 and 255.255.255.255, got 256.168.33.11' 16 | ); 17 | Endpoint::create('my_service', '256.168.33.11'); 18 | } 19 | 20 | public function testEndpointFailsDueToInvalidIpv6() 21 | { 22 | $this->expectException(InvalidArgumentException::class); 23 | $this->expectExceptionMessage('Invalid IPv6 1200::AB00:1234::2552:7777:1313'); 24 | Endpoint::create('my_service', null, '1200::AB00:1234::2552:7777:1313'); 25 | } 26 | 27 | public function testEndpointFailsDueToInvalidPort() 28 | { 29 | $this->expectException(InvalidArgumentException::class); 30 | $this->expectExceptionMessage('Invalid port. Expected a number between 0 and 65535, got 65536'); 31 | Endpoint::create('my_service', null, null, 65536); 32 | } 33 | 34 | public function testEndpointIsCreatedSuccessfully() 35 | { 36 | $endpoint = Endpoint::create( 37 | 'my_service', 38 | '192.168.33.11', 39 | '1200:0000:AB00:1234:0000:2552:7777:1313', 40 | 1234 41 | ); 42 | 43 | $this->assertEquals('my_service', $endpoint->getServiceName()); 44 | $this->assertEquals('192.168.33.11', $endpoint->getIpv4()); 45 | $this->assertEquals('1200:0000:AB00:1234:0000:2552:7777:1313', $endpoint->getIpv6()); 46 | $this->assertEquals(1234, $endpoint->getPort()); 47 | } 48 | 49 | public function testEndpointFromGlobalsIsCreatedSuccessfully() 50 | { 51 | $endpoint = Endpoint::createFromGlobals(); 52 | $this->assertEquals(PHP_SAPI, $endpoint->getServiceName()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Unit/InSpan/Callables.php: -------------------------------------------------------------------------------- 1 | 'test_value'] 30 | ); 31 | $this->assertInstanceOf(Request::class, $request); 32 | $this->assertEquals('GET', $request->getMethod()); 33 | $this->assertEquals('/path/to', $request->getPath()); 34 | $this->assertNull($request->getHeader('test_missing_key')); 35 | $this->assertEquals('test_value', $request->getHeader('test_key')); 36 | $this->assertSame($delegateRequest, $request->unwrap()); 37 | } 38 | 39 | /** 40 | * @dataProvider rootPathsProvider 41 | */ 42 | public function testRequestIsNormalizesRootPath(string $path): void 43 | { 44 | list($request) = static::createRequest('GET', $path, ['test_key' => 'test_value']); 45 | $this->assertInstanceOf(Request::class, $request); 46 | $this->assertEquals('/', $request->getPath()); 47 | } 48 | 49 | public static function rootPathsProvider(): array 50 | { 51 | return [ 52 | ['http://test.com'], 53 | ['http://test.com?'], 54 | ['http://test.com/'], 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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/Instrumentation/Http/Client/Psr18/ClientTest.php: -------------------------------------------------------------------------------- 1 | havingReporter($reporter) 29 | ->havingSampler(BinarySampler::createAsAlwaysSample()) 30 | ->build(); 31 | $tracer = $tracing->getTracer(); 32 | 33 | return [ 34 | new HttpClientTracing($tracing, new DefaultHttpClientParser), 35 | static function () use ($tracer, $reporter): array { 36 | $tracer->flush(); 37 | return $reporter->flush(); 38 | } 39 | ]; 40 | } 41 | 42 | public function testClientSendRequestSuccess() 43 | { 44 | $client = new class() implements ClientInterface { 45 | private $lastRequest; 46 | 47 | public function sendRequest(RequestInterface $request): ResponseInterface 48 | { 49 | $this->lastRequest = $request; 50 | return new Response(200); 51 | } 52 | 53 | public function getLastRequest(): ?RequestInterface 54 | { 55 | return $this->lastRequest; 56 | } 57 | }; 58 | 59 | list($tracing, $flusher) = self::createTracing(); 60 | $tracedClient = new Client($client, $tracing); 61 | $request = new Request('GET', 'http://mytest'); 62 | $response = $tracedClient->sendRequest($request); 63 | 64 | $this->assertTrue($client->getLastRequest()->hasHeader('X-B3-TraceId')); 65 | $this->assertTrue($client->getLastRequest()->hasHeader('X-B3-SpanId')); 66 | 67 | $this->assertEquals(200, $response->getStatusCode()); 68 | $this->assertEquals($request->getMethod(), $client->getLastRequest()->getMethod()); 69 | $this->assertEquals($request->getUri(), $client->getLastRequest()->getUri()); 70 | 71 | $spans = ($flusher)(); 72 | 73 | $this->assertCount(1, $spans); 74 | 75 | $span = $spans[0]; 76 | 77 | $this->assertEquals('GET', $span->getName()); 78 | $this->assertEquals([ 79 | 'http.method' => 'GET', 80 | 'http.path' => '/', 81 | ], $span->getTags()); 82 | } 83 | 84 | public function testClientSendRequestSuccessWithNon2xx() 85 | { 86 | $client = new class() implements ClientInterface { 87 | private $lastRequest; 88 | 89 | public function sendRequest(RequestInterface $request): ResponseInterface 90 | { 91 | $this->lastRequest = $request; 92 | return new Response(404); 93 | } 94 | 95 | public function getLastRequest(): ?RequestInterface 96 | { 97 | return $this->lastRequest; 98 | } 99 | }; 100 | 101 | list($tracing, $flusher) = self::createTracing(); 102 | $tracedClient = new Client($client, $tracing); 103 | $request = new Request('GET', 'http://mytest'); 104 | $response = $tracedClient->sendRequest($request); 105 | 106 | $this->assertTrue($client->getLastRequest()->hasHeader('X-B3-TraceId')); 107 | $this->assertTrue($client->getLastRequest()->hasHeader('X-B3-SpanId')); 108 | 109 | $this->assertEquals(404, $response->getStatusCode()); 110 | $this->assertEquals($request->getMethod(), $client->getLastRequest()->getMethod()); 111 | $this->assertEquals($request->getUri(), $client->getLastRequest()->getUri()); 112 | 113 | $spans = ($flusher)(); 114 | 115 | $this->assertCount(1, $spans); 116 | 117 | $span = $spans[0]; 118 | 119 | $this->assertEquals('GET', $span->getName()); 120 | $this->assertEquals('CLIENT', $span->getKind()); 121 | $this->assertEquals([ 122 | 'http.method' => 'GET', 123 | 'http.path' => '/', 124 | 'http.status_code' => '404', 125 | 'error' => '404', 126 | ], $span->getTags()); 127 | } 128 | 129 | public function testClientSendRequestFails() 130 | { 131 | $client = new class() implements ClientInterface { 132 | private $lastRequest; 133 | 134 | public function sendRequest(RequestInterface $request): ResponseInterface 135 | { 136 | $this->lastRequest = $request; 137 | throw new RuntimeException('transport error'); 138 | } 139 | 140 | public function getLastRequest(): ?RequestInterface 141 | { 142 | return $this->lastRequest; 143 | } 144 | }; 145 | 146 | list($tracing, $flusher) = self::createTracing(); 147 | $tracedClient = new Client($client, $tracing); 148 | $request = new Request('GET', 'http://mytest'); 149 | try { 150 | $tracedClient->sendRequest($request); 151 | $this->fail('should not reach this'); 152 | } catch (\Throwable $e) { 153 | } 154 | 155 | $this->assertTrue($client->getLastRequest()->hasHeader('X-B3-TraceId')); 156 | $this->assertTrue($client->getLastRequest()->hasHeader('X-B3-SpanId')); 157 | 158 | $spans = ($flusher)(); 159 | 160 | $this->assertCount(1, $spans); 161 | 162 | $span = $spans[0]; 163 | 164 | $this->assertEquals('GET', $span->getName()); 165 | $this->assertEquals([ 166 | 'http.method' => 'GET', 167 | 'http.path' => '/' 168 | ], $span->getTags()); 169 | $this->assertEquals('transport error', $span->getError()->getMessage()); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/Unit/Instrumentation/Http/Client/Psr18/RequestTestCase.php: -------------------------------------------------------------------------------- 1 | 'test_value'] 33 | ); 34 | $this->assertInstanceOf(Request::class, $request); 35 | $this->assertEquals('GET', $request->getMethod()); 36 | $this->assertEquals('/path/to', $request->getPath()); 37 | $this->assertNull($request->getHeader('test_missing_key')); 38 | $this->assertEquals('test_value', $request->getHeader('test_key')); 39 | $this->assertSame($delegateRequest, $request->unwrap()); 40 | } 41 | 42 | /** 43 | * @dataProvider rootPathsProvider 44 | */ 45 | public function testRequestIsNormalizesRootPath(string $path): void 46 | { 47 | /** 48 | * @var Request $request 49 | */ 50 | list($request) = static::createRequest('GET', $path, ['test_key' => 'test_value']); 51 | $this->assertEquals('/', $request->getPath()); 52 | } 53 | 54 | public static function rootPathsProvider(): array 55 | { 56 | return [ 57 | ['http://test.com'], 58 | ['http://test.com?'], 59 | ['http://test.com/'], 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Unit/Instrumentation/Http/Server/BaseResponseTestCase.php: -------------------------------------------------------------------------------- 1 | assertEquals(202, $response->getStatusCode()); 60 | $this->assertSame($request, $response->getRequest()); 61 | $this->assertSame($delegateResponse, $response->unwrap()); 62 | if (self::supportsRoute()) { 63 | $this->assertSame('/users/{user_id}', $route); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Unit/Instrumentation/Http/Server/Psr15/MiddlewareTest.php: -------------------------------------------------------------------------------- 1 | havingReporter($reporter) 33 | ->havingSampler(BinarySampler::createAsAlwaysSample()) 34 | ->build(); 35 | $tracer = $tracing->getTracer(); 36 | 37 | return [ 38 | new HttpServerTracing($tracing, new DefaultHttpServerParser(), $requestSampler), 39 | static function () use ($tracer, $reporter): array { 40 | $tracer->flush(); 41 | return $reporter->flush(); 42 | } 43 | ]; 44 | } 45 | 46 | private static function createRequestHandler($response = null): RequestHandlerInterface 47 | { 48 | return new class($response) implements RequestHandlerInterface { 49 | private $response; 50 | private $lastRequest; 51 | 52 | public function __construct(?ResponseInterface $response) 53 | { 54 | $this->response = $response ?? new Psr7Response(); 55 | } 56 | 57 | public function handle(ServerRequestInterface $request): ResponseInterface 58 | { 59 | $this->lastRequest = $request; 60 | 61 | return $this->response; 62 | } 63 | 64 | public function getLastRequest(): ?RequestInterface 65 | { 66 | return $this->lastRequest; 67 | } 68 | }; 69 | } 70 | 71 | public function testMiddlewareHandlesRequestSuccessfully() 72 | { 73 | list($tracing, $flusher) = self::createTracing(); 74 | $request = new ServerRequest('GET', 'http://mytest'); 75 | 76 | $handler = self::createRequestHandler(); 77 | 78 | $middleware = new Middleware($tracing); 79 | $middleware->process($request, $handler); 80 | 81 | $this->assertSame($request, $handler->getLastRequest()); 82 | 83 | $spans = ($flusher)(); 84 | 85 | $this->assertCount(1, $spans); 86 | 87 | /** 88 | * @var ReadbackSpan $span 89 | */ 90 | $span = $spans[0]; 91 | 92 | $this->assertEquals('GET', $span->getName()); 93 | $this->assertEquals([ 94 | 'http.method' => 'GET', 95 | 'http.path' => '/', 96 | ], $span->getTags()); 97 | } 98 | 99 | public function testMiddlewareParsesRequestSuccessfullyWithNon2xx() 100 | { 101 | list($tracing, $flusher) = self::createTracing(); 102 | $request = new ServerRequest('GET', 'http://mytest'); 103 | 104 | $handler = self::createRequestHandler(new Psr7Response(404)); 105 | 106 | $middleware = new Middleware($tracing); 107 | $middleware->process($request, $handler); 108 | 109 | $this->assertSame($request, $handler->getLastRequest()); 110 | 111 | $spans = ($flusher)(); 112 | 113 | $this->assertCount(1, $spans); 114 | 115 | /** 116 | * @var ReadbackSpan $span 117 | */ 118 | $span = $spans[0]; 119 | 120 | $this->assertEquals('GET', $span->getName()); 121 | $this->assertEquals([ 122 | 'http.method' => 'GET', 123 | 'http.path' => '/', 124 | 'http.status_code' => '404', 125 | 'error' => '404' 126 | ], $span->getTags()); 127 | } 128 | 129 | public function testMiddlewareKeepsContextForJoinSpan() 130 | { 131 | $request = new ServerRequest('GET', 'http://mytest'); 132 | 133 | $extractedContext = TraceContext::createAsRoot(DefaultSamplingFlags::createAsSampled()); 134 | 135 | list($tracing) = self::createTracing(); 136 | $middleware = new Middleware($tracing); 137 | 138 | /** 139 | * @var Span $span 140 | */ 141 | $span = $this->invokePrivateMethod($middleware, 'nextSpan', [$extractedContext, $request]); 142 | $this->assertSame($extractedContext->getTraceId(), $span->getContext()->getTraceId()); 143 | } 144 | 145 | /** 146 | * @dataProvider middlewareNextSpanProvider 147 | */ 148 | public function testMiddlewareNextSpanResolvesSampling( 149 | $extractedContext, 150 | callable $requestSampler, 151 | ?bool $expectedSampling 152 | ) { 153 | $request = new ServerRequest('GET', 'http://mytest'); 154 | 155 | list($tracing) = self::createTracing($requestSampler); 156 | $middleware = new Middleware($tracing); 157 | 158 | $span = $this->invokePrivateMethod($middleware, 'nextSpan', [$extractedContext, $request]); 159 | $this->assertEquals($expectedSampling, $span->getContext()->isSampled()); 160 | } 161 | 162 | public function middlewareNextSpanProvider(): array 163 | { 164 | return [ 165 | //[$extractedContext, $requestSampler, $expectedSampling] 166 | 'no context becomes sampled' => [null, function () { 167 | return true; 168 | }, true], 169 | 'not sampled becomes sampled' => [DefaultSamplingFlags::createAsNotSampled(), function () { 170 | return true; 171 | }, true], 172 | 'sampled remains the same' => [DefaultSamplingFlags::createAsSampled(), function () { 173 | return false; 174 | }, true], 175 | ]; 176 | } 177 | 178 | private function invokePrivateMethod(&$object, $methodName, array $parameters = array()) 179 | { 180 | $reflection = new \ReflectionClass(get_class($object)); 181 | $method = $reflection->getMethod($methodName); 182 | $method->setAccessible(true); 183 | return $method->invokeArgs($object, $parameters); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tests/Unit/Instrumentation/Http/Server/Psr15/RequestTestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue($span instanceof NoopSpan); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Unit/Propagation/CurrentTraceContextTest.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 | -------------------------------------------------------------------------------- /tests/Unit/Propagation/DefaultSamplingFlagsTest.php: -------------------------------------------------------------------------------- 1 | assertNull($samplingFlags0->isSampled()); 14 | $this->assertFalse($samplingFlags0->isDebug()); 15 | 16 | $samplingFlags1 = DefaultSamplingFlags::create(true, false); 17 | $this->assertTrue($samplingFlags1->isSampled()); 18 | $this->assertFalse($samplingFlags1->isDebug()); 19 | 20 | $samplingFlags2 = DefaultSamplingFlags::create(false, false); 21 | $this->assertFalse($samplingFlags2->isSampled()); 22 | $this->assertFalse($samplingFlags2->isDebug()); 23 | } 24 | 25 | public function testDebugOverridesSamplingDecision() 26 | { 27 | $samplingFlags = DefaultSamplingFlags::create(false, true); 28 | $this->assertTrue($samplingFlags->isSampled()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Propagation/ExtraFieldTest.php: -------------------------------------------------------------------------------- 1 | 'a']); 18 | $this->assertEquals(['x-a'], $propagation->getKeys()); 19 | } 20 | 21 | public function testGetInjector(): void 22 | { 23 | $propagation = new ExtraField(new B3(), ['x-request-id' => 'request_id']); 24 | $injector = $propagation->getInjector(new Map()); 25 | 26 | $carrier = []; 27 | $injector(TraceContext::createAsRoot()->withExtra( 28 | [ 29 | 'request_id' => 'abc123', 30 | 'other_field' => 'xyz987' 31 | ], 32 | ), $carrier); 33 | $this->assertEquals('abc123', $carrier['x-request-id']); 34 | $this->assertArrayNotHasKey('other_field', $carrier); 35 | } 36 | 37 | public function testGetExtractor(): void 38 | { 39 | $propagation = new ExtraField(new B3(), ['x-request-id' => 'request_id']); 40 | $extractor = $propagation->getExtractor(new Map()); 41 | 42 | /** 43 | * @var $context TraceContext 44 | */ 45 | $context = $extractor([ 46 | 'x-b3-traceid' => '7f46165474d11ee5836777d85df2cdab', 47 | 'x-b3-spanid' => '4654d1e567d8f2ab', 48 | 'x-request-id' => 'xyz987', 49 | 'x-something-else' => 'pqr456', 50 | ]); 51 | 52 | $extra = $context->getExtra(); 53 | $this->assertCount(1, $extra); 54 | $this->assertEquals('xyz987', $extra['request_id']); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/Propagation/IdTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(ctype_xdigit($nextId)); 17 | $this->assertEquals(16, strlen($nextId)); 18 | } 19 | 20 | public function testTraceIdWith128bitsSuccess() 21 | { 22 | $nextId = generateTraceIdWith128bits(); 23 | $this->assertTrue(ctype_xdigit($nextId)); 24 | $this->assertEquals(32, strlen($nextId)); 25 | } 26 | 27 | /** 28 | * @dataProvider spanIdsDataProvider 29 | */ 30 | public function testIsValidSpanIdSuccess($spanId, $isValid) 31 | { 32 | $this->assertEquals($isValid, isValidSpanId($spanId)); 33 | } 34 | 35 | public function spanIdsDataProvider() 36 | { 37 | return [ 38 | ['', false], 39 | ['1', true], 40 | ['50d1e105a060618', true], 41 | ['050d1e105a060618', true], 42 | ['g50d1e105a060618', false], 43 | ['050d1e105a060618a', false], 44 | ]; 45 | } 46 | 47 | /** 48 | * @dataProvider traceIdsDataProvider 49 | */ 50 | public function testIsValidTraceIdSuccess($traceId, $isValid) 51 | { 52 | $this->assertEquals($isValid, isValidTraceId($traceId)); 53 | } 54 | 55 | public function traceIdsDataProvider() 56 | { 57 | return [ 58 | ['', false], 59 | ['1', true], 60 | ['050d1e105a060618', true], 61 | ['g50d1e105a060618', false], 62 | ['050d1e105a060618050d1e105a060618', true], 63 | ['050d1e105a060618g50d1e105a060618', false], 64 | ['050d1e105a060618050d1e105a060618a', false], 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/Unit/Propagation/RequestHeadersTest.php: -------------------------------------------------------------------------------- 1 | get($request, self::TEST_KEY); 19 | $this->assertNull($value); 20 | } 21 | 22 | public function testGetReturnsTheExpectedValue() 23 | { 24 | $request = new Request('GET', '/', [self::TEST_KEY => self::TEST_VALUE]); 25 | $requestHeaders = new RequestHeaders; 26 | $value = $requestHeaders->get($request, self::TEST_KEY); 27 | $this->assertEquals(self::TEST_VALUE, $value); 28 | } 29 | 30 | public function testPutWritesTheExpectedValue() 31 | { 32 | $request = new Request('GET', '/'); 33 | $requestHeaders = new RequestHeaders; 34 | $requestHeaders->put($request, self::TEST_KEY, self::TEST_VALUE); 35 | $value = $requestHeaders->get($request, self::TEST_KEY); 36 | $this->assertEquals(self::TEST_VALUE, $value); 37 | } 38 | 39 | public function testPutOverridesWithTheExpectedValue() 40 | { 41 | $request = new Request('GET', '/', [self::TEST_KEY => 'foobar']); 42 | $requestHeaders = new RequestHeaders(); 43 | $requestHeaders->put($request, self::TEST_KEY, self::TEST_VALUE); 44 | $value = $requestHeaders->get($request, self::TEST_KEY); 45 | $this->assertEquals(self::TEST_VALUE, $value); 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/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 | -------------------------------------------------------------------------------- /tests/Unit/RecorderTest.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/Unit/Recording/SpanTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Span::class, $span); 21 | } 22 | 23 | public function testStartSpanSuccess() 24 | { 25 | $context = TraceContext::createAsRoot(DefaultSamplingFlags::createAsEmpty()); 26 | $endpoint = Endpoint::createAsEmpty(); 27 | $span = Span::createFromContext($context, $endpoint); 28 | $timestamp = now(); 29 | $span->start($timestamp); 30 | $this->assertEquals($timestamp, $span->getTimestamp()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = <<start($startTime); 78 | $span->setName('My\Command'); 79 | $span->tag('test_key_1', $jsonValue); 80 | $span->tag('test_key_2', $mutilineValue); 81 | $span->finish($startTime + 1000); 82 | $serializer = new JsonV2Serializer(); 83 | $serializedSpans = $serializer->serialize([$span]); 84 | 85 | $expectedSerialization = '[{' 86 | . '"id":"186f11b67460db4e","traceId":"186f11b67460db4e","timestamp":1594044779509687,' 87 | . '"name":"my\\\\command","duration":1000,"localEndpoint":{"serviceName":"service1"},' 88 | . '"tags":{"test_key_1":"{\"name\":\"Kurt\"}","test_key_2":"foo\nbar"}' 89 | . '}]'; 90 | 91 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 92 | $expectedSerialization = str_replace('\\n', '\\r\\n', $expectedSerialization); 93 | } 94 | 95 | $this->assertEquals($expectedSerialization, $serializedSpans); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Unit/Reporters/Log/LogSerializerTest.php: -------------------------------------------------------------------------------- 1 | start($startTime); 23 | $span->setName('Test'); 24 | $span->setKind('CLIENT'); 25 | $remoteEndpoint = Endpoint::create('SERVICE2', null, '2001:0db8:85a3:0000:0000:8a2e:0370:7334', 3302); 26 | $span->setRemoteEndpoint($remoteEndpoint); 27 | $span->tag('test_key', 'test_value'); 28 | $span->annotate($startTime + 100, 'test_annotation'); 29 | $span->setError(new \RuntimeException('test_error')); 30 | $span->finish($startTime + 1000); 31 | 32 | $serializer = new LogSerializer(); 33 | $serializedSpans = $serializer->serialize([$span]); 34 | 35 | $expectedSerialization = <<assertEquals($expectedSerialization, $serializedSpans); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/Unit/Reporters/NoopTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Reporter::class, $noopReporter); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unit/Samplers/BinarySamplerTest.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 | -------------------------------------------------------------------------------- /tests/Unit/SpanCustomizerShieldSpan.php: -------------------------------------------------------------------------------- 1 | test = $test; 26 | $this->context = TraceContext::createAsRoot(); 27 | } 28 | 29 | public function isNoop(): bool 30 | { 31 | return false; 32 | } 33 | 34 | public function getContext(): TraceContext 35 | { 36 | return $this->context; 37 | } 38 | 39 | public function start(?int $timestamp = null): void 40 | { 41 | throw new LogicException('should not be called'); 42 | } 43 | 44 | public function setKind(string $kind): void 45 | { 46 | throw new LogicException('should not be called'); 47 | } 48 | 49 | public function setError(Throwable $e): void 50 | { 51 | throw new LogicException('should not be called'); 52 | } 53 | 54 | public function setRemoteEndpoint(Endpoint $remoteEndpoint): void 55 | { 56 | throw new LogicException('should not be called'); 57 | } 58 | 59 | public function abandon(): void 60 | { 61 | throw new LogicException('should not be called'); 62 | } 63 | 64 | public function finish(?int $timestamp = null): void 65 | { 66 | throw new LogicException('should not be called'); 67 | } 68 | 69 | public function flush(): void 70 | { 71 | throw new LogicException('should not be called'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Unit/SpanCustomizerShieldTest.php: -------------------------------------------------------------------------------- 1 | test->assertEquals(SpanCustomizerShieldTest::TEST_NAME, $name); 29 | } 30 | 31 | public function tag(string $key, string $value): void 32 | { 33 | $this->test->assertEquals(SpanCustomizerShieldTest::TEST_TAG_KEY, $key); 34 | $this->test->assertEquals(SpanCustomizerShieldTest::TEST_TAG_VALUE, $value); 35 | } 36 | 37 | public function annotate(string $value, ?int $timestamp = null): void 38 | { 39 | $this->test->assertEquals(SpanCustomizerShieldTest::TEST_ANNOTATION_VALUE, $value); 40 | } 41 | }; 42 | 43 | $spanCustomizer = new SpanCustomizerShield($span); 44 | $spanCustomizer->setName(self::TEST_NAME); 45 | $spanCustomizer->tag(self::TEST_TAG_KEY, self::TEST_TAG_VALUE); 46 | $spanCustomizer->annotate(self::TEST_ANNOTATION_VALUE); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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/TracingBuilderTest.php: -------------------------------------------------------------------------------- 1 | build(); 24 | $this->assertInstanceOf(Tracing::class, $tracing); 25 | $this->assertEquals(false, $tracing->isNoop()); 26 | $this->assertInstanceOf(Propagation::class, $tracing->getPropagation()); 27 | } 28 | 29 | /** 30 | * @dataProvider boolProvider 31 | */ 32 | public function testCreatingTracingIncludesExpectedValues($isNoop) 33 | { 34 | $endpoint = Endpoint::createAsEmpty(); 35 | $reporter = new Noop(); 36 | $sampler = BinarySampler::createAsAlwaysSample(); 37 | $usesTraceId128bits = $this->randomBool(); 38 | $currentTraceContext = new CurrentTraceContext; 39 | $propagation = new class() implements Propagation { 40 | public function getKeys(): array 41 | { 42 | return []; 43 | } 44 | public function getInjector(Setter $setter): callable 45 | { 46 | return function () { 47 | }; 48 | } 49 | public function getExtractor(Getter $getter): callable 50 | { 51 | return function () { 52 | }; 53 | } 54 | public function supportsJoin(): bool 55 | { 56 | return true; 57 | } 58 | }; 59 | 60 | $tracing = TracingBuilder::create() 61 | ->havingLocalServiceName(self::SERVICE_NAME) 62 | ->havingLocalEndpoint($endpoint) 63 | ->havingReporter($reporter) 64 | ->havingSampler($sampler) 65 | ->havingTraceId128bits($usesTraceId128bits) 66 | ->havingCurrentTraceContext($currentTraceContext) 67 | ->beingNoop($isNoop) 68 | ->supportingJoin(true) 69 | ->havingPropagation($propagation) 70 | ->build(); 71 | 72 | $this->assertInstanceOf(Tracing::class, $tracing); 73 | $this->assertEquals($isNoop, $tracing->isNoop()); 74 | $this->assertSame($propagation, $tracing->getPropagation()); 75 | } 76 | 77 | public function boolProvider() 78 | { 79 | return [ 80 | [true], 81 | [false] 82 | ]; 83 | } 84 | 85 | private function randomBool() 86 | { 87 | return (bool) mt_rand(0, 1); 88 | } 89 | 90 | public function testAlwaysReportSpans() 91 | { 92 | // If `alwaysReportingSpans(true)` is called, we should be emitting the 93 | // spans even if the trace isn't sampled 94 | $endpoint = Endpoint::createAsEmpty(); 95 | $reporter = new InMemory(); 96 | $sampler = BinarySampler::createAsNeverSample(); 97 | 98 | $tracing = TracingBuilder::create() 99 | ->havingLocalServiceName(self::SERVICE_NAME) 100 | ->havingLocalEndpoint($endpoint) 101 | ->havingReporter($reporter) 102 | ->havingSampler($sampler) 103 | ->alwaysReportingSpans(true) 104 | ->build(); 105 | $tracer = $tracing->getTracer(); 106 | 107 | $span = $tracer->newTrace(); 108 | $span->setName('test'); 109 | $span->start(); 110 | $span->finish(); 111 | 112 | $tracer->flush(); 113 | $spans = $reporter->flush(); 114 | $this->assertCount(1, $spans); 115 | $this->assertFalse($spans[0]->isSampled()); 116 | } 117 | 118 | public function testDoNotReportByDefault() 119 | { 120 | // By default, let's verify that we don't emit any span if the 121 | // trace isn't sampled. 122 | $endpoint = Endpoint::createAsEmpty(); 123 | $reporter = new InMemory(); 124 | $sampler = BinarySampler::createAsNeverSample(); 125 | 126 | $tracing = TracingBuilder::create() 127 | ->havingLocalServiceName(self::SERVICE_NAME) 128 | ->havingLocalEndpoint($endpoint) 129 | ->havingReporter($reporter) 130 | ->havingSampler($sampler) 131 | ->build(); 132 | $tracer = $tracing->getTracer(); 133 | 134 | $span = $tracer->newTrace(); 135 | $span->setName('test'); 136 | $span->start(); 137 | $span->finish(); 138 | 139 | $tracer->flush(); 140 | $spans = $reporter->flush(); 141 | $this->assertCount(0, $spans); 142 | } 143 | } 144 | --------------------------------------------------------------------------------