Прямой ответ
Loki — «Prometheus для логов» (индексирует только лейблы, тело строки не индексируется — grep по сжатым чанкам — агрегация-контекст — дешевле ELK в разы, но «полнотекстовый поиск как в Elastic» — другая производительность — выбор осознанный — агрегация-статья); LogQL — язык запросов, две половины: (1) log-запросы (строки): селектор {app="orders", env="prod"} (лейблы — обязательная отправная точка — структура-меток — «без лейблов — сканируем всё» — медленно и дорого) + line-фильтры (|= "error", != "healthcheck", |~ "timeout.*db" — grep-семантика) + парсеры (| json (структурированные логи — поля в лейблы запроса — | level="error" после json — фильтрация по полю), | logfmt, | regexp, | pattern, | unwrap duration_ms (число из строки — метрика из лога) — «лог стал структурой на лету» — json-дисциплина окупается здесь); (2) metric-запросы (агрегаты — главная сила): sum by (app) (rate({env="prod"} |= "error" [5m])) — частота ошибок по логам — метрика из логов без инструментации кода (метрики-альтернатива), quantile_over_time(0.95, {app="api"} | json | unwrap duration_ms [5m]) by (app) — p95 из лог-поля (процентилям — «метрики нет — p95 есть» — логи как источник метрик — bridge к SLO), count_over_time/avg_over_time — окна — PromQL-подобие (PromQL — синтаксис знакомый — Grafana — панели на LogQL — как на метрики); лейбл-дизайн (критично — кардинальность Loki): лейблы — только низкое разнообразие (app/env/level/namespace — кардинальность-принцип — trace_id/user_id как лейбл — смерть индекса — в тело строки (json-поле — фильтр по нему — «лейблы — индекс, детали — в строке» — главное правило Loki); high-cardinality-потребности — в тело + line-фильтр (медленнее, но живее — агрегация); алерты на логи (ruler-компонент: metric-запрос + условие — rate({app="api"} |= "panic" [5m]) > 0 — алерты/Alertmanager — «panic в логах — page» — без метрик в коде — быстрый путь; осторожно: лог-алерты — хрупче метрик (алерт-дизайн — формат строки изменился — алерт ослеп — критичное — дублировать метрикой — метрики); производительность запросов: лейбл-селектор узкий (обязательно — скан чанков — ёмкость), временное окно осознанное («30 дней по error» — медленно — Loki хранит, но сканирует — retention), cache-уровень (query-frontend кэш — повторные дашборд-запросы — Mimir-аналогия); derived fields (Grafana-настройка: trace_id в логе → ссылка в Tempo (трейсинг — клик из лога в трейс — Alloy-связка — «лог → трейс → профиль» — observability-путь — Pyroscope).
Практика: типовые запросы и дашборды
Каталог рабочих запросов: (1) ошибки сервиса за час: {app="orders", env="prod"} |= "error" != "healthz" (health-шум отфильтрован — healthcheck — «в логах тонна health-чеков» — фильтр — логи-гигиена); (2) ошибки конкретного заказа: {app="orders"} | json | order_id="8f3a" (json-парсинг + поле — структура — «история заказа» — минуты); (3) топ-источников ошибок: topk(5, sum by (app) (rate({env="prod"} |= "error" [5m]))) (метрики-аналогия — «кто шумит» — дашборд-панель); (4) p95 из логов: quantile_over_time(0.95, {app="api"} | json | unwrap duration_ms [5m]) by (route) (процентилям — метрики латентности нет — лог-поле есть — быстрый SLO-черновик (SLO); (5) дубль-детект: {app="worker"} | json | unwrap attempts > 3 — задачи с ретраями (Celery-контекст — retry — «что ретраится аномально»); (6) безопасность-паттерн: {app="api"} |= "401" | json | line_format "{{.ip}} {{.path}}" (OWASP — брутфорс-паттерны — rate-limit — line_format — читаемый вывод — jq-культура); дашборды на LogQL (Grafana): errors-rate по сервисам (панель-тренд — алерт-визуализация), logs-панель с переменными (app/level — Grafana-переменные — «живой grep по парку» — структура); связки: exemplars-метрик → логи окна (метрики — «всплеск ошибок» → «вот строки»), trace_id → derived field → Tempo (трейсы); границы Loki (честно): ad-hoc-поиск «слово в любом логе за месяц» — медленный (нет полнотекстового индекса — ELK/OpenSearch — агрегация-выбор — «Loki для всего» — не правда), аналитика по лог-полям в масштабе (ClickHouse-путь (ClickHouse — логи как данные — SQL-аналитика — Loki — операционные логи/дебаг — разделение ролей — dbt-контекст); оптимум Loki: операционные логи + дашборды ошибок + алерты на паттерны — структура-дисциплина — «логи, которые читают запросами, а не глазами построчно».
Практический сценарий: «логи есть, ответов нет» → рабочий стек за неделю
Контекст: 15 сервисов, логи — в файл+stdout, дебаг — kubectl logs по pod'у (k8s — «инцидент: pod умер — логи умерли с ним» — классика), «показать ошибки за вчера» — невозможно; внедрение (неделя): (1) формат (первое — структура: все сервисы → JSON-логи (библиотека — 15 сервисов за 3 дня — по PR — CI — поля: ts/level/app/route/duration_ms/trace_id/error — trace_id из OTel — связь заложена сразу); (2) сбор (Alloy DaemonSet — filelog-ресивер — k8s-метки (namespace/pod/app — Alloy — лейблы Loki — только низкая кардинальность — pod-имя — в тело (per-pod-лейбл — churn — кардинальность); (3) Loki (single-binary на старте (агрегация-архитектура — S3/MinIO-хранилище (MinIO — retention 30 дней — стоимость); (4) дашборд (ошибки по сервисам — logs-панель с переменными — top-k источников (Grafana — «живой grep» — первый день — вау-эффект команды — adoption); (5) derived fields (trace_id → Tempo-ссылка — клик из ошибки в трейс — трейсы — «лог → причина» — путь расследования — incident); (6) алерты на паттерны (ruler: panic/deadlock detected (блокировки)/OOM — page (алерты — критичное дублировано метриками (метрики — хрупкость лог-алертов учтена — dedup); (7) проверка боем (инцидент недели: «заказы дублируются» — запрос {app="orders"} | json | order_id="..." — история заказа за 2 минуты (раньше — полдня раскопок по pod-логам — incident-метрики — причина — повтор вебхука (вебхуки — идемпотентность-фикс — идемпотентность); итоги: MTTR-расследования −60%, «логи умерли с pod'ом» — в прошлом, стоимость — 30% от ELK-оценки (агрегация-сравнение); выводы: формат логов — инвестиция №1 (JSON+поля — LogQL расцветает — структура — «plain-текст в Loki» — работает, но половину силы теряет), лейблы — минимум (кардинальность — метрики-урок), trace_id — мост (логи-трейсы-профили — одна нить — Alloy — observability-зрелость — три столпа).
FAQ
Loki против Elasticsearch/OpenSearch — когда что? Индекс-различие: Loki индексирует лейблы (строка — сжатый чанк — поиск — grep по чанкам — дёшево), ES — всё (полнотекстовый индекс — поиск мгновенный — дорого: RAM/диск ×3–10 (агрегация — ёмкость-стоимость); критерии: операционные логи/дебаг/алерты-паттерны + Grafana-стек — Loki (дешевле, запросы по лейблам+фильтрам достаточно быстры — сценарий выше), полнотекстовый поиск по неструктурированным логам/аналитика по полям в большом масштабе/комплаенс-поиск «любое упоминание за год» — ES/OpenSearch (ClickHouse-альтернатива — аналитика логов); «переехать с ES на Loki» — работает, если поиск лейбл+фильтр покрывает 90% запросов (аудит использования ES перед миграцией — техдолг-анализ); гибрид (Loki — операционка, ClickHouse — аналитика логов — dbt — две системы по ролям — выбор).
Как правильно выбирать лейблы для Loki?
Правило: лейбл = индексная колонка (низкое разнообразие — app/env/level/namespace/component — десятки значений — кардинальность); запрещены: id-подобные (trace_id/user_id/order_id — бесконечное разнообразие — индекс пухнет — «Loki тормозит» — почти всегда лейбл-кардинальность — pod-имена (k8s-churn — каждый рестарт — новый «ряд»); детали — в теле строки (json-поле — | json | order_id="X" — парсинг на запрос — медленнее лейбла, но индекс цел — баланс скорости и здоровья — структура); проверка дизайна: «сколько уникальных значений лейбла за месяц» — сотни — ок, тысячи+ — в тело; per-stream-лимиты Loki (per_stream_rate_limit — защита от флуда одного потока — алерт-защита — настройка — конфигурация); «лейбл convenient для дашборда» — не аргумент против кардинальности — переменные Grafana работают и с json-полями (Grafana — метрики-аналогия).
Как делать алерты на логи и когда это плохая идея? Механика: Loki ruler — metric-запрос (rate ошибок/паттернов) + условие → Alertmanager (алерты/Grafana — «паттерн в логах — уведомление» — быстро внедрить (минуты, без кода — метрики — первый путь); когда ок: редкие явные паттерны (panic/deadlock/OOM-строки — блокировки — «то, чего не должно быть никогда» — count > 0), быстрые временные алерты (легаси-сервис без метрик — мост); когда плохая: хрупкость формата (текст ошибки изменился в рефакторинге — алерт ослеп молча — «алерт есть, защиты нет» — критичное — метрикой в коде (метрики-инструментация — лог-алерт — дубль/временное), частотные условия (rate ошибок по логам — точнее из метрик (гистограммы — SLO — лог-парсинг под нагрузкой — ёмкость), «алерты на всё подряд» (fatigue — лог-шум × алерты — лавина — критерий действия — алерт-дизайн); тестирование: лог-алерт — тест-инъекцией (строка-паттерн в staging — сработал — chaos — тесты — «не проверенный алерт» — healthcheck).
Как связать Loki с трейсами и профилями?
Мост — trace_id: приложение пишет trace_id в каждую лог-строку (структура — OTel-контекст автоматически (OTel — logging-интеграция — «связь бесплатно при инструментации»); Grafana derived fields (настройка datasource: regex на trace_id → ссылка Tempo с ${__value.raw} — клик из строки лога → трейс — Tempo — обратный путь: трейс-спан → «logs for this span» (Tempo→Loki-запрос по trace_id — трейсинг-связка); профили (Pyroscope — окно инцидента из лога/трейса — «что горело в эти 30 секунд» — eBPF-контекст); полный путь расследования: алерт (метрика/лог) → SLO-дашборд → трейс ошибки → логи спана → профиль окна → фикс — минуты (incident — единые метки/идентификаторы — фундамент (Alloy-пайплайн — структура-меток — observability-стек окупается связностью, не отдельными частями — SRE).
Источники: документация Loki (query, labels, send data) — ссылки в шапке.