Логи разбросаны по десятку серверов — поиск причины падения превращается в недельный квест. При пиковых нагрузках логи теряются, а поиск ошибки занимает часы. Мы решаем это за день: ставим централизованный сбор на Grafana Loki и Promtail. В отличие от ELK, Loki не индексирует содержимое, только метки. На практике это даёт до 70% экономии на хранении и в два-три раза меньшую операционную нагрузку. На одном из проектов с 50 микросервисами (10 млн запросов в день) мы сократили time-to-detect с 2 дней до 15 минут. За 7 лет мы развернули более 40 production-стеков на Loki. Свяжитесь с нами — выполним настройку в течение 2-3 дней.
Почему Loki лучше Elasticsearch?
Согласно официальным данным Grafana Labs, Loki обеспечивает до 70% экономии на хранении по сравнению с ELK. При объеме логов 500 ГБ в сутки экономия может достигать $10 000 в месяц. Loki хранит логи в сжатых чанках без инвертированного индекса. Скорость запросов по меткам — миллисекунды. Если вам не нужна сложная агрегация по произвольным полям, Loki — правильный выбор. Экономия на инфраструктуре может составлять от $5 000 до $15 000 ежемесячно в зависимости от объема логов.
| Критерий | Loki | ELK |
|---|---|---|
| Стоимость хранения | Низкая (сжатые чанки) | Высокая (инвертированный индекс) |
| Скорость поиска по меткам | Высокая | Средняя |
| Full-text поиск | Ограниченный | Полноценный |
| Операционная сложность | Низкая (одна конфигурация) | Высокая (три стека) |
| Интеграция с Grafana | Нативная | Через доп. плагин |
Как мы разворачиваем стек?
Используем Docker Compose для быстрого развёртывания. Конфигурация включает три сервиса:
version: '3.8' services: loki: image: grafana/loki:3.0.0 ports: - "3100:3100" volumes: - ./loki-config.yml:/etc/loki/local-config.yaml - loki_data:/loki command: -config.file=/etc/loki/local-config.yaml promtail: image: grafana/promtail:3.0.0 volumes: - /var/log:/var/log:ro - /var/lib/docker/containers:/var/lib/docker/containers:ro - ./promtail-config.yml:/etc/promtail/config.yml command: -config.file=/etc/promtail/config.yml grafana: image: grafana/grafana:11.0.0 ports: - "3000:3000" environment: - GF_AUTH_ANONYMOUS_ENABLED=false - GF_SECURITY_ADMIN_PASSWORD=admin_password volumes: - grafana_data:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning volumes: loki_data: grafana_data: Конфигурация Loki
auth_enabled: false server: http_listen_port: 3100 grpc_listen_port: 9096 common: path_prefix: /loki storage: filesystem: chunks_directory: /loki/chunks rules_directory: /loki/rules replication_factor: 1 ring: kvstore: store: inmemory schema_config: configs: - from: 2024-01-01 store: tsdb object_store: filesystem schema: v13 index: prefix: index_ period: 24h limits_config: retention_period: 744h # 31 день ingestion_rate_mb: 16 ingestion_burst_size_mb: 32 max_query_length: 721h compactor: working_directory: /loki/compactor retention_enabled: true delete_request_store: filesystem Конфигурация Promtail
server: http_listen_port: 9080 grpc_listen_port: 0 positions: filename: /tmp/positions.yaml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - job_name: nginx static_configs: - targets: [localhost] labels: job: nginx env: production __path__: /var/log/nginx/access.log pipeline_stages: - regex: expression: '^(?P<ip>\S+) - (?P<user>\S+) \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<bytes>\d+)' - labels: status: method: - timestamp: source: timestamp format: "02/Jan/2006:15:04:05 -0700" - job_name: laravel static_configs: - targets: [localhost] labels: job: laravel-app env: production __path__: /var/www/app/storage/logs/laravel.log pipeline_stages: - multiline: firstline: '^\[\d{4}-\d{2}-\d{2}' max_wait_time: 3s - regex: expression: '^\[(?P<timestamp>[^\]]+)\] (?P<env>\w+)\.(?P<level>\w+): (?P<message>.*)' - labels: level: env: - timestamp: source: timestamp format: "2006-01-02 15:04:05" - job_name: docker docker_sd_configs: - host: unix:///var/run/docker.sock refresh_interval: 5s relabel_configs: - source_labels: [__meta_docker_container_name] target_label: container - source_labels: [__meta_docker_container_log_stream] target_label: logstream Развертывание в Kubernetes
Для Kubernetes используйте Helm-чарты: `helm install loki grafana/loki-stack` — это поднимет Loki, Promtail и Grafana с настройками по умолчанию. Для production настройте retention, метки и pipeline_stages через values.yaml.Как интегрировать Laravel с Loki через Monolog?
Пишем кастомный Monolog-хендлер, который отправляет записи в Loki по HTTP API. Он работает fire-and-forget — не блокирует запрос.
// app/Logging/LokiHandler.php namespace App\Logging; use Monolog\Handler\AbstractProcessingHandler; use Monolog\LogRecord; class LokiHandler extends AbstractProcessingHandler { public function __construct( private string $lokiUrl, private array $labels = [] ) { parent::__construct(); } protected function write(LogRecord $record): void { $timestamp = (string)($record->datetime->getTimestamp() * 1_000_000_000); $payload = [ 'streams' => [[ 'stream' => array_merge($this->labels, [ 'level' => $record->level->getName(), 'channel' => $record->channel, ]), 'values' => [[$timestamp, $record->formatted]], ]], ]; $context = stream_context_create(['http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/json', 'content' => json_encode($payload), 'timeout' => 1, ]]); @file_get_contents("{$this->lokiUrl}/loki/api/v1/push", false, $context); } } // config/logging.php 'loki' => [ 'driver' => 'monolog', 'handler' => App\Logging\LokiHandler::class, 'with' => [ 'lokiUrl' => env('LOKI_URL', 'http://loki:3100'), 'labels' => [ 'app' => 'web-app', 'env' => env('APP_ENV', 'production'), ], ], ], Пошаговая настройка сбора логов с Laravel
- Создайте класс
LokiHandlerкак указано выше. - Зарегистрируйте канал
lokiвconfig/logging.php. - Добавьте переменную окружения
LOKI_URLв.env. - Проверьте доставку логов через LogQL:
{job="laravel-app"}. - При необходимости настройте pipeline_stages для парсинга multi-line исключений.
Как писать запросы LogQL?
LogQL похож на PromQL. Основные паттерны:
# Все ошибки Laravel за последний час {job="laravel-app", level="error"} |= "Exception" # Nginx 5xx {job="nginx"} | json | status >= 500 # Rate ошибок по минутам rate({job="laravel-app", level="error"}[1m]) # Топ медленных запросов (если request_time в логе) {job="nginx"} | regexp `request_time=(?P<rt>[0-9.]+)` | unwrap rt | quantile_over_time(0.95, [5m]) by (path) # Подсчёт ошибок по типу sum by (level) ( count_over_time({job="laravel-app"}[5m]) ) Как настроить алерты на рост ошибок?
Создаём правило алерта через provisioning YAML. Пример условия: sum(rate({job="laravel-app", level="error"}[5m])) > 0.1. При превышении порога более 2 минут срабатывает уведомление. Настраивается в grafana/provisioning/alerting/alert_rules.yml. Также добавляем алерт на резкий рост 5xx ошибок Nginx.
Процесс и сроки работы
| Этап | Что делаем | Срок |
|---|---|---|
| Аналитика | Определяем источники логов, метки, retention | 0.5 дня |
| Проектирование | Выбираем конфигурацию Promtail, Loki, datasource | 0.5 дня |
| Реализация | Развёртываем стек, настраиваем сбор, дашборды, алерты | 0.5 дня |
| Тестирование | Проверяем корректность доставки, работу запросов | 0.5 дня |
| Деплой | Передаём документацию, доступы, проводим обучение | включено |
Итого: 2-3 дня на весь стек. Стоимость рассчитывается индивидуально в зависимости от объёма логов и количества источников. Получите консультацию по вашему проекту — оценим и предложим решение.
Что входит в работу
- Развёрнутый стек Loki + Promtail + Grafana в Docker Compose или на bare metal.
- Конфигурация retention, меток и pipeline_stages под ваши источники.
- Кастомный Monolog-хендлер для Laravel (или аналогичный для других фреймворков).
- Дашборды в Grafana с визуализацией ошибок, трендов, top endpoints.
- Правила алертов на рост ошибок, 5xx, медленные запросы.
- Документация по эксплуатации и восстановлению.
- Передача доступов и обучение команды (1 час).
- Гарантия корректной работы стека в течение месяца после деплоя.
Частые проблемы при настройке и их решение
- Неправильная настройка retention — забывают включить compactor, логи не удаляются. Всегда проверяйте
compactor.retention_enabled: true. - Отсутствие pipeline_stages — метки не парсятся, сложно фильтровать. Для Laravel обязательно используйте multiline stage.
- Слишком много меток — каждая новая метка создаёт индекс, замедляя запись. Ограничьте число меток до 5-7 на источник.
Подробнее о логировании читайте на Wikipedia. Закажите настройку и получите консультацию по вашему проекту.







