Loki — система агрегации логов от Grafana Labs, которая индексирует только метки потоков, а сами строки складывает сжатыми блоками в объектное хранилище. Из-за этого она дешевле Elasticsearch по диску и памяти, но поиск по тексту идёт полным перебором подходящих потоков. Для домашней лаборатории, небольшой команды или сотни гигабайт логов в месяц этого более чем достаточно. Ниже — рабочая установка, сбор логов Nginx через Grafana Alloy и практические запросы LogQL.
Чем Loki отличается от Elasticsearch
Elasticsearch строит инвертированный индекс по каждому слову, поэтому мгновенно ищет произвольные фразы, но требует много RAM и диска. Loki хранит индекс только для меток: job, host, container, namespace. Чем меньше уникальных комбинаций меток, тем эффективнее запросы. Плата за простоту — необходимость сначала сузить поток метками, а уже потом фильтровать текст.
- Метки — это не поля лога, а способ быстро найти нужный поток.
- Высокая кардинальность (идентификатор запроса, IP, user_id) убивает производительность.
- Хранение в S3-совместимом хранилище позволяет держать логи месяцами.
- Grafana выступает единой точкой входа для логов, метрик и трассировок.
Установка через Docker Compose
Для одного сервера подойдёт режим с файловым хранилищем. Конфигурация ниже включает TSDB-индекс, файловое хранилище и удержание логов 30 дней. Каталоги монтируем на диск, где достаточно места: сжатые чанки занимают немного, но метаданные и индекс тоже растут.
services:
loki:
image: grafana/loki:3.4.2
command: -config.file=/etc/loki/config.yaml
volumes:
- ./loki.yaml:/etc/loki/config.yaml:ro
- ./data/loki:/loki
ports:
- "3100:3100"
alloy:
image: grafana/alloy:v1.7.5
volumes:
- ./config.alloy:/etc/alloy/config.alloy:ro
- /var/log:/var/log:ro
command: run --server.http.listen-addr=0.0.0.0:12345 /etc/alloy/config.alloy
grafana:
image: grafana/grafana:11.5.0
ports:
- "3000:3000"
volumes:
- ./data/grafana:/var/lib/grafana
Файл loki.yaml задаёт схему, хранилище и политику удержания. Компактор включаем сразу: без него старые чанки не удаляются, даже если срок хранения указан.
auth_enabled: false
server:
http_listen_port: 3100
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-04-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 720h
compactor:
working_directory: /loki/compactor
retention_enabled: true
delete_request_store: filesystem
Сбор логов с помощью Grafana Alloy
Promtail официально завершил жизненный цикл в 2025 году, поэтому новые установки собираем на Alloy. Агент находит файлы, читает их с текущей позиции и отправляет поток в Loki. Метки job и host должны быть стабильными: именно по ним строятся запросы.
local.file_match "nginx" {
path_targets = [{
__path__ = "/var/log/nginx/*.log",
job = "nginx",
host = "web-01",
}]
}
loki.source.file "nginx" {
targets = local.file_match.nginx.targets
forward_to = [loki.write.local.receiver]
}
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
Для контейнеров вместо local.file_match используют discovery.docker и loki.source.docker, но принцип тот же: метки из конфигурации, текст из stdout. Не добавляйте в метки имя контейнера с хешем — оно меняется при каждом перезапуске и создаёт новый поток.
Запросы LogQL
Сначала выбираем поток в фигурных скобках, затем фильтруем строки операторами |= (содержит), !~ (не совпадает с регулярным выражением) и | json. Метрические запросы считают скорость и количество событий.
{job="nginx"} |= "error"
{job="nginx"} |~ " 50[0-9] "
{job="nginx"} | json | status >= 500
sum by (host) (rate({job="nginx"} |= "error" [5m]))
topk(5, sum by (path) (count_over_time({job="nginx"} | json [1h])))
Оператор | json разбирает JSON-строки и делает поля доступными для фильтров. Для логов в формате key=value есть | logfmt. Если поле встречается редко, сначала сузьте поток по метке и времени, иначе Loki просканирует лишние гигабайты.
Диагностика и обслуживание
Проверка готовности, метрики и список меток помогают быстро понять, где оборвалась цепочка: агент, сеть или сама Loki. Утилита logcli удобна для проверки запросов без браузера.
curl -s http://localhost:3100/ready
curl -s http://localhost:3100/metrics | grep -E 'loki_ingester_memory_streams|loki_request_duration'
logcli query '{job="nginx"} |= "timeout"' --since=2h --limit=50
curl -s http://localhost:3100/loki/api/v1/labels
Если Grafana показывает «No data», проверьте три вещи: совпадает ли метка в запросе с реальной, не истёк ли срок хранения и видит ли агент файл. Права на /var/log/nginx у контейнера часто недостаточны — монтируйте каталог только для чтения и запускайте агент от пользователя с доступом.
Отдельного внимания требует удержание. Параметр retention_period действует только вместе с включённым компактором и корректным delete_request_store. Проверить фактический объём можно через API и размер каталога: если чанки растут, а старые не исчезают, скорее всего компактор не запустился из-за прав или несовместимой схемы. Для больших объёмов вместо файловой системы подключают S3-совместимое хранилище и выносят индекс на отдельный диск.
Кардинальность, безопасность и резервные копии
- Держите число потоков в пределах десятков тысяч, а не миллионов.
- Не открывайте порт 3100 в интернет: Loki не имеет встроенной авторизации при
auth_enabled: false. - Ставьте перед Loki reverse proxy с Basic Auth или ограничьте доступ по сети.
- Резервируйте каталог
data/lokiвместе с конфигурацией; для больших объёмов используйте S3-совместимое хранилище. - Проверяйте
loki_ingester_memory_streamsи размер чанков после изменения формата логов.
Итог
Loki выигрывает там, где нужен недорогой централизованный поиск по логам без кластера Elasticsearch. Секрет стабильной работы — дисциплина меток: минимальный набор измерений, никаких идентификаторов в лейблах и обязательное сужение по времени. Связка Loki, Alloy и Grafana разворачивается за вечер и закрывает типовые задачи диагностики: найти ошибки за последний час, посчитать пятисотые по хостам и посмотреть контекст вокруг сбоя.