Перейти к содержанию

Как читать логи в Loki

Шпаргалка по LogQL: где смотреть, как фильтровать, как коррелировать с трейсами. Полный reference по формату логов — в ../conventions/logging.md.

Содержание

Где логи

Все prod-логи централизованы в Loki. UI — Grafana → Explore → datasource Loki.

Все сервисы пишут JSON в stdout, контейнерный log driver кладёт поток в Loki. Формат записи зафиксирован в ../conventions/logging.md: service, request_id, correlation_id, trace_id, user_id, route, level и прочее — обязательные структурные поля.

Основы LogQL

Базовая форма: {<label-selector>} <pipeline>.

Выбор потока по labels

{service="review"}

Labels — это метаданные Loki (service, pod, namespace), не поля JSON-payload'а. Индексируются заранее, поэтому фильтруй сначала по ним.

Текстовый фильтр

{service="review"} |= "error"           # substring
{service="review"} != "health"          # отрицание
{service="review"} |~ "(?i)panic"       # regex, case-insensitive
{service="review"} !~ "^debug"          # отрицание regex

Комбинации

{service="review", namespace="prod"} |= "outbox" != "healthz"

Структурированный поиск по JSON

Каждая запись — JSON; распарсить можно через | json:

{service="review"} | json

Дальше — фильтры по полям:

{service="review"} | json | user_id="42"
{service="review"} | json | status_code >= 500
{service="review"} | json | duration_ms > 1000
{service="review"} | json | level="ERROR"

Комбинируй:

{service="review"}
  | json
  | level="ERROR"
  | route="/v1/reviews"
  | user_id="42"

Line format для удобочитаемости

{service="review"}
  | json
  | level="ERROR"
  | line_format "{{.time}} {{.request_id}} {{.err}}"

Rate и агрегация

Count за период

count_over_time({service="review"} |= "level=ERROR" [5m])

Rate per second

rate({service="review"} |= "level=ERROR" [5m])

Группировка

sum by (status_code) (
  rate({service="review"} | json [5m])
)

Топ-N по error message

topk(10,
  sum by (msg) (
    count_over_time({service="review"} | json | level="ERROR" [15m])
  )
)

Временные диапазоны

  • UI по-умолчанию — последний час. Меняй через селектор в правом верхнем углу Grafana.
  • URL-параметры&from=1713830400000&to=1713834000000 (ms epoch). Удобно шарить ссылку с инцидентом.
  • Long-term (> 30 дней) — может быть недоступно из-за Loki retention. Для исторического анализа — экспорт или отдельный storage.

Типовые запросы

Ошибки конкретного сервиса за 15 минут

{service="review"} | json | level="ERROR"

Все логи конкретного user за час

{service=~".+"} | json | user_id="42"

Полезно при обращении в поддержку «у пользователя 42 что-то не работает» — собираешь полную трассу его действий по всем сервисам.

Все логи одного trace

{service=~".+"} | json | trace_id="4bf92f3577b34da6a3ce929d0e0e4736"

Переход с trace на логи — основной способ распутать distributed инцидент. См. ./read-traces.md.

Latency spike debug

{service="review"}
  | json
  | duration_ms > 500
  | line_format "{{.time}} {{.route}} {{.duration_ms}}ms {{.request_id}}"

Outbox lag в конкретный момент

{service="review"}
  | json
  | msg="outbox forwarder tick"
  | unacked != "0"

Kafka consumer ошибки

{service="notification"}
  | json
  | msg="kafka consume"
  | level=~"WARN|ERROR"

Все HTTP 5xx за час

{service=~".+"}
  | json
  | status_code >= 500
  | line_format "{{.service}} {{.route}} {{.status_code}} {{.request_id}}"

Correlation с traces

Loki ↔ Tempo интеграция настроена в Grafana datasource'ах.

  • Из Tempo в Loki. Открыл trace → каждый span имеет кнопку Logs for this span. Под капотом запрос вида {service="<span.service>"} | json | trace_id="<trace>".
  • Из Loki в Tempo. В JSON-записи, где виден trace_id, в раскрытом представлении кнопка View trace открывает trace в Tempo.

Это базовый workflow отладки распределённого запроса:

  1. Grafana dashboard показывает аномалию (p99 latency spike).
  2. Exemplar на графике → прыжок в Tempo с конкретным trace.
  3. Из trace → в Loki по trace_id → полный контекст каждого span'а.

Локальные логи (dev)

Когда Loki не доступен (локальная разработка, standalone-запуск):

docker compose logs -f review | jq 'select(.level=="ERROR")'
docker compose logs -f review | jq 'select(.user_id==42)'
docker compose logs -f review | jq 'select(.trace_id=="abc123")'

Для plain-text в dev используется SERVICE_LOG_FORMAT=text, если сервис поддерживает (см. ../conventions/logging.md). В prod — только JSON, jq не нужен.

Troubleshooting

«Нет данных за N минут»

  1. Проверь, что селектор labels соответствует живым потокам: {service=~".+"} без других фильтров за те же 5 минут.
  2. Если и этого нет — проблема в ingester Loki, эскалируй SRE.

Slow queries

  • Сузь time range до минимально нужного.
  • Добавь label-фильтр (namespace, service) — Loki не ищет в потоках, которые ты не выбрал.
  • Вынеси | json после всех текстовых фильтров: |= и != отрабатывают до парсинга и дёшевы.

«No logs found»

  • Проверь правильность имени сервиса в label: {service="review"}, не {service="review-service"}. Значение label приходит из newLogger(...)With("service", "review") — см. ../conventions/logging.md.
  • Проверь time range. Инцидент был час назад, а ты смотришь «последние 15 минут» — данных нет.

Только часть полей доступна после | json

Loki по умолчанию извлекает «плоский» JSON. Вложенные объекты надо доставать явно:

{service="review"} | json user="user_id"

Anti-patterns

  • {service=~".+"} на всё без time-window. Запрос идёт по всему кластеру на длительный период — timeout'ится и нагружает ingester.
  • Полнотекстовый regex без label-selector. |~ "foo" на всём — то же самое.
  • Поиск по msg substring вместо label. Если одна и та же строка пишется в десятке мест — фильтруй по дополнительному JSON-полю (event_type, route), а не по куску msg.
  • Зависимость от поля, которого нет. | json | err != "" падает, если в записи поля err нет. Используй | err="" наоборот — LogQL нормально трактует отсутствие.
  • Логика через логи. Если алерт рассчитывается LogQL-ом на каждое срабатывание — это гораздо дороже, чем counter-метрика. Метрики — в Prometheus, логи — для контекста.

Связанные разделы