Skip to content

Query Logging

Muhammet Şafak edited this page May 24, 2026 · 1 revision

Query Logging

Two independent facilities — often confused, but they answer different questions:

ToolCapturesGoes toQuestion it answers
enableQueryLog()Every executed SQL, its bound args, elapsed timeAn in-memory array (getQueryLogs())"What SQL did my code actually run?"
The log connection channelConnection-level events (driver errors, exceptions)File path, callable, PSR-3 logger, or object with critical()"What went wrong on the wire?"

Use the first for profiling and test assertions. Use the second for the production audit trail.

In-memory query log

useInitPHP\Database\DB;
DB::enableQueryLog();
DB::table('users')->where('name', 'Ada')->read();
DB::table('posts')->where('author_id', 5)->read();
print_r(DB::getQueryLogs());
/*[ [ 'query' => 'SELECT * FROM `users` WHERE `name` = :name', 'args' => [':name' => 'Ada'], 'timer' => 0.000385, ], [ 'query' => 'SELECT * FROM `posts` WHERE `author_id` = :author_id', 'args' => [':author_id' => 5], 'timer' => 0.000412, ],]*/DB::disableQueryLog(); // stop recording — the existing buffer stays untouched

enableQueryLog() flips a flag on the connection; every subsequent prepare / execute appends to the buffer.

Each log entry

KeyTypeNotes
querystringThe prepared SQL exactly as sent to PDO.
argsarray<string, mixed>The named-parameter map that was bound.
timerfloatSeconds elapsed inside execute().

Enable from bootstrap

DB::createImmutable([
'dsn' => '...',
'queryLogs' => true, // start the buffer already on
]);

Using it in tests

publicfunctiontestReadFiltersSoftDeletedRows(): void
{
$db = SqliteHelper::makeDatabase();
$db->enableQueryLog();
(newPosts())->read();
$log = $db->getQueryLogs();
self::assertStringContainsString('IS NULL', $log[0]['query']);
}

Using it for profiling

DB::enableQueryLog();
// run request handler...$logs = DB::getQueryLogs();
usort($logs, fn ($a, $b) => $b['timer'] <=> $a['timer']);
foreach (array_slice($logs, 0, 10) as$entry) {
printf("%.4fs %s\n", $entry['timer'], $entry['query']);
}

Memory cost

The buffer grows without bound. In long-lived processes (workers, daemons), either:

  • Leave logging off and turn it on temporarily around suspect code paths.
  • Periodically disableQueryLog() and re-enable after handing the buffer off.
  • Pump entries into the log channel and drop the in-memory array.

The buffer is per Database instance, so two connections have two independent buffers.

The log connection channel

DB::createImmutable([
'dsn' => '...',
'username' => '...',
'password' => '...',
'log' => __DIR__ . '/logs/db-{year}-{month}-{day}.log',
]);

The log credential accepts four shapes:

1. PSR-3 logger

'log' => newMonolog\Logger('db'),

The connection calls $logger->critical($message, $context). Anything implementing Psr\Log\LoggerInterface works — Monolog, Symfony, custom.

2. Callable

'log' => function (string$message): void {
error_log('[DB] ' . $message);
},

The closure / callable is invoked with the formatted message string.

3. Object with a critical() method

finalclass Notifier
{
publicfunctioncritical(string$msg): void
{
$this->slack->send($msg);
}
}
'log' => newNotifier(),

Duck-typed — kept around for callers that don't want to pull psr/log in.

4. String path

'log' => __DIR__ . '/db.log',

The connection writes with file_put_contents(..., FILE_APPEND). Date / time placeholders expand before opening the file:

TokenReplaced with
{date}Y-m-d
{datetime}Y-m-d H:i:s
{timestamp}Unix timestamp
{year} / {month} / {day}individual date parts
{hour} / {minute} / {second}individual time parts

So db-{year}-{month}-{day}.log rotates daily for free.

Disable the channel

Pass null, false, or an empty string for 'log' (or omit it entirely). The writer becomes a no-op.

Combining the two

DB::createImmutable([
'dsn' => '...',
'log' => __DIR__ . '/db.log', // production audit trail'queryLogs' => false, // off by default
]);
// Enable the in-memory buffer for one specific request handler:if ($request->getQueryParams()['debug'] ?? false) {
DB::enableQueryLog();
}
// ... at the end of the handler:if (DB::getQueryLogs() !== []) {
$response = $response->withHeader('X-Query-Count', (string) count(DB::getQueryLogs()));
}

Sample: counting queries per request

finalclass QueryCounterMiddleware
{
publicfunctionprocess($request, $handler)
{
DB::enableQueryLog();
$response = $handler->handle($request);
$logs = DB::getQueryLogs();
$time = array_sum(array_column($logs, 'timer'));
return$response
->withHeader('X-Query-Count', (string) count($logs))
->withHeader('X-Query-Time', sprintf('%.3fms', $time * 1000));
}
}

Next

Clone this wiki locally