Проблеми з продуктивністю часто найскладніше розв'язати, адже вони не є багами у прямому сенсі. Найчастіше вони ховаються у запитах до бази даних: застосунок працює коректно, але повільно.
Якщо маршрут (route) гальмує, причина зазвичай у неефективних запитах, відсутності індексів або занадто ресурсомістких агрегаціях. Такі вузькі місця важко виявити без належного моніторингу.
У цьому гайді ми створимо легку систему моніторингу для застосунків на Laravel та MongoDB. Наша мета — відстежувати час виконання запитів та тривалість обробки запитів, щоб розробники могли швидко знаходити та оптимізувати повільні операції.
Laravel та MongoDB — це потужне поєднання продуктивного PHP-фреймворка та гнучкої бази даних, створеної для масштабування. Завдяки офіційному пакету Laravel MongoDB ви можете використовувати Eloquent ORM для керування даними без жорстких обмежень традиційних SQL-схем.
Наприкінці цього посібника ви отримаєте систему, яка:
- Фіксує час виконання запитів до MongoDB.
- Автоматично ідентифікує повільні запити.
- Логує дані про продуктивність для подальшого аналізу.
- Автоматично видаляє старі логи за допомогою TTL-індексів.
# Попередні вимоги
Для роботи з туторіалом вам знадобляться:
- Налаштоване середовище розробки для Laravel та MongoDB.
- Досвід роботи з фреймворком Laravel.
- Акаунт у MongoDB Atlas.
# Архітектура системи
Розглянемо, як працюватиме наш моніторинг. Система складається з двох компонентів: Request Middleware та MongoDB Command Subscriber.
Request Middleware
Це кастомний Middleware, який вимірює загальну тривалість HTTP-запиту від моменту отримання до повернення відповіді клієнту.
MongoDB Command Subscriber
Це PHP-клас, який підписується на події драйвера MongoDB PHP. Драйвер дозволяє відстежувати виконання команд у базі даних у реальному часі.
Підписавшись на ці події, ми зможемо отримувати дані про тип операції (find, insert, aggregate), назву колекції та час виконання.
Нижче наведена схема роботи: від надходження запиту до збереження результатів у колекції MongoDB.

# Налаштування проєкту
Створимо новий проєкт Laravel:
composer create project laravel/laravel devrel-tutorial-customMiddlewareForQueryPerformance
Переконайтеся, що у вас встановлено MongoDB PHP Extension та додано до php.ini. Якщо потрібна допомога з інсталяцією, скористайтеся офіційною інструкцією.
Тепер встановимо пакет Laravel MongoDB:
composer require mongodb/laravel-mongodb
# Конфігурація .env
Додайте дані для підключення до MongoDB Atlas у файл .env. Замініть MONGODB_URL на свій рядок підключення.
# Налаштування config/database.php
Додайте конфігурацію MongoDB у масив connections файлу config/database.php:
'mongodb' => [
'driver' => 'mongodb',
'dsn' => env('MONGODB_URI'),
'database' => env('MONGODB_DATABASE'),
],
Також встановіть MongoDB як з'єднання за замовчуванням:
'default' => env('DB_CONNECTION', 'mongodb'),
# Створення моделі Post для тестування
Для перевірки трекера нам потрібні дані. Створимо модель Post, фабрику та seeder:
php artisan make:model Post -mf
Оновіть app/Models/Post.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use MongoDB\Laravel\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $connection = 'mongodb';
protected $collection = 'posts';
protected $fillable = [
'title',
'body'
];
}
# Наповнення даними (Seeding)
Налаштуйте фабрику в database/factories/PostFactory.php:
<?php
namespace Database\Factories;
use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
protected $model = Post::class;
public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraph(5),
];
}
}
Підготуйте seeder у database/seeders/PostSeeder.php:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Post;
class PostSeeder extends Seeder
{
public function run(): void
{
Post::factory()->count(10)->create();
}
}
Запустіть наповнення бази даних:
php artisan db:seed
# Лог продуктивності
Створимо модель PerformanceLog для збереження метрик у колекції performance_logs:
php artisan make:model PerformanceLog
Оновіть app/Models/PerformanceLog.php:
<?php
namespace App\Models;
use MongoDB\Laravel\Eloquent\Model;
class PerformanceLog extends Model
{
protected $connection = 'mongodb';
protected $collection = 'performance_logs';
protected $fillable = [
'route',
'collection',
'operation',
'duration_ms',
'request_duration',
'is_slow',
'created_at'
];
}
# Створення QueryMonitorService
Цей сервіс збиратиме метрики запитів. Створіть app/Services/QueryMonitorService.php:
<?php
namespace App\Services;
use App\Models\PerformanceLog;
class QueryMonitorService
{
protected array $queries = [];
protected int $slowThreshold = 200; // поріг у мілісекундах
public function record(string $collection, string $operation, float $duration): void
{
$this->queries[] = [
'collection' => $collection,
'operation' => $operation,
'duration_ms' => $duration,
'is_slow' => $duration > $this->slowThreshold
];
}
public function persist(string $route, float $requestDuration): void
{
foreach ($this->queries as $query) {
PerformanceLog::create([
'route' => $route,
'collection' => $query['collection'],
'operation' => $query['operation'],
'duration_ms' => $query['duration_ms'],
'request_duration' => $requestDuration,
'is_slow' => $query['is_slow'],
'created_at' => now()
]);
}
}
}
# Підписка на події драйвера MongoDB
Використовуємо Command Subscriber для перехоплення команд до бази даних. Створіть app/Monitoring/MongoCommandSubscriber.php:
<?php
namespace App\Monitoring;
use MongoDB\Driver\Monitoring\CommandSubscriber;
use MongoDB\Driver\Monitoring\CommandStartedEvent;
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use MongoDB\Driver\Monitoring\CommandFailedEvent;
use App\Services\QueryMonitorService;
class MongoCommandSubscriber implements CommandSubscriber
{
protected array $startTimes = [];
protected array $operations = [];
protected array $collections = [];
public function commandStarted(CommandStartedEvent $event): void
{
$requestId = $event->getRequestId();
$this->startTimes[$requestId] = microtime(true);
$operation = $event->getCommandName();
$command = get_object_vars($event->getCommand());
$collection = $command[$operation] ?? 'unknown';
$this->operations[$requestId] = $operation;
$this->collections[$requestId] = $collection;
}
public function commandSucceeded(CommandSucceededEvent $event): void
{
$requestId = $event->getRequestId();
if (!isset($this->startTimes[$requestId])) return;
$duration = (microtime(true) - $this->startTimes[$requestId]) * 1000;
$operation = $this->operations[$requestId] ?? $event->getCommandName();
$collection = $this->collections[$requestId] ?? 'unknown';
app(QueryMonitorService::class)->record($collection, $operation, $duration);
$this->cleanup($requestId);
}
public function commandFailed(CommandFailedEvent $event): void
{
$this->cleanup($event->getRequestId());
}
protected function cleanup($requestId): void
{
unset($this->startTimes[$requestId], $this->operations[$requestId], $this->collections[$requestId]);
}
}
# Реєстрація підписника
Зареєструйте підписника в app/Providers/AppServiceProvider.php:
public function register(): void
{
$this->app->singleton(QueryMonitorService::class, fn() => new QueryMonitorService());
}
public function boot(): void
{
\MongoDB\Driver\Monitoring\addSubscriber(new MongoCommandSubscriber());
}
# Створення Performance Middleware
Створіть Middleware для вимірювання тривалості запиту:
php artisan make:middleware PerformanceMiddleware
Код app/Http/Middleware/PerformanceMiddleware.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Services\QueryMonitorService;
class PerformanceMiddleware
{
public function handle(Request $request, Closure $next)
{
$start = microtime(true);
$response = $next($request);
$duration = (microtime(true) - $start) * 1000;
$route = $request->route()?->getName() ?? $request->route()?->uri() ?? $request->path();
app(QueryMonitorService::class)->persist($route, $duration);
return $response;
}
}
Зареєструйте його в bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
PerformanceMiddleware::class,
]);
})
# Тестування
Додайте тестовий маршрут у routes/web.php:
Route::get('/posts', function () {
return Post::where('title', 'like', '%API%')->limit(10)->get();
});
Запустіть сервер (php artisan serve) та перейдіть за адресою /posts. Після цього в колекції performance_logs з'являться дані про продуктивність запиту.
# Очищення старих логів через TTL-індекси
Щоб колекція логів не розросталася безмежно, використовуйте TTL (Time To Live) індекси. MongoDB автоматично видалятиме документи через заданий час.
У MongoDB Atlas:
- Відкрийте колекцію
performance_logs. - На вкладці Indexes натисніть Create Index.
- Поле:
created_at, Тип:Ascending (1), TTL:604800(7 днів).
Або через консоль:
db.performance_logs.createIndex( { created_at: 1 }, { expireAfterSeconds: 604800 })
# Висновок
Ця архітектура розділяє логіку моніторингу та самого застосунку. Завдяки Command Subscriber ми отримуємо глибоку аналітику запитів без модифікації коду репозиторіїв чи контролерів.
Повний сирцевий код доступний на GitHub. Також ви можете скористатися готовим Laravel-пакетом для швидкого впровадження цієї системи у ваші проєкти.