├── LICENSE ├── README.md ├── composer.json ├── config └── tracing.php ├── docker-compose.yml ├── resources └── docker │ └── workspace │ └── Dockerfile └── src ├── Contracts ├── Extractor.php ├── Injector.php ├── ShouldBeTraced.php ├── Span.php ├── SpanContext.php └── Tracer.php ├── Drivers ├── Null │ ├── NullSpan.php │ ├── NullSpanContext.php │ └── NullTracer.php └── Zipkin │ ├── Extractors │ ├── AMQPExtractor.php │ ├── GooglePubSubExtractor.php │ ├── IlluminateHttpExtractor.php │ ├── PsrRequestExtractor.php │ ├── TextMapExtractor.php │ └── ZipkinExtractor.php │ ├── Injectors │ ├── AMQPInjector.php │ ├── GooglePubSubInjector.php │ ├── IlluminateHttpInjector.php │ ├── PsrRequestInjector.php │ ├── TextMapInjector.php │ ├── VinelabHttpInjector.php │ └── ZipkinInjector.php │ ├── Propagation │ ├── AMQP.php │ ├── GooglePubSub.php │ ├── IlluminateHttp.php │ ├── PsrRequest.php │ └── VinelabHttp.php │ ├── ZipkinSpan.php │ ├── ZipkinSpanContext.php │ └── ZipkinTracer.php ├── Exceptions ├── UnregisteredFormatException.php └── UnresolvedCollectorIpException.php ├── Facades └── Trace.php ├── Integration └── Concerns │ └── TracesLucidArchitecture.php ├── Listeners ├── QueueJobSubscriber.php └── TraceCommand.php ├── Middleware └── TraceRequests.php ├── Propagation └── Formats.php ├── TracingDriverManager.php └── TracingServiceProvider.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2019 Vinelab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Tracing 2 | 3 | - [Introduction](#introduction) 4 | - [Requirements](#requirements) 5 | - [Installation](#installation) 6 | - [Driver Prerequisites](#driver-prerequisites) 7 | - [Zipkin](#zipkin) 8 | - [Jaeger](#jaeger) 9 | - [Null](#null) 10 | - [Usage](#usage) 11 | - [Creating Spans](#creating-spans) 12 | - [Customizing Spans](#customizing-spans) 13 | - [Retrieving Spans](#retrieving-spans) 14 | - [Controlling Spans](#controlling-spans) 15 | - [Flushing Spans](#flushing-spans) 16 | - [Logging](#logging) 17 | - [Middleware](#middleware) 18 | - [Console Commands](#console-commands) 19 | - [Queue Jobs](#queue-jobs) 20 | - [Context Propagation](#context-propagation) 21 | - [Custom Drivers](#custom-drivers) 22 | - [Writing New Driver](#writing-new-driver) 23 | - [Registering New Driver](#registering-new-driver) 24 | - [Usage With Lumen](#usage-with-lumen) 25 | - [Integrations](#integrations) 26 | - [Lucid Architecture](#lucid-architecture) 27 | 28 | ## Introduction 29 | 30 | Distributed tracing is the process of tracking the activity resulting from a request to an application. With this feature, you can: 31 | 32 | - Trace the path of a request as it travels across a complex system 33 | - Discover the latency of the components (services) along that path 34 | - Know which component in the path is creating a bottleneck 35 | - Inspect payloads that are being sent between components 36 | - Build execution graph for each separate component and more 37 | 38 | Simply put, distributed tracing is **a knowledge tool**. One of the most important perks of having it in your project is that deveopers can learn the system by simply following the traces it makes. 39 | 40 | See how Uber is using distributed tracing to make sense of large number of microservices and interactions within their product: 41 | 42 | [![](http://img.youtube.com/vi/WRntQsUajow/0.jpg)](http://www.youtube.com/watch?v=WRntQsUajow "") 43 | 44 | A distributed trace is composed of multiple spans, which represent time spent in services or resources of those services. 45 | 46 | Each **Span** has the following: 47 | 48 | - Operation name 49 | - Start timestamp 50 | - Finish timestamp 51 | - Set of zero or more key:value tags to enable lookup and record additional information 52 | - Set of zero or more logs paired with a timestamp 53 | - References to related Spans (e.g. a parent) 54 | 55 | Spans are typically displayed for your view as a time axis where each span can be unfolded to inpect additional details: 56 | 57 | ![image](https://i.gyazo.com/ee0065123c9d7536279e9e0f9ad60610.png) 58 | 59 | The **Tracer** interface (available via `Trace` facade) creates Spans and understands how to Inject (serialize) and Extract (deserialize) them across process boundaries. 60 | 61 | See [OpenTracing spec](https://opentracing.io/specification/) for more details on semantics behind distributed tracing. 62 | 63 | ## Requirements 64 | 65 | This package requires **PHP >= 7.1** and **Laravel 5.5 or later**. We also offer limited Lumen support (basic tracing and http middleware). 66 | 67 | ## Installation 68 | 69 | First, install the package via Composer: 70 | 71 | ```sh 72 | composer require vinelab/tracing-laravel 73 | ``` 74 | 75 | After installation, you can publish the package configuration using the `vendor:publish` command. This command will publish the `tracing.php` configuration file to your config directory: 76 | 77 | ```sh 78 | php artisan vendor:publish --provider="Vinelab\Tracing\TracingServiceProvider" 79 | ``` 80 | 81 | You may configure the driver and service name in your `.env` file: 82 | 83 | ```sh 84 | TRACING_DRIVER=zipkin 85 | TRACING_SERVICE_NAME=orders 86 | ``` 87 | 88 | You should also add credentials for your respective driver as described in the section below. 89 | 90 | For Lumen, see [dedicated section](#usage-with-lumen) with installation instructions. 91 | 92 | ## Driver Prerequisites 93 | 94 | ### Zipkin 95 | 96 | Use the following environment variables to configure Zipkin: 97 | 98 | ```sh 99 | ZIPKIN_HOST=localhost 100 | ZIPKIN_PORT=9411 101 | ``` 102 | 103 | If the collector is unreachable via a given hostname, you might see debug messages about that printed with each request. If you want to ignore these in production, simply edit your logging configuration to omit `debug` level messages. 104 | 105 | ### Jaeger 106 | 107 | Jaeger is not "officially" supported because of the lack of stable instrumentation for PHP. 108 | 109 | However, you can still post spans to Jaeger collector with Zipkin driver using [Zipkin compatible HTTP endpoint](https://www.jaegertracing.io/docs/1.11/features/#backwards-compatibility-with-zipkin). In fact, that's the recommended way to use this library since Jaeger's UI is just that much more convenient than Zipkin's. 110 | 111 | There are some downsides, however: 112 | - you won't be able to avail of some Jaeger specific features like contextualized logging since Zipkin only supports tags and time annotations 113 | - HTTP is your only choice of transport (no UDP option) 114 | 115 | We'll consider improving Jaeger support once its instrumetation matures. 116 | 117 | ### Null 118 | 119 | The package also includes `null` driver that discards created spans. 120 | 121 | ## Usage 122 | 123 | You will work with tracing via a `Trace` facade provided by this package. 124 | 125 | ### Creating Spans 126 | 127 | Starting new trace is as simple as calling `startSpan` method with name for a logical operation the span represents: 128 | 129 | ```php 130 | $span = Trace::startSpan('Create Order'); 131 | ``` 132 | 133 | Often, you need to continue an existing trace which is why `startSpan` also accepts additional parameter for span context. **SpanContext** may be propagated via various channels including HTTP requests, AMQP messages, arrays or even another span: 134 | 135 | ```php 136 | $spanContext = Trace::extract($request, Formats::ILLUMINATE_HTTP); 137 | 138 | $rootSpan = Trace::startSpan('Create Order', $spanContext); 139 | 140 | $childSpan = Trace::startSpan('Validate Order', $rootSpan->getContext()) 141 | ``` 142 | 143 | The possibilities are limitless. Refer to [Context Propagation](#context-propagation) section for more details. 144 | 145 | ### Customizing Spans 146 | 147 | Override span name: 148 | 149 | ```php 150 | $span->setName('Create Order'); 151 | ``` 152 | 153 | Add tags, which may be used as lookup keys (to search span on UI) or additional details: 154 | 155 | ```php 156 | $span->tag('shipping_method', $shipping_method); 157 | ``` 158 | 159 | ### Retrieving Spans 160 | 161 | You can retrieve the current span, which is also your most recently created span: 162 | 163 | ```php 164 | $span = Trace::getCurrentSpan() 165 | ``` 166 | 167 | The first span you create when processing a request in the service is called a root span (not to mix with the global root span of the trace): 168 | 169 | > After you call [flush](#flushing-spans), the root span is reset. 170 | 171 | ```php 172 | $span = Trace::getRootSpan() 173 | ``` 174 | 175 | ### Controlling Spans 176 | 177 | You may finish the span by calling `finish` on it. Span duration is derived by subtracting the start timestamp from this: 178 | 179 | ```php 180 | $span->finish() 181 | ``` 182 | 183 | You can log additional data between span start and finish. For example, `annotate` creates a time-stamped event to explain latencies: 184 | 185 | ```php 186 | $span->annotate('Order Validated') 187 | ``` 188 | 189 | ## Flushing Spans 190 | 191 | Flush refers to the process of sending all pending spans to the transport. It will also reset the state of the tracer including the active spans and UUID: 192 | 193 | ```php 194 | Trace::flush() 195 | ``` 196 | 197 | Most of the time though you don't need to explicitly call `flush`. Since PHP is designed to die after each request, we already handle finishing the root span and calling flush upon application shutdown for you. 198 | 199 | It's only when processing requests continuously in a loop (e.g. AMQP channels) that you must resort to calling `flush` manually. 200 | 201 | ### Logging 202 | 203 | Each root span is associated with a unique identifier that can be used to lookup its trace. It is recommended you include it as part of [context](https://github.com/laravel/framework/blob/v5.8.31/src/Illuminate/Foundation/Exceptions/Handler.php#L151) when reporting errors to bridge the gap between different parts of your monitoring stack: 204 | 205 | ```php 206 | // Illuminate\Foundation\Exceptions\Handler 207 | 208 | /** 209 | * Get the default context variables for logging. 210 | * 211 | * @return array 212 | */ 213 | protected function context() 214 | { 215 | return array_filter([ 216 | 'userId' => Auth::id(), 217 | 'uuid' => Trace::getUUID(), 218 | ]); 219 | } 220 | ``` 221 | 222 | [Custom drivers](#custom-drivers) may also support logging structured data (not available in Zipkin) which can be used for integrating tracing with a Log facade: 223 | 224 | ```php 225 | use Illuminate\Support\Facades\Event; 226 | 227 | /** 228 | * Bootstrap any application services. 229 | * 230 | * @return void 231 | */ 232 | public function boot() 233 | { 234 | Event::listen(MessageLogged::class, function (MessageLogged $e) { 235 | Tracer::getCurrentSpan()->log((array) $e); 236 | }); 237 | } 238 | ``` 239 | 240 | ### Middleware 241 | 242 | This package includes a `\Vinelab\Tracing\Middleware\TraceRequests` middleware to take care of continuing the trace from incoming HTTP request. 243 | 244 | You should register middleware class in the `$middleware` property of your `app/Http/Kernel.php` class. 245 | 246 | The middleware adds the following **tags** on a root span: 247 | - `type` (http) 248 | - `request_method` 249 | - `request_path` 250 | - `request_uri` 251 | - `request_headers` 252 | - `request_ip` 253 | - `request_input` 254 | - `response_status` 255 | - `response_headers` 256 | - `response_content` 257 | 258 | > Request and response body are only included for whitelisted content-types. See `logging.content_types` option in your `config/tracing.php`. 259 | 260 | You can override the default name of the span (which is `VERB /path/for/route`) in the controller: 261 | 262 | ```php 263 | Trace::getRootSpan()->setName('Create Order') 264 | ``` 265 | 266 | ### Console Commands 267 | 268 | > Lumen does not support this feature, but you can still create traces for commands manually using tracer instance. 269 | 270 | Let your console commsands be traced by adding `Vinelab\Tracing\Contracts\ShouldBeTraced` interface to your class. 271 | 272 | The container span will include the following tags: 273 | 274 | - `type` (cli) 275 | - `argv` 276 | 277 | The span will be named after the console command. You can override the default name of the span in the command itself: 278 | 279 | ```php 280 | Trace::getRootSpan()->setName('Mark Orders Expired') 281 | ``` 282 | 283 | ### Queue Jobs 284 | 285 | > Lumen does not support this feature, but you can still create traces for jobs manually using tracer instance. 286 | 287 | Let your queue jobs be traced by adding `Vinelab\Tracing\Contracts\ShouldBeTraced` interface to your job class. 288 | 289 | The container span will include the following tags: 290 | 291 | - `type` (queue) 292 | - `connection_name` (i.e. sync, redis etc.) 293 | - `queue_name` 294 | - `job_input` 295 | 296 | As the name implies, `job_input` allows you to view your job's contructor parameters as JSON. Serialization of objects to this JSON string can be controlled by implementing one of the following interfaces: `Arrayable`, `Jsonable`, `JsonSerializable`, or a `__toString` method. A fallback behavior is to print all your object's public properties. 297 | 298 | > Constructor arguments must be saved as a class property with the same name (see ProcessPodcast example below). 299 | 300 | The span will be named after the queue job class. You can override the default name of the span in the job itself: 301 | 302 | ```php 303 | app('tracing.queue.span')->setName('Process Podcast') 304 | ``` 305 | 306 | Note here that the queue span may not necessarily be the root span of the trace. You would usually want the queue to continue the trace from where it left of when the job was dispatched. You can achieve this by simply giving SpanContext to the job's constructor: 307 | 308 | ```php 309 | class ProcessPodcast implements ShouldQueue 310 | { 311 | use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; 312 | 313 | protected $podcast; 314 | 315 | protected $spanContext; 316 | 317 | public function __construct(Podcast $podcast, SpanContext $spanContext) 318 | { 319 | $this->podcast = $podcast; 320 | $this->spanContext = $spanContext; 321 | } 322 | 323 | public function handle(AudioProcessor $processor) 324 | { 325 | // Process uploaded podcast... 326 | } 327 | } 328 | ``` 329 | 330 | The job above can be dispatched like so: 331 | 332 | ```php 333 | ProcessPodcast::dispatch($podcast, Trace::getRootSpan()->getContext()); 334 | ``` 335 | 336 | The rest will be handled automatically. Note that SpanContext will be excluded from logged `job_input`. 337 | 338 | **This package doesn't automatically handle tracing of queued closures and queued event listeners.** You can still trace them manually by opening and closing spans. Improving support for these features might be considered for future versions of the package. 339 | 340 | ### Context Propagation 341 | 342 | As we talked about previously, the tracer understands how to inject and extract trace context across different applications (services). 343 | 344 | We have already seen the example of extracting trace from HTTP request: 345 | 346 | ```php 347 | $spanContext = Trace::extract($request, Formats::ILLUMINATE_HTTP); 348 | ``` 349 | 350 | Of course, you don't need to do this manually because this package already includes a [middleware](#middleware) to handle this for you, but the trace may not necessarily come from HTTP request. 351 | 352 | The second parameter is a format descriptor that tells us how to deserialize tracing headers from given carrier. By default, the following formats are supported: 353 | 354 | ```php 355 | use Vinelab\Tracing\Propagation\Formats; 356 | 357 | $spanContext = Trace::extract($carrier, Formats::TEXT_MAP); 358 | $spanContext = Trace::extract($carrier, Formats::PSR_REQUEST); 359 | $spanContext = Trace::extract($carrier, Formats::ILLUMINATE_HTTP); 360 | $spanContext = Trace::extract($carrier, Formats::AMQP); 361 | $spanContext = Trace::extract($carrier, Formats::GOOGLE_PUBSUB); 362 | ``` 363 | 364 | You may also add your own format using `registerExtractionFormat` method. 365 | 366 | ```php 367 | Trace::registerExtractionFormat("pubsub", new PubSubExtractor()); 368 | ``` 369 | 370 | The injection format must implement `Vinelab\Tracing\Contracts\Extractor`. Refer to default Zipkin implementation for example. 371 | 372 | ```php 373 | interface Extractor 374 | { 375 | public function extract($carrier): ?SpanContext; 376 | } 377 | ``` 378 | 379 | Naturally, you can also inject existing trace context from the **current span** into a given carrier so that another service can continue the trace: 380 | 381 | ```php 382 | $message = Trace::inject($message, Formats::AMQP); 383 | 384 | $channel->basic_publish($message, $this->exchangeName, $routingKey); 385 | ``` 386 | 387 | By default, the following formats are supported: 388 | 389 | ```php 390 | use Vinelab\Tracing\Propagation\Formats; 391 | 392 | $carrier = Trace::inject($carrier, Formats::TEXT_MAP); 393 | $carrier = Trace::inject($carrier, Formats::PSR_REQUEST); 394 | $carrier = Trace::inject($carrier, Formats::ILLUMINATE_HTTP); 395 | $carrier = Trace::inject($carrier, Formats::AMQP); 396 | $carrier = Trace::inject($carrier, Formats::GOOGLE_PUBSUB); 397 | $carrier = Trace::inject($carrier, Formats::VINELAB_HTTP); 398 | ``` 399 | 400 | You may also add your own format using `registerInjectionFormat` method. 401 | 402 | The injection format must implement `Vinelab\Tracing\Contracts\Injector`. Refer to default Zipkin implementation for example. 403 | 404 | ```php 405 | interface Injector 406 | { 407 | public function inject(SpanContext $spanContext, &$carrier): void; 408 | } 409 | ``` 410 | 411 | You can also use `injectContext` method if you need to pass span context explicitly: 412 | 413 | ```php 414 | $carrier = Trace::injectContext($carrier, Formats::TEXT_MAP, $span->getContext()); 415 | ``` 416 | 417 | --- 418 | **IMPORTANT**: You don't need to create a custom propagation format if you need to get something done quickly. You can always avail of the default `TEXT_MAP` format to inject or extract tracing headers from an associative array. 419 | 420 | ## Custom Drivers 421 | 422 | ### Writing New Driver 423 | 424 | New drivers must adhere to `Vinelab\Tracing\Contracts\Tracer` contract. Refer to the default ZipkinTracer imlementation for example. 425 | 426 | ```php 427 | use Vinelab\Tracing\Contracts\Extractor; 428 | use Vinelab\Tracing\Contracts\Injector; 429 | use Vinelab\Tracing\Contracts\Span; 430 | use Vinelab\Tracing\Contracts\SpanContext; 431 | 432 | public function startSpan(string $name, SpanContext $spanContext = null, ?int $timestamp = null): Span; 433 | public function getRootSpan(): ?Span; 434 | public function getCurrentSpan(): ?Span; 435 | public function getUUID(): ?string; 436 | public function extract($carrier, string $format): ?SpanContext; 437 | public function inject($carrier, string $format); 438 | public function injectContext($carrier, string $format, SpanContext $spanContext); 439 | public function registerExtractionFormat(string $format, Extractor $extractor): array; 440 | public function registerInjectionFormat(string $format, Injector $injector): array; 441 | public function flush(): void; 442 | ``` 443 | 444 | ### Registering New Driver 445 | 446 | Once you have written your custom driver, you may register it using the extend method of the `TracingDriverManager`. You should call the `extend` method from the `boot` method of your `AppServiceProvider` or any other service provider used by your application. For example, if you have written a `JaegerTracer`, you may register it like so: 447 | 448 | ```php 449 | use Vinelab\Tracing\TracingDriverManager; 450 | 451 | /** 452 | * Bootstrap any application services. 453 | * 454 | * @return void 455 | */ 456 | public function boot() 457 | { 458 | resolve(TracingDriverManager::class)->extend('jaeger', function () { 459 | return new JaegerTracer; 460 | }); 461 | } 462 | ``` 463 | 464 | Once your driver has been registered, you may specify it as your tracing driver in your environment variables: 465 | 466 | ``` 467 | TRACING_DRIVER=jaeger 468 | ``` 469 | 470 | ## Usage With Lumen 471 | 472 | You need to register service provider manually in `bootstrap/app.php` file: 473 | 474 | ```php 475 | $app->register(Vinelab\Tracing\TracingServiceProvider::class); 476 | ``` 477 | 478 | You should also register middleware in the same file: 479 | 480 | ```php 481 | $app->middleware([ 482 | Vinelab\Tracing\Middleware\TraceRequests::class, 483 | ]); 484 | ``` 485 | 486 | Add the following lines to the end of `public/index.php` file: 487 | 488 | ```php 489 | $tracer = app(Vinelab\Tracing\Contracts\Tracer::class); 490 | 491 | optional($tracer->getRootSpan())->finish(); 492 | $tracer->flush(); 493 | ``` 494 | 495 | Finally, you may also want to copy over `config/tracing.php` from this repo if you need to customize default settings. 496 | 497 | If you don't use facades in your Lumen project, you can resolve tracer instance from container like this: 498 | 499 | ```php 500 | use Vinelab\Tracing\Contracts\Tracer; 501 | 502 | app(Tracer::class)->startSpan('Create Order') 503 | ``` 504 | 505 | Note that Lumen currently doesn't support automatic tracing for console commands and jobs because it doesn't dispatch some events and terminating callbacks. However, you can still create traces manually where you need them. 506 | 507 | ## Integrations 508 | 509 | ### Lucid Architecture 510 | 511 | This package includes optional `Vinelab\Tracing\Integration\Concerns\TracesLucidArchitecture` trait to enable tracing for [Lucid projects](https://github.com/lucid-architecture/laravel-microservice): 512 | 513 | ```php 514 | class TracingServiceProvider extends ServiceProvider 515 | { 516 | use TracesLucidArchitecture; 517 | 518 | /** 519 | * Bootstrap any application services. 520 | * 521 | * @return void 522 | */ 523 | public function boot() 524 | { 525 | $this->traceLucidArchitecture(); 526 | } 527 | } 528 | ``` 529 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vinelab/tracing-laravel", 3 | "description": "Distributed tracing for Laravel made easy", 4 | "keywords": [ 5 | "laravel", 6 | "tracing", 7 | "zipkin", 8 | "jaeger", 9 | "lucid" 10 | ], 11 | "type": "library", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Alexander Diachenko", 16 | "email": "adiach3nko@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.1.3", 21 | "illuminate/bus": ">=5.5.0", 22 | "illuminate/console": ">=5.5.0", 23 | "illuminate/contracts": ">=5.5.0", 24 | "illuminate/http": ">=5.5.0", 25 | "illuminate/log": ">=5.5.0", 26 | "illuminate/container": ">=5.5.0", 27 | "illuminate/queue": ">=5.5.0", 28 | "illuminate/routing": ">=5.5.0", 29 | "illuminate/support": ">=5.5.0", 30 | "openzipkin/zipkin": "~2.0.2|~3.0", 31 | "psr/http-message": "~1.0", 32 | "ramsey/uuid": "~3.0|~4.0" 33 | }, 34 | "require-dev": { 35 | "google/cloud-pubsub": "^1.18", 36 | "guzzlehttp/psr7": "~1.0|~2.0", 37 | "mockery/mockery": "~1.0", 38 | "php-amqplib/php-amqplib": "~2.8|~3.0", 39 | "phpunit/phpunit": "~7.0|~8.0|~9.0", 40 | "vinelab/http": "~1.5", 41 | "lucid-arch/laravel-foundation": "^7.0|^8.0", 42 | "lucidarch/lucid": "^1.0" 43 | }, 44 | "suggest": { 45 | "php-amqplib/php-amqplib": "A pure PHP implementation of the AMQP protocol", 46 | "vinelab/http": "Fault-tolerant HTTP client for sending and receiving JSON and XML that we also support tracing for", 47 | "lucid-arch/laravel-foundation": "Base package for the legacy Lucid Architecture that we also support tracing for", 48 | "lucidarch/lucid": "Latest distribution package for the Lucid Architecture that we also support tracing for" 49 | }, 50 | "autoload": { 51 | "psr-4": { 52 | "Vinelab\\Tracing\\": "src/" 53 | } 54 | }, 55 | "autoload-dev": { 56 | "psr-4": { 57 | "Vinelab\\Tracing\\Tests\\": "tests/" 58 | } 59 | }, 60 | "extra": { 61 | "laravel": { 62 | "providers": [ 63 | "Vinelab\\Tracing\\TracingServiceProvider" 64 | ] 65 | } 66 | }, 67 | "config": { 68 | "sort-packages": true 69 | }, 70 | "prefer-stable": true 71 | } 72 | -------------------------------------------------------------------------------- /config/tracing.php: -------------------------------------------------------------------------------- 1 | env('TRACING_DRIVER', 'zipkin'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Service Name 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Use this to lookup your application (microservice) on a tracing dashboard. 25 | | 26 | */ 27 | 28 | 'service_name' => env('TRACING_SERVICE_NAME', 'example'), 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Middleware 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Configure settings for tracing HTTP requests. You can exclude certain paths 36 | | from tracing like '/horizon/api/*' (note that we can use wildcards), allow 37 | | headers to be logged or hide values for ones that have sensitive info. It 38 | | is also possible to specify content types for which you want to log 39 | | request and response bodies. 40 | | 41 | */ 42 | 43 | 'middleware' => [ 44 | 'excluded_paths' => [ 45 | // 46 | ], 47 | 48 | 'allowed_headers' => [ 49 | '*' 50 | ], 51 | 52 | 'sensitive_headers' => [ 53 | // 54 | ], 55 | 56 | 'sensitive_input' => [ 57 | // 58 | ], 59 | 60 | 'payload' => [ 61 | 'content_types' => [ 62 | 'application/json', 63 | ], 64 | ], 65 | ], 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Errors 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Whether you want to automatically tag span with error=true 73 | | to denote the operation represented by the Span has failed 74 | | when error message was logged 75 | | 76 | */ 77 | 78 | 'errors' => true, 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Zipkin 83 | |-------------------------------------------------------------------------- 84 | | 85 | | Configure settings for a zipkin driver like whether you want to use 86 | | 128-bit Trace IDs and what is the max value size for flushed span 87 | | tags in bytes. Values bigger than this amount will be discarded 88 | | but you will still see whether certain tag was reported or not. 89 | | 90 | */ 91 | 92 | 'zipkin' => [ 93 | 'host' => env('ZIPKIN_HOST', 'localhost'), 94 | 'port' => env('ZIPKIN_PORT', 9411), 95 | 'options' => [ 96 | '128bit' => false, 97 | 'max_tag_len' => 1048576, 98 | 'request_timeout' => 5, 99 | ], 100 | 'sampler_class' => \Zipkin\Samplers\BinarySampler::class, 101 | 'percentage_sampler_rate' => env('ZIPKIN_PERCENTAGE_SAMPLER_RATE', 0.2), 102 | ], 103 | 104 | ]; 105 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | workspace: 5 | tty: true 6 | build: 7 | context: resources/docker/workspace 8 | args: 9 | PUID: "${PUID:-1000}" 10 | PGID: "${PGID:-1000}" 11 | volumes: 12 | - ./:/var/www/html 13 | -------------------------------------------------------------------------------- /resources/docker/workspace/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0-cli-alpine 2 | 3 | ARG PUID=1000 4 | ARG PGID=1000 5 | 6 | RUN apk add --no-cache --virtual .build-deps \ 7 | # for extensions 8 | $PHPIZE_DEPS \ 9 | && \ 10 | apk add --no-cache \ 11 | bash \ 12 | # for composer 13 | unzip \ 14 | && \ 15 | docker-php-ext-install \ 16 | # for php-amqplib 17 | bcmath \ 18 | && \ 19 | apk del .build-deps 20 | 21 | COPY --from=composer /usr/bin/composer /usr/bin/composer 22 | 23 | # Add a non-root user to prevent files being created with root permissions on host machine. 24 | RUN addgroup -g ${PGID} user && \ 25 | adduser -u ${PUID} -G user -D user 26 | 27 | WORKDIR /var/www/html 28 | 29 | USER user 30 | -------------------------------------------------------------------------------- /src/Contracts/Extractor.php: -------------------------------------------------------------------------------- 1 | isRoot = $isRoot; 19 | } 20 | 21 | /** 22 | * Sets the string name for the logical operation this span represents. 23 | * 24 | * @param string $name 25 | */ 26 | public function setName(string $name): void 27 | { 28 | // 29 | } 30 | 31 | /** 32 | * Tags give your span context for search, viewing and analysis. For example, 33 | * a key "your_app.version" would let you lookup spans by version. 34 | * 35 | * @param string $key 36 | * @param string|null $value 37 | */ 38 | public function tag(string $key, ?string $value = null): void 39 | { 40 | // 41 | } 42 | 43 | /** 44 | * Notify that operation has finished. 45 | * Span duration is derived by subtracting the start 46 | * timestamp from this, and set when appropriate. 47 | * 48 | * @param int|null $timestamp intval(microtime(true) * 1000000) 49 | */ 50 | public function finish(?int $timestamp = null): void 51 | { 52 | // 53 | } 54 | 55 | /** 56 | * Associates an event that explains latency with a timestamp. 57 | * 58 | * @param string $message 59 | */ 60 | public function annotate(string $message): void 61 | { 62 | // 63 | } 64 | 65 | /** 66 | * Log structured data. Only supported in Jaeger instrumentation 67 | * despite it technically being a part of OpenTracing spec 68 | * 69 | * @param array $fields key:value pairs 70 | */ 71 | public function log(array $fields): void 72 | { 73 | // 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function isRoot(): bool 80 | { 81 | return $this->isRoot; 82 | } 83 | 84 | /** 85 | * @return SpanContext 86 | */ 87 | public function getContext(): SpanContext 88 | { 89 | return new NullSpanContext(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Drivers/Null/NullSpanContext.php: -------------------------------------------------------------------------------- 1 | rootSpan) { 38 | $span = new NullSpan(false); 39 | } else { 40 | $span = new NullSpan(true); 41 | $this->rootSpan = $span; 42 | } 43 | 44 | $this->currentSpan = $span; 45 | 46 | return $span; 47 | } 48 | 49 | /** 50 | * Retrieve the root span of the service 51 | * 52 | * @return Span|null 53 | */ 54 | public function getRootSpan(): ?Span 55 | { 56 | return $this->rootSpan; 57 | } 58 | 59 | /** 60 | * Retrieve the most recently activated span. 61 | * 62 | * @return Span|null 63 | */ 64 | public function getCurrentSpan(): ?Span 65 | { 66 | return $this->currentSpan; 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | public function getUUID(): ?string 73 | { 74 | return null; 75 | } 76 | 77 | /** 78 | * Extract span context from from a given carrier using the format descriptor 79 | * that tells tracer how to decode it from the carrier parameters 80 | * 81 | * @param mixed $carrier 82 | * @param string $format 83 | * @return SpanContext|null 84 | */ 85 | public function extract($carrier, string $format): ?SpanContext 86 | { 87 | return null; 88 | } 89 | 90 | /** 91 | * Implicitly inject current span context using the format descriptor that 92 | * tells how to encode trace info in the carrier parameters 93 | * 94 | * @param mixed $carrier 95 | * @param string $format 96 | * @return mixed $carrier 97 | */ 98 | public function inject($carrier, string $format) 99 | { 100 | return $carrier; 101 | } 102 | 103 | /** 104 | * Inject specified span context into a given carrier using the format descriptor 105 | * that tells how to encode trace info in the carrier parameters 106 | * 107 | * @param mixed $carrier 108 | * @param string $format 109 | * @param SpanContext $spanContext 110 | * @return mixed $carrier 111 | */ 112 | public function injectContext($carrier, string $format, SpanContext $spanContext) 113 | { 114 | return $carrier; 115 | } 116 | 117 | /** 118 | * Register extractor for new format 119 | * 120 | * @param string $format 121 | * @param Extractor $extractor 122 | * @return array 123 | */ 124 | public function registerExtractionFormat(string $format, Extractor $extractor): array 125 | { 126 | return []; 127 | } 128 | 129 | /** 130 | * Register injector for new format 131 | * 132 | * @param string $format 133 | * @param Injector $injector 134 | * @return array 135 | */ 136 | public function registerInjectionFormat(string $format, Injector $injector): array 137 | { 138 | return []; 139 | } 140 | 141 | /** 142 | * Calling this will flush any pending spans to the transport and reset the state of the tracer. 143 | * Make sure this method is called after the request is finished. 144 | */ 145 | public function flush(): void 146 | { 147 | $this->rootSpan = null; 148 | $this->currentSpan = null; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/Extractors/AMQPExtractor.php: -------------------------------------------------------------------------------- 1 | propagation->getExtractor($this->getGetter()); 28 | $context = $extract($carrier); 29 | 30 | if ($context instanceof TraceContext) { 31 | return new ZipkinSpanContext($context); 32 | } 33 | 34 | return null; 35 | } 36 | 37 | /** 38 | * @param Propagation $propagation 39 | * @return $this 40 | */ 41 | public function setPropagation(Propagation $propagation): self 42 | { 43 | $this->propagation = $propagation; 44 | return $this; 45 | } 46 | 47 | abstract protected function getGetter(): Getter; 48 | } 49 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/Injectors/AMQPInjector.php: -------------------------------------------------------------------------------- 1 | propagation->getInjector($this->getSetter()); 26 | $inject($spanContext->getRawContext(), $carrier); 27 | } 28 | 29 | /** 30 | * @param Propagation $propagation 31 | * @return $this 32 | */ 33 | public function setPropagation(Propagation $propagation): self 34 | { 35 | $this->propagation = $propagation; 36 | return $this; 37 | } 38 | 39 | abstract protected function getSetter(): Setter; 40 | } 41 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/Propagation/AMQP.php: -------------------------------------------------------------------------------- 1 | get_properties(), 'application_headers', new AMQPTable); 27 | 28 | return Arr::get($amqpTable->getNativeData(), strtolower($key)); 29 | } 30 | 31 | throw InvalidPropagationCarrier::forCarrier($carrier); 32 | } 33 | 34 | /** 35 | * Replaces a propagated key with the given value 36 | * 37 | * @param $carrier 38 | * @param string $key 39 | * @param string $value 40 | * @return void 41 | */ 42 | public function put(&$carrier, string $key, string $value): void 43 | { 44 | if ($key === '') { 45 | throw InvalidPropagationKey::forEmptyKey(); 46 | } 47 | 48 | if ($carrier instanceof AMQPMessage) { 49 | /** @var AMQPTable $amqpTable */ 50 | $amqpTable = Arr::get($carrier->get_properties(), 'application_headers', new AMQPTable); 51 | 52 | $amqpTable->set(strtolower($key), $value); 53 | 54 | $carrier->set('application_headers', $amqpTable); 55 | 56 | return; 57 | } 58 | 59 | throw InvalidPropagationCarrier::forCarrier($carrier); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/Propagation/GooglePubSub.php: -------------------------------------------------------------------------------- 1 | attribute(strtolower($key)); 25 | } 26 | 27 | throw InvalidPropagationCarrier::forCarrier($carrier); 28 | } 29 | 30 | /** 31 | * Replaces a propagated key with the given value 32 | * 33 | * @param $carrier 34 | * @param string $key 35 | * @param string $value 36 | * @return void 37 | */ 38 | public function put(&$carrier, string $key, string $value): void 39 | { 40 | if ($key === '') { 41 | throw InvalidPropagationKey::forEmptyKey(); 42 | } 43 | 44 | if (is_array($carrier)) { 45 | $lKey = strtolower($key); 46 | Arr::set($carrier, "attributes.$lKey", $value); 47 | return; 48 | } 49 | 50 | throw InvalidPropagationCarrier::forCarrier($carrier); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/Propagation/IlluminateHttp.php: -------------------------------------------------------------------------------- 1 | header(strtolower($key)); 24 | } 25 | 26 | throw InvalidPropagationCarrier::forCarrier($carrier); 27 | } 28 | 29 | /** 30 | * Replaces a propagated key with the given value 31 | * 32 | * @param mixed $carrier 33 | * @param string $key 34 | * @param string $value 35 | * @return void 36 | */ 37 | public function put(&$carrier, string $key, string $value): void 38 | { 39 | if ($key === '') { 40 | throw InvalidPropagationKey::forEmptyKey(); 41 | } 42 | 43 | if ($carrier instanceof Request) { 44 | $carrier->headers->set(strtolower($key), $value); 45 | return; 46 | } 47 | 48 | throw InvalidPropagationCarrier::forCarrier($carrier); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/Propagation/PsrRequest.php: -------------------------------------------------------------------------------- 1 | getHeader(strtolower($key)); 25 | return $headers ? $headers[0] : null; 26 | } 27 | 28 | throw InvalidPropagationCarrier::forCarrier($carrier); 29 | } 30 | 31 | /** 32 | * Replaces a propagated key with the given value 33 | * 34 | * @param mixed $carrier 35 | * @param string $key 36 | * @param string $value 37 | * @return void 38 | */ 39 | public function put(&$carrier, string $key, string $value): void 40 | { 41 | if ($key === '') { 42 | throw InvalidPropagationKey::forEmptyKey(); 43 | } 44 | 45 | if ($carrier instanceof RequestInterface) { 46 | $carrier = $carrier->withHeader(strtolower($key), $value); 47 | return; 48 | } 49 | 50 | throw InvalidPropagationCarrier::forCarrier($carrier); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/Propagation/VinelabHttp.php: -------------------------------------------------------------------------------- 1 | span = $span; 30 | $this->isRoot = $isRoot; 31 | } 32 | 33 | /** 34 | * Sets the string name for the logical operation this span represents. 35 | * 36 | * @param string $name 37 | */ 38 | public function setName(string $name): void 39 | { 40 | $this->span->setName($name); 41 | } 42 | 43 | /** 44 | * Tags give your span context for search, viewing and analysis. For example, 45 | * a key "your_app.version" would let you lookup spans by version. 46 | * 47 | * @param string $key 48 | * @param string|null $value 49 | */ 50 | public function tag(string $key, ?string $value = null): void 51 | { 52 | if (mb_strlen($value) > ZipkinTracer::getMaxTagLen()) { 53 | $value = sprintf("Value exceeds the maximum allowed length of %d bytes", ZipkinTracer::getMaxTagLen()); 54 | } 55 | 56 | $this->span->tag($key, $value ?? ''); 57 | } 58 | 59 | /** 60 | * Notify that operation has finished. 61 | * Span duration is derived by subtracting the start 62 | * timestamp from this, and set when appropriate. 63 | * 64 | * @param int|null $timestamp intval(microtime(true) * 1000000) 65 | */ 66 | public function finish(?int $timestamp = null): void 67 | { 68 | $this->span->finish($timestamp); 69 | } 70 | 71 | /** 72 | * Associates an event that explains latency with a timestamp. 73 | * 74 | * @param string $message 75 | */ 76 | public function annotate(string $message): void 77 | { 78 | $this->span->annotate($message, now()); 79 | } 80 | 81 | /** 82 | * Log structured data. Not supported in Zipkin instrumentation. 83 | * 84 | * @param array $fields key:value pairs 85 | */ 86 | public function log(array $fields): void 87 | { 88 | // 89 | } 90 | 91 | /** 92 | * @return bool 93 | */ 94 | public function isRoot(): bool 95 | { 96 | return $this->isRoot; 97 | } 98 | 99 | /** 100 | * @return SpanContext 101 | */ 102 | public function getContext(): SpanContext 103 | { 104 | return new ZipkinSpanContext($this->span->getContext()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/ZipkinSpanContext.php: -------------------------------------------------------------------------------- 1 | context = $spanContext; 22 | } 23 | 24 | /** 25 | * Returns underlying (original) span context. 26 | * 27 | * @return mixed 28 | */ 29 | public function getRawContext() 30 | { 31 | return $this->context; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Drivers/Zipkin/ZipkinTracer.php: -------------------------------------------------------------------------------- 1 | serviceName = $serviceName; 127 | $this->host = $host; 128 | $this->port = $port; 129 | $this->usesTraceId128bits = $usesTraceId128bits; 130 | $this->requestTimeout = $requestTimeout; 131 | $this->reporter = $reporter; 132 | $this->sampler = $sampler; 133 | } 134 | 135 | /** 136 | * @return int 137 | */ 138 | public static function getMaxTagLen(): int 139 | { 140 | return self::$maxTagLen; 141 | } 142 | 143 | /** 144 | * @param int $maxTagLen 145 | */ 146 | public static function setMaxTagLen(int $maxTagLen): void 147 | { 148 | self::$maxTagLen = $maxTagLen; 149 | } 150 | 151 | /** 152 | * Initialize tracer based on parameters provided during object construction 153 | * 154 | * @return Tracer 155 | */ 156 | public function init(): Tracer 157 | { 158 | $this->tracing = TracingBuilder::create() 159 | ->havingLocalEndpoint($this->createEndpoint()) 160 | ->havingTraceId128bits($this->usesTraceId128bits) 161 | ->havingSampler($this->createSampler()) 162 | ->havingReporter($this->createReporter()) 163 | ->build(); 164 | 165 | $this->registerDefaultExtractionFormats(); 166 | $this->registerDefaultInjectionFormats(); 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Start a new span based on a parent trace context. The context may come either from 173 | * external source (extracted from HTTP request, AMQP message, etc., see extract method) 174 | * or received from another span in this service. 175 | * 176 | * If parent context does not contain a trace, a new trace will be implicitly created. 177 | * 178 | * The first span you create in the service will be considered the root span. Calling 179 | * flush {@see ZipkinTracer::flush()} will unset the root span along with request uuid. 180 | * 181 | * @param string $name 182 | * @param SpanContext|null $spanContext 183 | * @param int|null $timestamp intval(microtime(true) * 1000000) 184 | * @return Span 185 | */ 186 | public function startSpan(string $name, ?SpanContext $spanContext = null, ?int $timestamp = null): Span 187 | { 188 | $rawSpan = $this->tracing->getTracer()->nextSpan($spanContext ? $spanContext->getRawContext() : null); 189 | 190 | if ($this->rootSpan) { 191 | $span = new ZipkinSpan($rawSpan, false); 192 | } else { 193 | $span = new ZipkinSpan($rawSpan, true); 194 | $this->rootSpan = $span; 195 | 196 | $this->uuid = Uuid::uuid1()->toString(); 197 | $span->tag('uuid', $this->uuid); 198 | } 199 | 200 | $this->currentSpan = $span; 201 | $span->setName($name); 202 | 203 | $rawSpan->start($timestamp); 204 | 205 | return $span; 206 | } 207 | 208 | /** 209 | * Retrieve the root span of the service 210 | * 211 | * @return Span|null 212 | */ 213 | public function getRootSpan(): ?Span 214 | { 215 | return $this->rootSpan; 216 | } 217 | 218 | /** 219 | * Retrieve the most recently activated span. 220 | * 221 | * @return Span|null 222 | */ 223 | public function getCurrentSpan(): ?Span 224 | { 225 | return $this->currentSpan; 226 | } 227 | 228 | /** 229 | * @return string 230 | */ 231 | public function getUUID(): ?string 232 | { 233 | return $this->uuid; 234 | } 235 | 236 | /** 237 | * Extract span context given the format that tells tracer how to decode the carrier 238 | * 239 | * @param mixed $carrier 240 | * @param string $format 241 | * @return SpanContext|null 242 | */ 243 | public function extract($carrier, string $format): ?SpanContext 244 | { 245 | return $this->resolveExtractor($format) 246 | ->setPropagation($this->tracing->getPropagation()) 247 | ->extract($carrier); 248 | } 249 | 250 | /** 251 | * Implicitly inject current span context using the format descriptor that 252 | * tells how to encode trace info in the carrier parameters 253 | * 254 | * @param mixed $carrier 255 | * @param string $format 256 | * @return mixed $carrier 257 | */ 258 | public function inject($carrier, string $format) 259 | { 260 | $span = $this->getCurrentSpan(); 261 | 262 | if ($span) { 263 | $this->resolveInjector($format) 264 | ->setPropagation($this->tracing->getPropagation()) 265 | ->inject($span->getContext(), $carrier); 266 | } 267 | 268 | return $carrier; 269 | } 270 | 271 | /** 272 | * Inject specified span context into a given carrier using the format descriptor 273 | * that tells how to encode trace info in the carrier parameters 274 | * 275 | * @param mixed $carrier 276 | * @param string $format 277 | * @param SpanContext $spanContext 278 | * @return mixed $carrier 279 | */ 280 | public function injectContext($carrier, string $format, SpanContext $spanContext) 281 | { 282 | $this->resolveInjector($format) 283 | ->setPropagation($this->tracing->getPropagation()) 284 | ->inject($spanContext, $carrier); 285 | 286 | return $carrier; 287 | } 288 | 289 | /** 290 | * Register extractor for new format 291 | * 292 | * @param string $format 293 | * @param Extractor $extractor 294 | * @return array 295 | */ 296 | public function registerExtractionFormat(string $format, Extractor $extractor): array 297 | { 298 | return Arr::set($this->extractionFormats, $format, $extractor); 299 | } 300 | 301 | /** 302 | * Register injector for new format 303 | * 304 | * @param string $format 305 | * @param Injector $injector 306 | * @return array 307 | */ 308 | public function registerInjectionFormat(string $format, Injector $injector): array 309 | { 310 | return Arr::set($this->injectionFormats, $format, $injector); 311 | } 312 | 313 | /** 314 | * Calling this will flush any pending spans to the transport and reset the state of the tracer. 315 | * Make sure this method is called after the request is finished. 316 | */ 317 | public function flush(): void 318 | { 319 | $this->tracing->getTracer()->flush(); 320 | $this->rootSpan = null; 321 | $this->currentSpan = null; 322 | $this->uuid = null; 323 | } 324 | 325 | /** 326 | * @return Reporter 327 | */ 328 | protected function createReporter(): Reporter 329 | { 330 | if (!$this->reporter) { 331 | $this->reporter = new HttpReporter([ 332 | 'endpoint_url' => sprintf('http://%s:%s/api/v2/spans', $this->host, $this->port), 333 | 'timeout' => $this->requestTimeout, 334 | ]); 335 | } 336 | 337 | return $this->reporter; 338 | } 339 | 340 | /** 341 | * @return Endpoint 342 | */ 343 | protected function createEndpoint(): Endpoint 344 | { 345 | if (strpos($this->host, ":") === false) { 346 | $ipv4 = filter_var($this->host, FILTER_VALIDATE_IP) ? $this->host : $this->resolveCollectorIp($this->host); 347 | 348 | return Endpoint::create($this->serviceName, $ipv4, null, $this->port); 349 | } 350 | 351 | return Endpoint::create($this->serviceName, null, $this->host, $this->port); 352 | } 353 | 354 | /** 355 | * @return Sampler 356 | */ 357 | protected function createSampler(): Sampler 358 | { 359 | if (!$this->sampler) { 360 | $this->sampler = BinarySampler::createAsAlwaysSample(); 361 | } 362 | 363 | return $this->sampler; 364 | } 365 | 366 | protected function registerDefaultExtractionFormats(): void 367 | { 368 | $this->registerExtractionFormat(Formats::TEXT_MAP, new TextMapExtractor()); 369 | $this->registerExtractionFormat(Formats::PSR_REQUEST, new PsrRequestExtractor()); 370 | $this->registerExtractionFormat(Formats::ILLUMINATE_HTTP, new IlluminateHttpExtractor()); 371 | $this->registerExtractionFormat(Formats::AMQP, new AMQPExtractor()); 372 | $this->registerExtractionFormat(Formats::GOOGLE_PUBSUB, new GooglePubSubExtractor()); 373 | } 374 | 375 | protected function registerDefaultInjectionFormats(): void 376 | { 377 | $this->registerInjectionFormat(Formats::TEXT_MAP, new TextMapInjector()); 378 | $this->registerInjectionFormat(Formats::PSR_REQUEST, new PsrRequestInjector()); 379 | $this->registerInjectionFormat(Formats::ILLUMINATE_HTTP, new IlluminateHttpInjector()); 380 | $this->registerInjectionFormat(Formats::AMQP, new AMQPInjector()); 381 | $this->registerInjectionFormat(Formats::VINELAB_HTTP, new VinelabHttpInjector()); 382 | $this->registerInjectionFormat(Formats::GOOGLE_PUBSUB, new GooglePubSubInjector()); 383 | } 384 | 385 | /** 386 | * @param string $format 387 | * @return ZipkinInjector 388 | */ 389 | protected function resolveInjector(string $format): ZipkinInjector 390 | { 391 | $injector = Arr::get($this->injectionFormats, $format); 392 | 393 | if (!$injector) { 394 | throw new UnregisteredFormatException("No injector registered for format $format"); 395 | } 396 | 397 | return $injector; 398 | } 399 | 400 | /** 401 | * @param string $format 402 | * @return ZipkinExtractor 403 | */ 404 | protected function resolveExtractor(string $format): ZipkinExtractor 405 | { 406 | $extractor = Arr::get($this->extractionFormats, $format); 407 | 408 | if (!$extractor) { 409 | throw new UnregisteredFormatException("No extractor registered for format $format"); 410 | } 411 | 412 | return $extractor; 413 | } 414 | 415 | /** 416 | * @param string $host 417 | * @return string 418 | */ 419 | protected function resolveCollectorIp(string $host): string 420 | { 421 | $ipv4 = gethostbyname($host); 422 | 423 | if ($ipv4 == $host) { 424 | $e = new UnresolvedCollectorIpException("Unable to resolve collector's IP address from hostname $host"); 425 | 426 | app('log')->debug($e->getMessage()); 427 | 428 | return "127.0.0.1"; 429 | } 430 | 431 | return $ipv4; 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/Exceptions/UnregisteredFormatException.php: -------------------------------------------------------------------------------- 1 | resolveLucidUnits(); 27 | 28 | $this->renameSpanAfterFeature(); 29 | $this->annotateRunningOperations(); 30 | $this->annotateRunningJobs(); 31 | } 32 | 33 | protected function resolveLucidUnits() 34 | { 35 | if (!class_exists(FeatureStarted::class)) { 36 | App::bind(FeatureStarted::class, function () { 37 | return LegacyFeatureStarted::class; 38 | }); 39 | } 40 | 41 | if (!class_exists(OperationStarted::class)) { 42 | App::bind(OperationStarted::class, function () { 43 | return LegacyOperationStarted::class; 44 | }); 45 | } 46 | 47 | if (!class_exists(JobStarted::class)) { 48 | App::bind(JobStarted::class, function () { 49 | return LegacyJobStarted::class; 50 | }); 51 | } 52 | } 53 | 54 | protected function renameSpanAfterFeature() 55 | { 56 | Event::listen(FeatureStarted::class, function (FeatureStarted $event) { 57 | if (in_array(ShouldQueue::class, class_implements($event->name))) { 58 | /* 59 | * If feature is queued using a "sync" driver, FeatureStarted event will be triggered twice 60 | * within a single request lifecycle: first time on dispatch and another time during execution. 61 | * We want to skip renaming the span if it's the former and rename queue span if it's the latter. 62 | */ 63 | if (App::has('tracing.queue.span')) { 64 | App::get('tracing.queue.span')->setName($event->name); 65 | } 66 | } else { 67 | optional(Trace::getRootSpan())->setName($event->name); 68 | } 69 | }); 70 | } 71 | 72 | protected function annotateRunningOperations() 73 | { 74 | Event::listen(OperationStarted::class, function (OperationStarted $event) { 75 | $this->annotateEvent($event->name); 76 | }); 77 | } 78 | 79 | protected function annotateRunningJobs() 80 | { 81 | Event::listen(JobStarted::class, function (JobStarted $event) { 82 | $this->annotateEvent($event->name); 83 | }); 84 | } 85 | 86 | /** 87 | * @param string $eventName 88 | */ 89 | private function annotateEvent(string $eventName) 90 | { 91 | if (App::has('tracing.queue.span')) { 92 | App::get('tracing.queue.span')->annotate($eventName); 93 | } else { 94 | optional(Trace::getRootSpan())->annotate($eventName); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Listeners/QueueJobSubscriber.php: -------------------------------------------------------------------------------- 1 | app = $app; 37 | } 38 | 39 | /** 40 | * @param JobProcessing $event 41 | */ 42 | public function onJobProcessing(JobProcessing $event) 43 | { 44 | $this->startQueueJobSpan($event); 45 | } 46 | 47 | /** 48 | * @param JobProcessed $event 49 | */ 50 | public function onJobProcessed(JobProcessed $event) 51 | { 52 | $this->closeQueueJobSpan($event); 53 | } 54 | 55 | /** 56 | * @param JobFailed $event 57 | */ 58 | public function onJobFailed(JobFailed $event) 59 | { 60 | $this->closeQueueJobSpan($event); 61 | } 62 | 63 | /** 64 | * @param JobProcessing $event 65 | */ 66 | protected function startQueueJobSpan(JobProcessing $event) 67 | { 68 | /** @var Job $job */ 69 | $job = $event->job; 70 | /** @var mixed $jobInstance */ 71 | $jobInstance = unserialize(Arr::get($job->payload(), 'data.command')); 72 | 73 | if ($jobInstance && in_array(ShouldBeTraced::class, class_implements($jobInstance))) { 74 | $jobInput = $this->retrieveQueueJobInput($jobInstance); 75 | 76 | /** @var Span $span */ 77 | $span = $this->app[Tracer::class]->startSpan(class_basename($job->resolveName()), $jobInput->first(function ($attr) { 78 | return $attr instanceof SpanContext; 79 | })); 80 | 81 | $span->tag('type', 'queue'); 82 | $span->tag('connection_name', $event->connectionName); 83 | $span->tag('queue_name', $jobInstance->queue); 84 | $span->tag('job_input', json_encode($this->normalizeQueueJobInputForLogging($jobInput))); 85 | 86 | $this->app->instance('tracing.queue.span', $span); 87 | } 88 | } 89 | 90 | /** 91 | * @param JobProcessed|JobFailed $event 92 | */ 93 | protected function closeQueueJobSpan($event) 94 | { 95 | if ($this->app->has('tracing.queue.span')) { 96 | if ($event instanceof JobFailed) { 97 | $this->app->get('tracing.queue.span')->tag('error', 'true'); 98 | $this->app->get('tracing.queue.span')->tag('error_message', $event->exception->getMessage()); 99 | } 100 | 101 | $this->app->get('tracing.queue.span')->finish(); 102 | 103 | // If it's a sync driver, the main process will flush, we don't want to do that prematurely 104 | if ($event->connectionName != 'sync') { 105 | $this->app[Tracer::class]->flush(); 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * @param mixed $jobInstance 112 | * @return Collection 113 | */ 114 | protected function retrieveQueueJobInput($jobInstance): Collection 115 | { 116 | $class = new ReflectionClass($jobInstance); 117 | $constructor = $class->getConstructor(); 118 | 119 | return collect(optional($constructor)->getParameters()) 120 | ->filter(function (ReflectionParameter $param) use ($class) { 121 | return $class->hasProperty($param->name); 122 | }) 123 | ->mapWithKeys(function (ReflectionParameter $param) use ($class, $jobInstance) { 124 | $prop = $class->getProperty($param->name); 125 | 126 | $prop->setAccessible(true); 127 | 128 | return [$param->name => $prop->getValue($jobInstance)]; 129 | }); 130 | } 131 | 132 | /** 133 | * @param Collection $jobInput 134 | * @return array 135 | */ 136 | protected function normalizeQueueJobInputForLogging(Collection $jobInput): array 137 | { 138 | return $jobInput 139 | ->reject(function ($attr) { 140 | return $attr instanceof SpanContext; 141 | }) 142 | ->map(function ($attr) { 143 | if (is_scalar($attr) || is_array($attr)) { 144 | return $attr; 145 | } 146 | 147 | if (is_object($attr) && $attr instanceof Arrayable) { 148 | return $attr->toArray(); 149 | } 150 | 151 | if (is_object($attr) && $attr instanceof Jsonable) { 152 | return $attr->toJson(); 153 | } 154 | 155 | if (is_object($attr) && $attr instanceof JsonSerializable) { 156 | return $attr->jsonSerialize(); 157 | } 158 | 159 | return $attr; 160 | }) 161 | ->toArray(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Listeners/TraceCommand.php: -------------------------------------------------------------------------------- 1 | tracer = $tracer; 33 | $this->artisan = $artisan; 34 | } 35 | 36 | /** 37 | * Handle the event. 38 | * 39 | * @param CommandStarting $event 40 | * @return void 41 | */ 42 | public function handle(CommandStarting $event) 43 | { 44 | if ($event->command && $this->shouldTraceCommand($event->command)) { 45 | $span = $this->tracer->startSpan("artisan {$event->command}"); 46 | 47 | $span->tag('type', 'cli'); 48 | $span->tag('argv', implode(PHP_EOL, $_SERVER['argv'])); 49 | } 50 | } 51 | 52 | /** 53 | * @param string $command 54 | * @return bool 55 | */ 56 | protected function shouldTraceCommand(string $command): bool 57 | { 58 | /** @var Command $command */ 59 | $command = Arr::get($this->artisan->all(), $command); 60 | 61 | if (!$command) { 62 | return false; 63 | } 64 | 65 | $interfaces = class_implements($command); 66 | 67 | return isset($interfaces[ShouldBeTraced::class]); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Middleware/TraceRequests.php: -------------------------------------------------------------------------------- 1 | tracer = $tracer; 37 | $this->config = $config; 38 | } 39 | 40 | /** 41 | * Handle an incoming request. 42 | * 43 | * @param Request $request 44 | * @param Closure $next 45 | * @return mixed 46 | */ 47 | public function handle(Request $request, Closure $next) 48 | { 49 | if (!$this->shouldBeExcluded($request->path())) { 50 | $spanContext = $this->tracer->extract($request, Formats::ILLUMINATE_HTTP); 51 | 52 | $span = $this->tracer->startSpan('Http Request', $spanContext); 53 | 54 | $this->tagRequestData($span, $request); 55 | } 56 | 57 | return $next($request); 58 | } 59 | 60 | /** 61 | * @param Request $request 62 | * @param Response|JsonResponse $response 63 | */ 64 | public function terminate(Request $request, $response) 65 | { 66 | $span = $this->tracer->getRootSpan(); 67 | 68 | if ($span) { 69 | $this->tagResponseData($span, $request, $response); 70 | 71 | $route = $request->route(); 72 | 73 | if ($this->isLaravelRoute($route)) { 74 | $span->setName(sprintf('%s %s', $request->method(), $request->route()->uri())); 75 | } 76 | 77 | if ($this->isLumenRoute($route)) { 78 | $routeUri = $this->getLumenRouteUri($request->path(), $route[2]); 79 | $span->setName(sprintf('%s %s', $request->method(), $routeUri)); 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * @param Request $request 86 | * @param Span $span 87 | */ 88 | protected function tagRequestData(Span $span, Request $request): void 89 | { 90 | $span->tag('type', 'http'); 91 | $span->tag('request_method', $request->method()); 92 | $span->tag('request_path', $request->path()); 93 | $span->tag('request_uri', $request->getRequestUri()); 94 | $span->tag('request_headers', $this->transformedHeaders($this->filterHeaders($request->headers))); 95 | $span->tag('request_ip', $request->ip()); 96 | 97 | if (in_array($request->headers->get('Content_Type'), $this->config->get('tracing.middleware.payload.content_types'))) { 98 | $span->tag('request_input', json_encode($this->filterInput($request->input()))); 99 | } 100 | } 101 | 102 | /** 103 | * @param Span $span 104 | * @param Request $request 105 | * @param Response|JsonResponse $response 106 | */ 107 | protected function tagResponseData(Span $span, Request $request, $response): void 108 | { 109 | if ($route = $request->route()) { 110 | if (method_exists($route, 'getActionName')) { 111 | $span->tag('laravel_action', $route->getActionName()); 112 | } 113 | } 114 | 115 | $span->tag('response_status', strval($response->getStatusCode())); 116 | $span->tag('response_headers', $this->transformedHeaders($this->filterHeaders($response->headers))); 117 | 118 | if (in_array($response->headers->get('Content_Type'), $this->config->get('tracing.middleware.payload.content_types'))) { 119 | $span->tag('response_content', $response->content()); 120 | } 121 | } 122 | 123 | /** 124 | * @param string $path 125 | * @return bool 126 | */ 127 | protected function shouldBeExcluded(string $path): bool 128 | { 129 | foreach ($this->config->get('tracing.middleware.excluded_paths') as $excludedPath) { 130 | if (Str::is($excludedPath, $path)) { 131 | return true; 132 | } 133 | } 134 | 135 | return false; 136 | } 137 | 138 | /** 139 | * @param HeaderBag $headers 140 | * @return HeaderBag 141 | */ 142 | protected function filterHeaders(HeaderBag $headers): array 143 | { 144 | return $this->hideSensitiveHeaders($this->filterAllowedHeaders(collect($headers)))->all(); 145 | } 146 | 147 | /** 148 | * @param Collection $headers 149 | * @return Collection 150 | */ 151 | protected function filterAllowedHeaders(Collection $headers): Collection 152 | { 153 | $allowedHeaders = $this->config->get('tracing.middleware.allowed_headers'); 154 | 155 | if (in_array('*', $allowedHeaders)) { 156 | return $headers; 157 | } 158 | 159 | $normalizedHeaders = array_map('strtolower', $allowedHeaders); 160 | 161 | return $headers->filter(function ($value, $name) use ($normalizedHeaders) { 162 | return in_array($name, $normalizedHeaders); 163 | }); 164 | } 165 | 166 | protected function hideSensitiveHeaders(Collection $headers): Collection 167 | { 168 | $sensitiveHeaders = $this->config->get('tracing.middleware.sensitive_headers'); 169 | 170 | $normalizedHeaders = array_map('strtolower', $sensitiveHeaders); 171 | 172 | $headers->transform(function ($value, $name) use ($normalizedHeaders) { 173 | return in_array($name, $normalizedHeaders) 174 | ? ['This value is hidden because it contains sensitive info'] 175 | : $value; 176 | }); 177 | 178 | return $headers; 179 | } 180 | 181 | /** 182 | * @param array $headers 183 | * @return string 184 | */ 185 | protected function transformedHeaders(array $headers = []): string 186 | { 187 | if (!$headers) { 188 | return ''; 189 | } 190 | 191 | ksort($headers); 192 | $max = max(array_map('strlen', array_keys($headers))) + 1; 193 | 194 | $content = ''; 195 | foreach ($headers as $name => $values) { 196 | $name = implode('-', array_map('ucfirst', explode('-', $name))); 197 | 198 | foreach ($values as $value) { 199 | $content .= sprintf("%-{$max}s %s\r\n", $name.':', $value); 200 | } 201 | } 202 | 203 | return $content; 204 | } 205 | 206 | /** 207 | * @param array $input 208 | * @return array 209 | */ 210 | protected function filterInput(array $input = []): array 211 | { 212 | return $this->hideSensitiveInput(collect($input))->all(); 213 | } 214 | 215 | /** 216 | * @param Collection $input 217 | * @return Collection 218 | */ 219 | protected function hideSensitiveInput(Collection $input): Collection 220 | { 221 | $sensitiveInput = $this->config->get('tracing.middleware.sensitive_input'); 222 | 223 | $normalizedInput = array_map('strtolower', $sensitiveInput); 224 | 225 | $input->transform(function ($value, $name) use ($normalizedInput) { 226 | return in_array($name, $normalizedInput) 227 | ? ['This value is hidden because it contains sensitive info'] 228 | : $value; 229 | }); 230 | 231 | return $input; 232 | } 233 | 234 | /** 235 | * @param $route 236 | * @return bool 237 | */ 238 | protected function isLaravelRoute($route): bool 239 | { 240 | return $route && method_exists($route, 'uri'); 241 | } 242 | 243 | /** 244 | * @param $route 245 | * @return bool 246 | */ 247 | protected function isLumenRoute($route): bool 248 | { 249 | return is_array($route) && is_array($route[2]); 250 | } 251 | 252 | /** 253 | * @param string $path 254 | * @param array $parameters 255 | * @return string 256 | */ 257 | protected function getLumenRouteUri(string $path, array $parameters): string 258 | { 259 | $replaceMap = array_combine( 260 | array_values($parameters), 261 | array_map(function ($v) { return '{'.$v.'}'; }, array_keys($parameters)) 262 | ); 263 | 264 | return strtr($path, $replaceMap); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/Propagation/Formats.php: -------------------------------------------------------------------------------- 1 | config = $app->make('config'); 29 | } 30 | 31 | /** 32 | * Get the default driver name. 33 | * 34 | * @return string 35 | */ 36 | public function getDefaultDriver() 37 | { 38 | if (is_null($this->config->get('tracing.driver'))) { 39 | return 'null'; 40 | } 41 | 42 | return $this->config->get('tracing.driver'); 43 | } 44 | 45 | /** 46 | * Create an instance of Zipkin tracing engine 47 | * 48 | * @return ZipkinTracer|Tracer 49 | * @throws InvalidArgumentException 50 | */ 51 | public function createZipkinDriver() 52 | { 53 | $tracer = new ZipkinTracer( 54 | $this->config->get('tracing.service_name'), 55 | $this->config->get('tracing.zipkin.host'), 56 | $this->config->get('tracing.zipkin.port'), 57 | $this->config->get('tracing.zipkin.options.128bit'), 58 | $this->config->get('tracing.zipkin.options.request_timeout', 5), 59 | null, 60 | $this->getZipkinSampler() 61 | ); 62 | 63 | ZipkinTracer::setMaxTagLen( 64 | $this->config->get('tracing.zipkin.options.max_tag_len', ZipkinTracer::getMaxTagLen()) 65 | ); 66 | 67 | return $tracer->init(); 68 | } 69 | 70 | public function createNullDriver() 71 | { 72 | return new NullTracer(); 73 | } 74 | 75 | /** 76 | * @return BinarySampler|PercentageSampler 77 | * @throws InvalidArgumentException 78 | */ 79 | protected function getZipkinSampler() 80 | { 81 | $samplerClassName = $this->config->get('tracing.zipkin.sampler_class'); 82 | if (!class_exists($samplerClassName)) { 83 | throw new InvalidArgumentException( 84 | \sprintf('Invalid sampler class. Expected `BinarySampler` or `PercentageSampler`, got %f', $samplerClassName) 85 | ); 86 | } 87 | 88 | switch ($samplerClassName) { 89 | case BinarySampler::class: 90 | $sampler = BinarySampler::createAsAlwaysSample(); 91 | break; 92 | case PercentageSampler::class: 93 | $sampler = PercentageSampler::create($this->config->get('tracing.zipkin.percentage_sampler_rate')); 94 | break; 95 | } 96 | 97 | return $sampler; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/TracingServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole() && function_exists('config_path')) { 26 | $this->publishes([ 27 | __DIR__.'/../config/tracing.php' => config_path('tracing.php'), 28 | ]); 29 | } 30 | 31 | $this->app['events']->listen(CommandStarting::class, TraceCommand::class); 32 | 33 | $this->app['events']->listen( 34 | JobProcessing::class, 35 | 'Vinelab\Tracing\Listeners\QueueJobSubscriber@onJobProcessing' 36 | ); 37 | $this->app['events']->listen( 38 | JobProcessed::class, 39 | 'Vinelab\Tracing\Listeners\QueueJobSubscriber@onJobProcessed' 40 | ); 41 | $this->app['events']->listen( 42 | JobFailed::class, 43 | 'Vinelab\Tracing\Listeners\QueueJobSubscriber@onJobFailed' 44 | ); 45 | 46 | if ($this->app['config']['tracing.errors']) { 47 | $this->app['events']->listen(MessageLogged::class, function (MessageLogged $event) { 48 | if ($event->level == 'error') { 49 | optional(Trace::getRootSpan())->tag('error', 'true'); 50 | optional(Trace::getRootSpan())->tag('error_message', $event->message); 51 | } 52 | }); 53 | } 54 | 55 | if (method_exists($this->app, 'terminating')) { 56 | $this->app->terminating(function () { 57 | optional(Trace::getRootSpan())->finish(); 58 | Trace::flush(); 59 | }); 60 | } 61 | } 62 | 63 | /** 64 | * Register any application services. 65 | * 66 | * @return void 67 | */ 68 | public function register() 69 | { 70 | $this->mergeConfigFrom( dirname(__DIR__).'/config/tracing.php', 'tracing'); 71 | 72 | $this->app->singleton(TracingDriverManager::class, function ($app) { 73 | return new TracingDriverManager($app); 74 | }); 75 | 76 | $this->app->singleton(Tracer::class, function ($app) { 77 | return $app->make(TracingDriverManager::class)->driver($this->app['config']['tracing.driver']); 78 | }); 79 | } 80 | } 81 | --------------------------------------------------------------------------------