Skip to content

Commit e2d8629

Browse files
authored
Capture query connection type (read or write) (#318)
1 parent b848ebe commit e2d8629

File tree

6 files changed

+212
-0
lines changed

6 files changed

+212
-0
lines changed

src/Compatibility.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ final class Compatibility
3939

4040
public static bool $subMinuteScheduledTasksSupported = false;
4141

42+
public static bool $queryConnectionTypeCapturable = false;
43+
4244
/**
4345
* @var array{
4446
* nightwatch_should_sample?: bool|null,
@@ -113,6 +115,12 @@ public static function boot(Application $app): void
113115
if (version_compare($version, '11.5.0', '<')) {
114116
Event::macro('tap', fn (callable $callable) => tap($this, $callable));
115117
}
118+
119+
/**
120+
* @see https://github.com/laravel/framework/pull/58156
121+
* @see https://github.com/laravel/framework/releases/tag/v12.45.0
122+
*/
123+
self::$queryConnectionTypeCapturable = version_compare($version, '12.45.0', '>=');
116124
}
117125

118126
/**

src/QueryConnectionType.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Laravel\Nightwatch;
4+
5+
enum QueryConnectionType: string
6+
{
7+
case Read = 'read';
8+
case Write = 'write';
9+
case Unknown = 'unknown';
10+
}

src/Records/Query.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Laravel\Nightwatch\Records;
44

5+
use Laravel\Nightwatch\QueryConnectionType;
6+
57
final class Query
68
{
79
public function __construct(
@@ -10,6 +12,7 @@ public function __construct(
1012
public readonly int $line,
1113
public readonly int $duration,
1214
public readonly string $connection,
15+
public readonly QueryConnectionType $connectionType,
1316
) {
1417
//
1518
}

src/Sensors/QuerySensor.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use Illuminate\Database\Events\QueryExecuted;
66
use Laravel\Nightwatch\Clock;
7+
use Laravel\Nightwatch\Compatibility;
78
use Laravel\Nightwatch\Location;
9+
use Laravel\Nightwatch\QueryConnectionType;
810
use Laravel\Nightwatch\Records\Query;
911
use Laravel\Nightwatch\State\CommandState;
1012
use Laravel\Nightwatch\State\RequestState;
@@ -39,13 +41,18 @@ public function __invoke(QueryExecuted $event, array $trace): array
3941

4042
[$file, $line] = $this->location->forQueryTrace($trace);
4143

44+
$connectionType = Compatibility::$queryConnectionTypeCapturable && $event->readWriteType !== null
45+
? QueryConnectionType::from($event->readWriteType)
46+
: QueryConnectionType::Unknown;
47+
4248
return [
4349
$record = new Query(
4450
sql: $event->sql,
4551
file: $file ?? '',
4652
line: $line ?? 0,
4753
duration: $durationInMicroseconds,
4854
connection: $event->connectionName ?? '', // @phpstan-ignore nullCoalesce.property
55+
connectionType: $connectionType,
4956
),
5057
function () use ($event, $record) {
5158
$this->executionState->queries++;
@@ -68,6 +75,7 @@ function () use ($event, $record) {
6875
'line' => $record->line,
6976
'duration' => $record->duration,
7077
'connection' => Str::tinyText($record->connection),
78+
'connection_type' => $record->connectionType === QueryConnectionType::Unknown ? '' : $record->connectionType->value,
7179
];
7280
},
7381
];

tests/Feature/Sensors/QuerySensorTest.php

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@
1414
use Illuminate\Support\Facades\DB;
1515
use Illuminate\Support\Facades\Event;
1616
use Illuminate\Support\Facades\Route;
17+
use Laravel\Nightwatch\Compatibility;
1718
use MongoDB\Laravel\Connection as MongoDbConnection;
1819
use PDO;
1920
use PHPUnit\Framework\Attributes\DataProvider;
2021
use SingleStore\Laravel\Connect\Connection as LegacySingleStoreConnection;
2122
use SingleStore\Laravel\Connect\SingleStoreConnection;
2223
use Tests\TestCase;
2324

25+
use function array_merge;
2426
use function base64_encode;
2527
use function class_exists;
2628
use function dirname;
29+
use function fake;
2730
use function hash;
2831
use function hex2bin;
2932
use function in_array;
@@ -93,6 +96,7 @@ public function test_it_can_ingest_queries(): void
9396
'line' => $line,
9497
'duration' => 4321,
9598
'connection' => $connection,
99+
'connection_type' => Compatibility::$queryConnectionTypeCapturable ? 'write' : '',
96100
],
97101
]);
98102
}
@@ -352,4 +356,182 @@ public function test_it_can_capture_null_connection_name()
352356
$ingest->assertWrittenTimes(1);
353357
$ingest->assertLatestWrite('query:0.connection', '');
354358
}
359+
360+
public function test_it_captures_connection_type_as_write_when_read_and_write_connections_are_not_configured()
361+
{
362+
$ingest = $this->fakeIngest();
363+
364+
Route::get('/users', function () {
365+
return DB::table('users')->get();
366+
});
367+
368+
$response = $this->get('/users');
369+
370+
$response->assertOk();
371+
$ingest->assertWrittenTimes(1);
372+
$ingest->assertLatestWrite('query:0.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'write' : '');
373+
}
374+
375+
public function test_it_captures_connection_type_as_read_for_select_query()
376+
{
377+
$this->configureReadWriteConnection();
378+
379+
$ingest = $this->fakeIngest();
380+
381+
Route::get('/users', function () {
382+
return DB::table('users')->get();
383+
});
384+
385+
$response = $this->get('/users');
386+
387+
$response->assertOk();
388+
$ingest->assertWrittenTimes(1);
389+
$ingest->assertLatestWrite('query:0.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'read' : '');
390+
}
391+
392+
public function test_it_captures_connection_type_as_write_for_write_query()
393+
{
394+
$this->configureReadWriteConnection();
395+
396+
$ingest = $this->fakeIngest();
397+
398+
Route::get('/users', function () {
399+
return DB::table('users')->insert([
400+
'name' => fake()->name(),
401+
'email' => fake()->email(),
402+
'password' => fake()->password(),
403+
]);
404+
});
405+
406+
$response = $this->get('/users');
407+
408+
$response->assertOk();
409+
$ingest->assertWrittenTimes(1);
410+
$ingest->assertLatestWrite('query:0.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'write' : '');
411+
}
412+
413+
public function test_it_captures_connection_type_as_write_when_records_have_been_modified_and_sticky_connection_is_enabled()
414+
{
415+
$this->configureReadWriteConnection(['sticky' => true]);
416+
417+
$ingest = $this->fakeIngest();
418+
419+
Route::get('/users', function () {
420+
DB::table('users')->insert([
421+
'name' => fake()->name(),
422+
'email' => fake()->email(),
423+
'password' => fake()->password(),
424+
]);
425+
426+
return DB::table('users')->get();
427+
});
428+
429+
$response = $this->get('/users');
430+
431+
$response->assertOk();
432+
$ingest->assertWrittenTimes(1);
433+
$ingest->assertLatestWrite('query:0.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'write' : ''); // insert
434+
$ingest->assertLatestWrite('query:1.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'write' : ''); // select
435+
}
436+
437+
public function test_it_captures_connection_type_as_write_for_insert_and_read_for_select_when_sticky_connection_is_disabled()
438+
{
439+
$this->configureReadWriteConnection(['sticky' => false]);
440+
441+
$ingest = $this->fakeIngest();
442+
443+
Route::get('/users', function () {
444+
DB::table('users')->insert([
445+
'name' => fake()->name(),
446+
'email' => fake()->email(),
447+
'password' => fake()->password(),
448+
]);
449+
450+
return DB::table('users')->get();
451+
});
452+
453+
$response = $this->get('/users');
454+
455+
$response->assertOk();
456+
$ingest->assertWrittenTimes(1);
457+
$ingest->assertLatestWrite('query:0.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'write' : ''); // insert
458+
$ingest->assertLatestWrite('query:1.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'read' : ''); // select
459+
}
460+
461+
public function test_it_captures_connection_type_as_write_when_it_should_use_write_connection_when_reading()
462+
{
463+
$this->configureReadWriteConnection();
464+
465+
$ingest = $this->fakeIngest();
466+
467+
Route::get('/users', function () {
468+
return DB::useWriteConnectionWhenReading()->table('users')->get();
469+
});
470+
471+
$response = $this->get('/users');
472+
473+
$response->assertOk();
474+
$ingest->assertWrittenTimes(1);
475+
$ingest->assertLatestWrite('query:0.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'write' : '');
476+
}
477+
478+
public function test_it_captures_connection_type_as_write_when_in_a_transaction()
479+
{
480+
$this->configureReadWriteConnection();
481+
482+
$ingest = $this->fakeIngest();
483+
484+
Route::get('/users', function () {
485+
DB::beginTransaction();
486+
487+
$users = DB::table('users')->get();
488+
489+
DB::rollBack();
490+
491+
return $users;
492+
});
493+
494+
$response = $this->get('/users');
495+
496+
$response->assertOk();
497+
$ingest->assertWrittenTimes(1);
498+
$ingest->assertLatestWrite('query:0.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'write' : '');
499+
}
500+
501+
public function test_it_captures_connection_type_when_forgetting_modified_records_state()
502+
{
503+
$this->configureReadWriteConnection(['sticky' => true]);
504+
505+
$ingest = $this->fakeIngest();
506+
507+
Route::get('/users', function () {
508+
DB::statement('select 1');
509+
DB::forgetRecordModificationState();
510+
DB::select('select 1');
511+
});
512+
513+
$response = $this->get('/users');
514+
515+
$response->assertOk();
516+
$ingest->assertWrittenTimes(1);
517+
$ingest->assertLatestWrite('query:0.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'write' : '');
518+
$ingest->assertLatestWrite('query:1.connection_type', Compatibility::$queryConnectionTypeCapturable ? 'read' : '');
519+
}
520+
521+
private function configureReadWriteConnection(array $options = []): void
522+
{
523+
$connection = Config::get('database.default');
524+
$config = Config::get("database.connections.{$connection}");
525+
526+
Config::set("database.connections.{$connection}", array_merge($config, [
527+
'read' => [
528+
'database' => $config['database'],
529+
],
530+
'write' => [
531+
'database' => $config['database'],
532+
],
533+
], $options));
534+
535+
DB::purge($connection);
536+
}
355537
}

tests/Unit/ArchitectureTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function test_classes_are_internal(): void
3636
\Laravel\Nightwatch\Core::class,
3737
\Laravel\Nightwatch\Facades\Nightwatch::class,
3838
\Laravel\Nightwatch\Http\Middleware\Sample::class,
39+
\Laravel\Nightwatch\QueryConnectionType::class,
3940
\Laravel\Nightwatch\Records\CacheEvent::class,
4041
\Laravel\Nightwatch\Records\Command::class,
4142
\Laravel\Nightwatch\Records\Exception::class,

0 commit comments

Comments
 (0)