Логи разбросаны по десятку серверов — поиск причины падения превращается в недельный квест. При пиковых нагрузках логи теряются, а поиск ошибки занимает часы. Мы решаем это за день: ставим централизованный сбор на 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. Закажите настройку и получите консультацию по вашему проекту.







