├── .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 |