├── .github └── workflows │ ├── phpunit.yml │ └── psalm.yml ├── .gitignore ├── LICENSE ├── README.md ├── changelog.md ├── composer.json ├── config └── jaeger.php ├── phpunit.xml.dist ├── psalm.xml.dist ├── src ├── Jaeger.php ├── JaegerMiddleware.php ├── LaravelJaegerServiceProvider.php └── Listeners │ ├── CommandListener.php │ ├── JobListener.php │ └── QueryListener.php └── tests └── TestCase.php /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest, windows-latest] 12 | php: [8.0] 13 | laravel: [9.*] 14 | dependency-version: [prefer-stable] 15 | include: 16 | - laravel: 9.* 17 | testbench: 7.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v2 24 | 25 | - name: Cache dependencies 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.composer/cache/files 29 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 30 | 31 | - name: Setup PHP 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: ${{ matrix.php }} 35 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 36 | coverage: none 37 | 38 | - name: Install dependencies 39 | run: | 40 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --ignore-platform-reqs 41 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --ignore-platform-reqs 42 | 43 | - name: Execute tests 44 | run: vendor/bin/phpunit 45 | -------------------------------------------------------------------------------- /.github/workflows/psalm.yml: -------------------------------------------------------------------------------- 1 | name: Psalm 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.php' 7 | - 'psalm.xml.dist' 8 | 9 | jobs: 10 | psalm: 11 | name: psalm 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: '8.0' 20 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 21 | coverage: none 22 | 23 | - name: Cache composer dependencies 24 | uses: actions/cache@v2 25 | with: 26 | path: vendor 27 | key: composer-${{ hashFiles('composer.lock') }} 28 | 29 | - name: Run composer install 30 | run: composer install -n --prefer-dist 31 | 32 | - name: Run psalm 33 | run: ./vendor/bin/psalm --shepherd --stats 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .phpunit.result.cache 3 | build 4 | composer.lock 5 | coverage 6 | phpunit.xml 7 | psalm.xml 8 | vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Chocofamily 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Jaeger wrapper 2 | 3 | 4 | ## Requirements 5 | 6 | - PHP ^8.0 7 | - Laravel ^9.0 8 | 9 | ## Installation 10 | 11 | You can install the package via composer: 12 | 13 | ```bash 14 | composer require chocofamilyme/laravel-jaeger 15 | ``` 16 | 17 | You can publish the config file with: 18 | ```bash 19 | php artisan vendor:publish --provider="Chocofamilyme\LaravelJaeger\LaravelJaegerServiceProvider" --tag="config" 20 | ``` 21 | 22 | ## Basic Usage 23 | 24 | 1) You need to inject `\Chocofamilyme\LaravelJaeger\Jaeger` class by DI 25 | 2) Start new span by command 26 | ```php 27 | $jaeger->start('Some operation', [ 28 | 'tag1' => 'test', 29 | 'tag2' => 'test' 30 | ]); 31 | ``` 32 | 3) do some stuff 33 | 4) (optional) stop span 34 | ```php 35 | $jaeger->stop('Some operation', [ 36 | 'tag3' => 'test', 37 | ]); 38 | 39 | ``` 40 | 41 | All unstopped spans will be automatically stopped when application is terminated 42 | 43 | ### Controlling the rate of traces 44 | 45 | In the configuration file you may modify *JAEGER_SAMPLE_RATE* variable 46 | to configure the rate. The variable accepts values from 0 to 1. 47 | 48 | For example, if you set 0.1 then only 10% of all traces is displayed. 49 | Set 1 to output them all. 50 | 51 | ## Listeners 52 | 53 | There are 4 available listeners, they are disabled by default, you can turn on or write your own implementation for this listeners in config file 54 | 55 | ```php 56 | 'listeners' => [ 57 | 'http' => [ 58 | 'enabled' => env('JAEGER_HTTP_LISTENER_ENABLED', false), 59 | 'handler' => \Chocofamilyme\LaravelJaeger\JaegerMiddleware::class, 60 | ], 61 | 'console' => [ 62 | 'enabled' => env('JAEGER_CONSOLE_LISTENER_ENABLED', false), 63 | 'handler' => \Chocofamilyme\LaravelJaeger\Listeners\CommandListener::class, 64 | ], 65 | 'query' => [ 66 | 'enabled' => env('JAEGER_QUERY_LISTENER_ENABLED', false), 67 | 'handler' => \Chocofamilyme\LaravelJaeger\Listeners\QueryListener::class, 68 | ], 69 | 'job' => [ 70 | 'enabled' => env('JAEGER_JOB_LISTENER_ENABLED', false), 71 | 'handler' => \Chocofamilyme\LaravelJaeger\Listeners\JobListener::class, 72 | ], 73 | ] 74 | ``` 75 | 76 | - Http - Start new span for every http request 77 | - Console - Start new span for every running artisan console commands 78 | - Query - Start new span for every executed database query 79 | - Job - Start new span for every dispatched queue job 80 | 81 | ## Testing 82 | 83 | ``` bash 84 | composer test 85 | ``` 86 | 87 | ## Changelog 88 | 89 | Read changelog [here](/changelog.md) 90 | 91 | ## License 92 | 93 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 94 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 4 | 5 | - Remove jukylin/jaeger-php dependency (switch to jonahgeorge/jaeger-client-php) 6 | - Add startWithInject method 7 | - Add possibility to configure sampler type and dispatch mode (see config/jaeger.php) 8 | - Switch to Zipkin over compact udp transport by default 9 | 10 | 11 | Upgrade from v1 12 | 13 | - You need to copy config/jaeger.php from v2 to your local project, and change it according your needs -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chocofamilyme/laravel-jaeger", 3 | "description": "Jaeger wrapper for Laravel", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Aidyn Makhataev", 9 | "email": "makataev.7@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.0", 14 | "ext-json": "*", 15 | "jonahgeorge/jaeger-client-php": "^1.4", 16 | "laravel/framework": "^9.0" 17 | }, 18 | "require-dev": { 19 | "orchestra/testbench": "^7.0", 20 | "phpunit/phpunit": "^9.5", 21 | "vimeo/psalm": "^4.6.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Chocofamilyme\\LaravelJaeger\\": "src" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Chocofamilyme\\LaravelJaeger\\Tests\\": "tests" 31 | } 32 | }, 33 | "scripts": { 34 | "psalm": "vendor/bin/psalm", 35 | "test": "vendor/bin/phpunit --colors=always", 36 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 37 | }, 38 | "config": { 39 | "sort-packages": true, 40 | "allow-plugins": { 41 | "composer/package-versions-deprecated": false 42 | } 43 | }, 44 | "minimum-stability": "dev", 45 | "prefer-stable": true, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Chocofamilyme\\LaravelJaeger\\LaravelJaegerServiceProvider" 50 | ] 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/jaeger.php: -------------------------------------------------------------------------------- 1 | env('JAEGER_SERVICE_NAME', env('APP_NAME', 'Laravel')), 7 | 8 | 'config' => [ 9 | 'sampler' => [ 10 | 'type' => \Jaeger\SAMPLER_TYPE_PROBABILISTIC, 11 | 'param' => env('JAEGER_SAMPLE_RATE', 0.1), 12 | ], 13 | 'local_agent' => [ 14 | 'reporting_host' => env('JAEGER_HOST', 'jaeger'), 15 | 'reporting_port' => env('JAEGER_PORT', 5775), 16 | ], 17 | 'dispatch_mode' => Config::ZIPKIN_OVER_COMPACT_UDP, 18 | ], 19 | 20 | 'listeners' => [ 21 | 'http' => [ 22 | 'enabled' => env('JAEGER_HTTP_LISTENER_ENABLED', false), 23 | 'handler' => \Chocofamilyme\LaravelJaeger\JaegerMiddleware::class, 24 | ], 25 | 'console' => [ 26 | 'enabled' => env('JAEGER_CONSOLE_LISTENER_ENABLED', false), 27 | 'handler' => \Chocofamilyme\LaravelJaeger\Listeners\CommandListener::class, 28 | ], 29 | 'query' => [ 30 | 'enabled' => env('JAEGER_QUERY_LISTENER_ENABLED', false), 31 | 'handler' => \Chocofamilyme\LaravelJaeger\Listeners\QueryListener::class, 32 | ], 33 | 'job' => [ 34 | 'enabled' => env('JAEGER_JOB_LISTENER_ENABLED', false), 35 | 'handler' => \Chocofamilyme\LaravelJaeger\Listeners\JobListener::class, 36 | ], 37 | ], 38 | ]; -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | 20 | ./src 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Jaeger.php: -------------------------------------------------------------------------------- 1 | tracer = $tracer; 26 | $this->spans = new \SplStack(); 27 | } 28 | 29 | public function __destruct() 30 | { 31 | $this->finish(); 32 | } 33 | 34 | public function tracer(): Tracer 35 | { 36 | return $this->tracer; 37 | } 38 | 39 | public function startWithInject(string $operationName, array &$carrier, array $tags = []): Span 40 | { 41 | $span = $this->start($operationName, $tags); 42 | 43 | $this->tracer->inject($span->getContext(), TEXT_MAP, $carrier); 44 | 45 | return $span; 46 | } 47 | 48 | public function start(string $operationName, array $tags = []): Span 49 | { 50 | if ($this->spans->isEmpty()) { 51 | $span = $this->startSpan($operationName, $this->serverContext); 52 | } else { 53 | /** @var Span $parentSpan */ 54 | $parentSpan = $this->spans->top(); 55 | $span = $this->startSpan($operationName, $parentSpan->getContext()); 56 | } 57 | 58 | if ($tags) { 59 | foreach ($tags as $key => $value) { 60 | $span->setTag($key, $value); 61 | } 62 | } 63 | 64 | $this->spans->push($span); 65 | 66 | return $span; 67 | } 68 | 69 | public function stop(string $operationName, array $tags = []): void 70 | { 71 | if ($this->spans->isEmpty()) { 72 | return ; 73 | } 74 | 75 | $span = $this->spans->top(); 76 | 77 | /** @var Span $span */ 78 | if (strcmp($span->getOperationName(), $operationName) === 0) { 79 | foreach ($tags as $key => $value) { 80 | $span->setTag($key, $value); 81 | } 82 | $span->finish(); 83 | $this->spans->pop(); 84 | } 85 | } 86 | 87 | public function startStop(string $operationName, array $tags = [], ?float $duration = 0): void 88 | { 89 | $currentTime = microtime(true); 90 | 91 | $startTime = $currentTime - $duration; 92 | 93 | if ($this->spans->isEmpty()) { 94 | $span = $this->startSpan($operationName, $this->serverContext, $startTime); 95 | } else { 96 | /** @var Span $parentSpan */ 97 | $parentSpan = $this->spans->top(); 98 | $span = $this->startSpan($operationName, $parentSpan->getContext(), $startTime); 99 | } 100 | 101 | if ($tags) { 102 | foreach ($tags as $key => $value) { 103 | $span->setTag($key, $value); 104 | } 105 | } 106 | 107 | $span->finish((int)($currentTime * 1000000)); 108 | } 109 | 110 | public function inject(array &$carrier): void 111 | { 112 | if ($this->getCurrentSpan() === null) { 113 | throw new \RuntimeException('Can not inject, there is no available span'); 114 | } 115 | 116 | $this->tracer->inject( 117 | $this->getCurrentSpan()->getContext(), 118 | TEXT_MAP, 119 | $carrier, 120 | ); 121 | } 122 | 123 | public function getCurrentSpan(): ?Span 124 | { 125 | if ($this->spans->isEmpty()) { 126 | return null; 127 | } 128 | 129 | return $this->spans->top(); 130 | } 131 | 132 | public function initServerContext(array $carrier = null): ?SpanContext 133 | { 134 | $this->isFinished = false; 135 | 136 | if (!$carrier) { 137 | $context = $this->tracer->extract(TEXT_MAP, $_SERVER); 138 | } else { 139 | $context = $this->tracer->extract(TEXT_MAP, $carrier); 140 | } 141 | 142 | $this->serverContext = $context; 143 | 144 | return $this->serverContext; 145 | } 146 | 147 | 148 | public function finish(): void 149 | { 150 | if ($this->isFinished) { 151 | return; 152 | } 153 | 154 | try { 155 | $this->finishSpans(); 156 | $this->tracer->flush(); 157 | } catch (\Throwable $e) { 158 | } 159 | 160 | $this->isFinished = true; 161 | } 162 | 163 | private function finishSpans(): void 164 | { 165 | while (false === $this->spans->isEmpty()) { 166 | /** @var Span $span */ 167 | $span = $this->spans->pop(); 168 | 169 | $span->finish(); 170 | } 171 | } 172 | 173 | /** 174 | * @param string $operationName 175 | * @param SpanContext|null $context 176 | * @param float|null $startTime 177 | * 178 | * @return Span 179 | */ 180 | private function startSpan(string $operationName, SpanContext $context = null, float $startTime = null): Span 181 | { 182 | $options = []; 183 | 184 | if ($context !== null) { 185 | $options['child_of'] = $context; 186 | } 187 | 188 | if ($startTime !== null) { 189 | $options['start_time'] = $startTime; 190 | } 191 | 192 | return $this->tracer->startSpan($operationName, $options); 193 | } 194 | 195 | public function getTraceId(): ?string 196 | { 197 | if (false === $this->spans->isEmpty()) { 198 | $spanParent = $this->spans->top(); 199 | 200 | if ($spanParent instanceof \Jaeger\Span) { 201 | /** @psalm-suppress UndefinedInterfaceMethod */ 202 | return $spanParent->getContext()->getTraceId(); 203 | } 204 | } 205 | 206 | return null; 207 | } 208 | 209 | public function getRootTraceId(): ?string 210 | { 211 | $serverSpan = $this->serverContext; 212 | 213 | if ($serverSpan instanceof \Jaeger\SpanContext) { 214 | return $serverSpan->getTraceId(); 215 | } 216 | 217 | return null; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/JaegerMiddleware.php: -------------------------------------------------------------------------------- 1 | jaeger = $jaeger; 19 | } 20 | 21 | /** 22 | * @param Request $request 23 | * @param Closure $next 24 | * @return mixed 25 | */ 26 | public function handle($request, Closure $next) 27 | { 28 | $route = Route::getRoutes()->match($request); 29 | 30 | if ($route->isFallback) { 31 | return $next($request); 32 | } 33 | 34 | $httpMethod = $request->method(); 35 | $uri = $route->uri(); 36 | 37 | $headers = []; 38 | 39 | foreach ($request->headers->all() as $key => $value) { 40 | $headers[$key] = Arr::first($value); 41 | } 42 | 43 | $jaeger = $this->jaeger; 44 | 45 | $jaeger->initServerContext($headers); 46 | $jaeger->start("$httpMethod: /$uri", [ 47 | 'http.scheme' => $request->getScheme(), 48 | 'http.ip_address' => $request->ip(), 49 | 'http.host' => $request->getHost(), 50 | 'laravel.version' => app()->version(), 51 | ]); 52 | 53 | return $next($request); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/LaravelJaegerServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 24 | __DIR__ . '/../config/jaeger.php', 'jaeger' 25 | ); 26 | } 27 | 28 | public function boot(): void 29 | { 30 | $this->publishes([ 31 | __DIR__ . '/../config/jaeger.php' => $this->app->configPath('jaeger.php'), 32 | ], 'config'); 33 | 34 | $this->app->singleton(Jaeger::class, static function () { 35 | $config = new Config( 36 | config('jaeger.config'), 37 | config('jaeger.service_name'), 38 | ); 39 | 40 | $config->initializeTracer(); 41 | 42 | $client = GlobalTracer::get(); 43 | 44 | return new Jaeger($client); 45 | }); 46 | 47 | app()->terminating(function () { 48 | app(Jaeger::class)->finish(); 49 | }); 50 | 51 | $this->initHttp(); 52 | $this->initConsole(); 53 | $this->initQuery(); 54 | $this->initJob(); 55 | } 56 | 57 | private function initHttp(): void 58 | { 59 | if (config('jaeger.listeners.http.enabled') && false === $this->app->runningInConsole()) { 60 | $router = $this->app->get('router'); 61 | $router->middleware( 62 | config('jaeger.listeners.http.handler') 63 | ); 64 | 65 | /** @var Kernel $kernel */ 66 | $kernel = $this->app->get(\Illuminate\Contracts\Http\Kernel::class); 67 | $kernel->pushMiddleware( 68 | config('jaeger.listeners.http.handler') 69 | ); 70 | } 71 | } 72 | 73 | private function initConsole(): void 74 | { 75 | if (config('jaeger.listeners.console.enabled') && $this->app->runningInConsole()) { 76 | Event::listen(CommandStarting::class, config('jaeger.listeners.console.handler')); 77 | Event::listen(CommandFinished::class, config('jaeger.listeners.console.handler')); 78 | } 79 | } 80 | 81 | private function initQuery(): void 82 | { 83 | if (config('jaeger.listeners.query.enabled')) { 84 | Event::listen(QueryExecuted::class, config('jaeger.listeners.query.handler')); 85 | } 86 | } 87 | 88 | private function initJob(): void 89 | { 90 | if (config('jaeger.listeners.job.enabled')) { 91 | Event::listen(JobProcessing::class, config('jaeger.listeners.job.handler')); 92 | Event::listen(JobProcessed::class, config('jaeger.listeners.job.handler')); 93 | Event::listen(JobFailed::class, config('jaeger.listeners.job.handler')); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/Listeners/CommandListener.php: -------------------------------------------------------------------------------- 1 | jaeger = $jaeger; 19 | } 20 | 21 | public function handle($event): void 22 | { 23 | if ($event instanceof CommandStarting) { 24 | $command = $event->command ?? $event->input->getArguments()['command'] ?? 'default'; 25 | 26 | self::$operationName = "Console command: php artisan $command"; 27 | 28 | $this->jaeger->start(self::$operationName, [ 29 | 'console.arguments' => json_encode($event->input->getArguments(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), 30 | 'console.options' => json_encode($event->input->getOptions(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), 31 | ]); 32 | 33 | return; 34 | } 35 | 36 | if ($event instanceof CommandFinished) { 37 | $command = $event->command ?? $event->input->getArguments()['command'] ?? 'default'; 38 | 39 | $operationName = self::$operationName ?? "Console command: php artisan $command"; 40 | 41 | $this->jaeger->stop($operationName, [ 42 | 'console.exit_code' => (string) $event->exitCode, 43 | ]); 44 | 45 | return; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Listeners/JobListener.php: -------------------------------------------------------------------------------- 1 | jaeger = $jaeger; 20 | } 21 | 22 | 23 | public function handle($event): void 24 | { 25 | if ($event instanceof JobProcessing) { 26 | self::$operationName = "Job {$event->job->resolveName()}"; 27 | 28 | $this->jaeger->start(self::$operationName, [ 29 | 'job.connection_name' => $event->connectionName, 30 | 'job.id' => $event->job->getJobId(), 31 | 'job.queue' => $event->job->getQueue(), 32 | 'job.body' => $event->job->getRawBody(), 33 | ]); 34 | 35 | return; 36 | } 37 | 38 | if ($event instanceof JobProcessed || $event instanceof JobFailed) { 39 | $operationName = self::$operationName ?? "Job {$event->job->resolveName()}"; 40 | 41 | $this->jaeger->stop($operationName); 42 | 43 | return; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Listeners/QueryListener.php: -------------------------------------------------------------------------------- 1 | jaeger = $jaeger; 17 | } 18 | 19 | public function handle(QueryExecuted $event): void 20 | { 21 | $this->jaeger->startStop("DB Query: {$event->sql}", [ 22 | 'query.sql' => $event->sql, 23 | 'query.bindings' => implode(',', $event->bindings), 24 | 'query.connection_name' => $event->connectionName, 25 | 'query.time' => (string)$event->time 26 | ], $event->time/1000); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |