├── .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 |
It is part of the HTTP RFC 25 | * that an HTTP method is case-sensitive. Do not downcase results. 26 | * 27 | * @see HTTP_METHOD 28 | */ 29 | abstract public function getMethod(): string; 30 | 31 | /** 32 | * The absolute http path, without any query parameters. Ex. "/objects/abcd-ff" 33 | * 34 | *
Conventionally associated with the key "http.path" 35 | * 36 | *
{@code null} could mean not applicable to the HTTP method (ex CONNECT). 37 | * 38 | *
Conventionally associated with the key "http.url" 52 | * 53 | * @see HTTP_URL 54 | */ 55 | abstract public function getUrl(): string; 56 | 57 | /** 58 | * Returns one value corresponding to the specified header, or null. 59 | */ 60 | abstract public function getHeader(string $name): ?string; 61 | 62 | /** 63 | * @return mixed the underlying request object or {@code null} if there is none. 64 | */ 65 | abstract public function unwrap(); 66 | } 67 | -------------------------------------------------------------------------------- /src/Zipkin/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 | *
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.
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 = <<