Настройка логирования (Loki/Grafana) для веб-приложения
Loki — это не Elasticsearch. Ключевое отличие: Loki не индексирует содержимое логов, только метки (labels). Это делает его на порядок дешевле в хранении и проще в операционном обслуживании. Платите за это ограниченной скоростью full-text поиска по телу лога. Для большинства веб-приложений этот компромисс оправдан.
Стек: Promtail (или Alloy) → Loki → 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
# loki-config.yml
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
# promtail-config.yml
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
Отправка логов из приложения
Для Laravel — прямая отправка в Loki через HTTP API:
// 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]],
]],
];
// Fire and forget — не блокируем запрос
$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'),
],
],
],
LogQL — язык запросов Loki
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])
)
Grafana: datasource и дашборд
Автопровизионирование datasource:
# grafana/provisioning/datasources/loki.yml
apiVersion: 1
datasources:
- name: Loki
type: loki
url: http://loki:3100
isDefault: true
jsonData:
maxLines: 1000
derivedFields:
- datasourceUid: prometheus
matcherRegex: "request_id=(\\w+)"
name: RequestID
url: '${__value.raw}'
Базовый дашборд включает:
-
Logs panel с фильтрацией по label
levelиjob -
Time series с
rate({job="laravel-app", level="error"}[1m]) - Stat panel — количество ошибок за последние 24 часа
-
Table с топ-20 сообщениями об ошибках через
count_over_time
Alerting в Grafana
# Правило алерта на рост ошибок
apiVersion: 1
groups:
- name: app-alerts
rules:
- uid: error-rate-spike
title: High error rate
condition: C
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 300
to: 0
model:
expr: 'sum(rate({job="laravel-app", level="error"}[5m]))'
- refId: C
queryType: ''
model:
type: threshold
conditions:
- evaluator:
params: [0.1]
type: gt
noDataState: OK
execErrState: Error
for: 2m
annotations:
summary: "Error rate > 0.1/s for 2 minutes"
labels:
severity: warning
Сравнение с ELK
Loki выигрывает по стоимости хранения (нет инвертированного индекса — хранит только сжатые чанки) и простоте эксплуатации. ELK выигрывает при необходимости сложного full-text поиска, aggregation по полям лога без предварительного парсинга через labels.
Для большинства веб-приложений с JSON-логами и Grafana как единым дашбордом — Loki предпочтительнее.
Сроки
Развёртывание Loki + Promtail + Grafana, настройка сбора Nginx и application logs, базовые дашборды и один алерт на критические ошибки: 1-2 рабочих дня.







