Введение в Grafana Loki: Логирование без боли

Курс закладывает фундамент понимания современной системы сбора логов через призму сравнения с традиционными решениями. Вы освоите уникальный подход Loki к индексации и запустите свою первую рабочую среду в Docker.

1. Философия Loki: почему индексация только метаданных меняет правила игры в сравнении с ELK

Философия Loki: почему индексация только метаданных меняет правила игры в сравнении с ELK

Высоконагруженный микросервис генерирует 10 000 строк логов в секунду. При среднем размере строки в 1 КБ это около 860 гигабайт сырых текстовых данных в сутки. Если система работает штатно, 99.9% этих строк никогда не будут прочитаны человеком. Однако традиционные системы логирования заставляют серверы тратить огромные вычислительные ресурсы на разбор, токенизацию и индексацию каждого слова в этих 860 гигабайтах еще до того, как они осядут на дорогих SSD-дисках. Это фундаментальный парадокс современного мониторинга: мы платим максимальную цену за запись данных, которые, скорее всего, нам не понадобятся, ради того, чтобы мгновенно найти их в тот редкий момент, когда произойдет сбой.

Чтобы понять, как Grafana Loki решает эту проблему, необходимо препарировать классический подход к логированию, безоговорочным лидером которого долгие годы оставался стек ELK (Elasticsearch, Logstash, Kibana).

Цена полнотекстового поиска

Elasticsearch под капотом является мощной поисковой системой, построенной на базе библиотеки Lucene. Его главная суперсила — инвертированный индекс.

Когда строка лога попадает в Elasticsearch, она не просто сохраняется на диск. Процессор начинает тяжелую работу. Возьмем типичный лог веб-сервера: [2023-10-27T15:04:05Z] ERROR [auth-service] User login failed: connection timeout to database 10.0.1.5

Анализатор Elasticsearch разбивает эту строку на токены (отдельные слова). Он отбрасывает знаки препинания, приводит слова к нижнему регистру и создает структуру данных, где каждое уникальное слово указывает на документ (строку лога), в котором оно встречается.

В индексе появляются записи:

  • error → документ #1492
  • auth → документ #1492
  • service → документ #1492
  • login → документ #1492
  • timeout → документ #1492
  • Если у вас миллиард строк, инвертированный индекс разрастается до колоссальных размеров. Зачастую размер индекса в Elasticsearch превышает размер самих исходных логов. Это приводит к трем критическим проблемам:

  • Высокое потребление CPU при записи: токенизация гигабайтов текста в реальном времени требует мощных процессоров.
  • Огромное потребление RAM: для быстрого поиска значительная часть индекса должна находиться в оперативной памяти.
  • Дорогое хранение: индексы требуют быстрого случайного доступа (Random Access), поэтому Elasticsearch нуждается в дорогих блочных хранилищах (SSD/NVMe). Использовать дешевое объектное хранилище напрямую для горячих данных невозможно.
  • В результате инфраструктура логирования становится сопоставимой по стоимости с инфраструктурой самого приложения.

    Радикальная идея Loki: отказ от чтения текста при записи

    Создатели Grafana Loki посмотрели на проблему под другим углом, вдохновившись архитектурой системы мониторинга Prometheus. В Prometheus метрики идентифицируются не длинными строковыми именами, а наборами пар «ключ-значение» — лейблами (labels). Например: cpu_usage{app="payment-gateway", env="production", region="eu-west"}.

    Философия Loki укладывается в одну фразу: индексировать нужно только метаданные, а сам текст лога оставлять в покое.

    !Сравнение архитектур обработки логов в Elasticsearch и Loki

    Когда та же самая строка лога [2023-10-27T15:04:05Z] ERROR [auth-service] User login failed... поступает в Loki, система не пытается читать сообщение. Агент сборщика логов (например, Promtail) заранее прикрепляет к этой строке несколько лейблов, описывающих контекст её происхождения. Например:

  • app = "auth-service"
  • env = "production"
  • cluster = "k8s-main"
  • Loki берет эти лейблы и строит по ним крошечный, легковесный индекс. Сам текст лога вместе с временной меткой (timestamp) просто сжимается в бинарный блок (chunk) алгоритмом gzip или snappy.

    Поскольку Loki не строит инвертированный индекс по тексту, ему не нужен быстрый случайный доступ к каждому слову. Сжатые чанки с логами можно сбрасывать в самые дешевые объектные хранилища — Amazon S3, Google Cloud Storage или MinIO. Индекс метаданных получается настолько маленьким, что легко помещается в оперативной памяти или недорогих базах данных.

    Следствие этой архитектуры — невероятная пропускная способность на запись. Loki может принимать терабайты логов во время DDoS-атаки или каскадного сбоя, не перегружая CPU, потому что ему не нужно парсить этот шквал сообщений. Он просто пакует их в архивы и отправляет в S3.

    Потоки (Streams): фундаментальная единица данных

    Чтобы система работала эффективно, метаданные должны быть структурированы. В Loki вводится понятие потока (stream).

    Поток — это уникальная комбинация всех лейблов. Любое изменение хотя бы одного значения лейбла создает совершенно новый поток.

    Рассмотрим пример. У нас есть три лог-записи:

  • app="frontend", env="prod" | msg="User clicked button"
  • app="frontend", env="prod" | msg="Page loaded"
  • app="frontend", env="dev" | msg="Debug: state updated"
  • Первая и вторая записи имеют идентичный набор лейблов. Они попадут в один и тот же поток. Loki будет накапливать эти строки в памяти, пока они не достигнут определенного размера (обычно несколько мегабайт) или не пройдет заданное время, после чего сожмет их в единый чанк и отправит в хранилище.

    Третья запись имеет лейбл env="dev". Это другая комбинация, а значит — совершенно другой поток. Для него будет создан свой собственный чанк.

    !Процесс распределения логов по потокам и формирование чанков

    Такая изоляция потоков имеет решающее значение для производительности. Если вы добавите в лейблы уникальный идентификатор пользователя (например, user_id="12345"), то каждый пользователь вашего сервиса создаст свой собственный поток. При 100 000 активных пользователей Loki попытается держать в памяти 100 000 открытых чанков. Это приведет к исчерпанию оперативной памяти и падению кластера. Эта проблема называется кардинальностью (cardinality), и управление ею — главный навык при работе с Loki. Лейблы должны описывать статический контекст (приложение, датацентр, окружение), а не динамические данные (IP-адреса, ID транзакций).

    Распределенный Grep: как работает поиск

    Если текст не проиндексирован, как найти ошибку тайм-аута в гигабайтах архивов? Критики Loki часто указывают на то, что поиск без инвертированного индекса — это медленный полный перебор (brute force). Технически это правда. Поиск в Loki — это, по сути, распределенный grep. Но дьявол кроется в деталях реализации.

    Когда вы выполняете запрос, процесс делится на два этапа.

    Этап 1: Фильтрация по индексу метаданных. Вы пишете запрос: найти слово "timeout" в приложении auth-service в окружении production. Loki обращается к своему маленькому индексу лейблов и мгновенно отсекает 99% данных. Ему не нужно сканировать логи фронтенда, базы данных, тестовых сред. Индекс сообщает: логи потока {app="auth-service", env="production"} за последние 4 часа лежат в конкретных 50 чанках в S3.

    Этап 2: Параллельное сканирование. Loki не сканирует эти 50 чанков последовательно. Он распределяет задачу между десятками или сотнями микросервисов (queriers) внутри своего кластера. Каждый процесс скачивает свой небольшой чанк из S3, распаковывает его в памяти и прогоняет через него регулярное выражение в поисках слова "timeout".

    Поскольку современные процессоры способны сканировать гигабайты текста в секунду, а задача распараллелена, результат возвращается пользователю за доли секунды. Вы получаете скорость, сопоставимую с Elasticsearch, но платите за вычислительные мощности только в момент чтения (которое происходит редко), а не в момент записи (которая происходит постоянно).

    Бесшовная корреляция с метриками

    Экономия на инфраструктуре — весомый аргумент для бизнеса, но для инженеров эксплуатации (SRE) и разработчиков главная ценность Loki заключается в другом. Это единая топология данных с Prometheus.

    В классическом стеке мониторинга метрики и логи живут в параллельных вселенных. Вы видите на графике в Grafana всплеск 500-х ошибок. График построен по метрикам Prometheus, которые имеют лейблы job="payment-api", instance="10.0.5.12". Чтобы посмотреть логи этого инцидента, вам нужно открыть Kibana, вспомнить, как в Elasticsearch называется индекс с логами платежей, и написать запрос, пытаясь отфильтровать нужный IP-адрес по полям, которые парсер Logstash назвал как-нибудь иначе (например, server_ip или host.address). Этот контекстный разрыв съедает драгоценные минуты во время аварии.

    Loki устраняет этот разрыв на фундаментальном уровне. Поскольку сборщик логов может использовать тот же механизм обнаружения сервисов (Service Discovery), что и Prometheus, логи получают в точности те же самые лейблы, что и метрики.

    Увидев аномалию на графике метрики http_requests_total{job="payment-api", instance="10.0.5.12"}, инженер одним кликом переключается в режим логов, и Grafana автоматически формирует запрос к Loki: {job="payment-api", instance="10.0.5.12"}. Контекст сохраняется идеально. Инструмент перестает быть препятствием между инженером и причиной сбоя.

    Границы применимости

    Философия отказа от индексации текста не делает Loki универсальным решением. Это специализированный инструмент для операционного логирования и траблшутинга.

    Если бизнес-аналитикам требуется строить сложные агрегации по логам — например, «посчитать среднюю сумму покупок по городам из JSON-логов за последний год» — Loki окажется плохим выбором. Сканирование и парсинг JSON «на лету» для терабайтов исторических данных без предварительного индекса займет слишком много времени. Для таких задач (Business Intelligence, Security Information and Event Management) ELK-стек с его детальным разбором полей и инвертированным индексом остается непревзойденным стандартом.

    Смещение фокуса с тотальной индексации на умное управление метаданными — это признание суровой реальности: логи современных распределенных систем слишком объемны, чтобы обращаться с ними как с ценной библиотекой, где каждая буква занесена в картотеку. Loki предлагает относиться к ним как к сырому потоку событий, где ярлыки на контейнерах важнее содержимого — до тех пор, пока содержимое действительно не понадобится для расследования инцидента.

    2. Концепция лейблов и потоков: как правильно структурировать логи для высокой производительности

    Запрос {app="billing-service"} возвращает результаты за 40 миллисекунд. Тот же запрос, но с добавлением, казалось бы, безобидного уточнения {app="billing-service", status="500"}, заставляет систему «думать» две минуты, после чего Grafana выдает ошибку тайм-аута. Разница между молниеносным ответом и отказом кластера часто кроется не в объеме логов, а в одной архитектурной ошибке — неправильном понимании того, какие данные имеют право становиться лейблами.

    Анатомия потока в оперативной памяти

    В основе архитектуры Loki лежит компонент Ingester — узел, принимающий входящие логи. Когда строка лога поступает в систему, Ingester не пишет ее сразу на диск. Он вычисляет хеш от комбинации всех прикрепленных к строке лейблов. Этот хеш становится уникальным идентификатором потока (stream).

    В оперативной памяти Ingester выделяет под каждый активный поток отдельный буфер. Строки логов сжимаются и накапливаются в этом буфере, пока не будет достигнут один из двух лимитов:

  • Оптимальный размер (обычно около 1.5 МБ в сжатом виде, что соответствует 10–15 МБ сырого текста).
  • Максимальное время жизни в памяти (часто 1–2 часа).
  • Как только лимит достигнут, буфер «закрывается», превращается в неизменяемый чанк (chunk) и сбрасывается в долговременное хранилище (например, AWS S3).

    !Структура буферизации потоков в Ingester

    Проблема высокой кардинальности, убивающая производительность, возникает именно на этапе буферизации. Если потоков слишком много, Ingester вынужден держать в оперативной памяти сотни тысяч открытых буферов. RAM быстро исчерпывается. Но даже если памяти хватает, возникает вторичный эффект: потоки наполняются слишком медленно.

    Если поток получает одну строку лога в минуту, он никогда не достигнет оптимального размера в 1.5 МБ. Сработает тайм-аут по времени, и Ingester запишет в хранилище «микро-чанк» размером в несколько килобайт. При чтении Loki придется скачивать из S3 не один крупный файл, а тысячи мелких, что создает колоссальный overhead на сетевые запросы и распаковку, замедляя поиск в десятки раз.

    Математика взрыва кардинальности

    Количество потоков в системе не складывается, а перемножается. Общее число активных потоков вычисляется как произведение количества уникальных значений каждого лейбла:

    Где — количество уникальных значений -го лейбла.

    Рассмотрим микросервис, развернутый в трех дата-центрах, имеющий 10 подов (экземпляров) в каждом. Базовый набор лейблов:

  • app = "payment-gateway" (1 значение)
  • dc = "eu-west", "us-east", "ap-south" (3 значения)
  • pod = "pod-1" ... "pod-10" (10 значений)
  • Итого: потоков. Это идеальная ситуация. Ingester легко держит 30 буферов в памяти, они быстро наполняются и формируют крупные, эффективные для чтения чанки.

    Теперь разработчик решает, что ему удобно фильтровать логи по HTTP-статусу ответа, и добавляет лейбл status (например, 200, 404, 500). Допустим, сервис возвращает 10 разных статусов. Количество потоков становится: . Это все еще приемлемо.

    Но затем принимается фатальное решение — добавить в лейблы ID клиента (tenant_id), чтобы быстро искать логи конкретного пользователя. У сервиса 50 000 активных клиентов. Новое количество потоков: .

    !Калькулятор кардинальности потоков

    Ingester попытается создать 15 миллионов буферов в оперативной памяти. Процесс будет убит операционной системой (OOM Killer) в течение нескольких минут. Кластер упадет.

    Золотое правило: Статические vs Динамические данные

    Чтобы система работала стабильно, необходимо жестко разделять метаданные на статические и динамические.

    Статические данные описывают физическую или логическую топологию инфраструктуры. Они не меняются на протяжении жизни процесса (контейнера). Именно они и только они должны становиться лейблами. Примеры правильных лейблов:

  • cluster (имя Kubernetes-кластера)
  • namespace (логическое окружение: prod, stage, dev)
  • app или container (имя микросервиса)
  • host или node (имя физического сервера)
  • Динамические данные описывают контекст конкретного запроса или события. Они меняются от строки к строке. Им категорически запрещено быть лейблами. Примеры данных, которые должны оставаться внутри текста лога:

  • user_id, tenant_id, session_id
  • trace_id, span_id
  • ip_address
  • Пути запросов (/api/v1/users/123)
  • Единственное исключение из правила динамических данных — лейбл уровня логирования (level="info", level="error"). Хотя уровень меняется от строки к строке, его множество строго ограничено (обычно 5–6 значений). Умножение общего числа потоков на 5 не приводит к взрыву кардинальности, но дает огромное преимущество: при инциденте можно мгновенно отфильтровать только ошибки, отбросив 95% чанков с информационными сообщениями еще до этапа чтения.

    Парадигма «Парсинг при чтении»

    Отказ от динамических лейблов вызывает закономерный вопрос: если мы не индексируем user_id или status_code, как найти ошибки 500 для конкретного пользователя, не ожидая результатов часами?

    Loki решает эту задачу за счет грубой вычислительной силы (brute-force) и распараллеливания. Вместо того чтобы поддерживать тяжелый индекс на этапе записи, система переносит нагрузку на этап чтения.

    Допустим, приложение пишет логи в формате JSON: {"time":"2023-11-01T10:00:00Z", "level":"error", "status":500, "user_id":"A-893", "msg":"connection timeout"}

    В Loki эта строка попадет в поток с лейблами {app="payment", namespace="prod"}. Метаданных о статусе или пользователе в индексе нет.

    Когда вы выполняете запрос для поиска ошибок конкретного пользователя, процесс выглядит так:

  • Фильтрация по индексу: Loki смотрит в индекс лейблов и находит все чанки, принадлежащие потоку {app="payment", namespace="prod"} за указанный период времени.
  • Параллельный grep: Найденные чанки (например, 100 файлов по 1.5 МБ) распределяются между десятками воркеров (Queriers).
  • Распаковка и парсинг на лету: Воркеры загружают чанки в память, распаковывают их и применяют фильтры непосредственно к тексту.
  • Синтаксис LogQL позволяет на лету извлечь поля из JSON и отфильтровать их: {app="payment", namespace="prod"} | json | status == 500 and user_id == "A-893"

    Поскольку чанки сгруппированы по приложениям и сжаты, а поиск распараллелен, Loki способен просматривать гигабайты логов в секунду. Вы жертвуете долями секунды при поиске, но экономите терабайты RAM и CPU на индексации.

    Компромиссы и граничные случаи

    Подход «парсинг при чтении» отлично работает для 90% задач траблшутинга. Однако существуют сценарии, где грубая сила дает сбой.

    Если микросервис генерирует 500 ГБ логов в час, а вам нужно найти одну транзакцию по trace_id, полное сканирование чанков этого сервиса займет слишком много времени, даже при максимальном распараллеливании. В таких случаях возникает соблазн вынести trace_id в лейблы, что неминуемо приведет к падению кластера.

    Для решения подобных задач применяются архитектурные компромиссы:

  • Разделение потоков (Stream sharding): Искусственное дробление большого потока на несколько меньших с помощью ограниченного хэширования. Например, можно создать лейбл shard, значение которого вычисляется как hash(trace_id) % 10. Это увеличит количество потоков ровно в 10 раз (контролируемый рост), но позволит при поиске конкретного трейса сканировать только 1/10 часть всех логов приложения.
  • Structured Metadata (Метаданные без индексации): В новых версиях Loki появилась возможность прикреплять к строкам логов структурированные метаданные (пары ключ-значение), которые не участвуют в формировании потока и не попадают в индекс, но хранятся в чанке в оптимизированном бинарном виде. Это позволяет фильтровать логи по trace_id значительно быстрее, чем при парсинге сырого JSON, не увеличивая кардинальность.
  • Синхронизация топологии с метриками

    Главный критерий правильно спроектированной схемы лейблов в Loki — ее полная идентичность топологии метрик в Prometheus.

    Если в Prometheus метрика потребления памяти выглядит как container_memory_usage_bytes{cluster="eu-1", namespace="backend", pod="auth-service-7b8c"}, то логи этого контейнера в Loki должны иметь абсолютно идентичный набор лейблов: {cluster="eu-1", namespace="backend", pod="auth-service-7b8c"}.

    Такая симметрия позволяет Grafana бесшовно связывать графики метрик с логами. При обнаружении всплеска ошибок на графике, система может автоматически сгенерировать запрос в Loki, передав ему тот же контекст лейблов, который использовался для построения графика. Если топологии расходятся (например, в метриках используется лейбл pod, а в логах container_name), магия автоматической корреляции ломается, и инженеру приходится вручную переписывать запросы при переходе от дашборда к логам.

    3. Быстрый старт в Docker: развертывание связки Loki и Grafana для получения первых результатов

    Быстрый старт в Docker: развертывание связки Loki и Grafana для получения первых результатов

    Когда микросервисов становится больше трех, команда docker logs -f container_name превращается в неэффективный инструмент. Разработчику приходится открывать несколько терминалов, визуально сопоставлять временные метки и пытаться выловить корреляцию между ошибкой в API-шлюзе и падением запроса к базе данных. Чтобы перейти от хаоса разрозненных текстовых выводов к централизованному поиску, требуется минимально жизнеспособный стенд. Развертывание такого стенда локально позволяет понять механику работы системы до того, как она обрастет сложными балансировщиками и распределенными хранилищами.

    Топология локального стенда

    Для получения первого работающего результата нам потребуется развернуть три взаимосвязанных компонента. Эта триада является классическим паттерном наблюдаемости (observability) в экосистеме Grafana.

  • Loki — серверная часть (backend). Принимает логи, индексирует их метаданные, сжимает текст в чанки и сохраняет на диск. В локальном варианте Loki будет работать в монолитном режиме (один процесс выполняет все роли: от приема до поиска).
  • Promtail — агент сбора (collector). Устанавливается там, где генерируются логи. Его задача — найти файлы с логами, прочитать новые строки, прикрепить к ним заранее заданные лейблы и отправить по HTTP в Loki.
  • Grafana — пользовательский интерфейс (frontend). Подключается к Loki как к источнику данных, предоставляет удобную строку поиска, строит графики и таблицы.
  • !Схема локального стенда: Promtail, Loki, Grafana

    Чтобы процесс был максимально прозрачным для понимания, мы не будем сразу использовать сложные механизмы перехвата логов на уровне демона Docker. Вместо этого мы создадим контейнер-генератор, который будет просто писать текстовые строки в обычный файл. Promtail будет читать этот файл и отправлять данные в Loki. Это изолирует концепцию сбора логов от специфики контейнеризации.

    Подготовка конфигурационных файлов

    Все компоненты будут запущены через Docker Compose. Для этого необходимо создать отдельную директорию (например, loki-quickstart) и разместить в ней три файла: конфигурацию Loki, конфигурацию Promtail и сам манифест Compose.

    Конфигурация Loki (loki-config.yaml)

    Создайте файл loki-config.yaml. Это минимально необходимый набор настроек для запуска базы данных без внешних зависимостей вроде AWS S3 или Consul.

    Ключевой параметр здесь — auth_enabled: false. В production-среде Loki поддерживает мультитенантность (разделение данных разных команд или клиентов). При включенной авторизации каждый запрос обязан содержать HTTP-заголовок X-Scope-OrgID. Отключение этого параметра переводит Loki в режим единого глобального тенанта, что идеально для локальной разработки.

    Блок schema_config определяет, как именно данные будут сохраняться на диск. Значение object_store: filesystem указывает, что сжатые чанки с логами будут лежать в обычной папке локальной файловой системы контейнера, а store: boltdb-shipper определяет формат хранения самого индекса.

    Конфигурация Promtail (promtail-config.yaml)

    Следующий файл — promtail-config.yaml. Он объясняет агенту, где искать логи и куда их отправлять.

    В этом файле скрыты две важнейшие механики: Во-первых, блок positions. Promtail не читает весь файл логов заново при каждом перезапуске. Он постоянно записывает смещение (в байтах) для каждого отслеживаемого файла в служебный файл positions.yaml. Если контейнер Promtail перезагрузится, он прочитает этот файл и продолжит отправку ровно с того места, где остановился.

    Во-вторых, блок scrape_configs. Мы используем статический поиск файлов по маске, указанной в служебном лейбле __path__. Все строки, найденные в файлах директории /var/log/app/, будут отправлены в Loki с двумя статичными лейблами: job="dummy-generator" и env="local".

    Манифест инфраструктуры (docker-compose.yml)

    Теперь объединим компоненты, добавив генератор логов. Создайте файл docker-compose.yml:

    Обратите внимание на механизм передачи файлов. Сервис log-generator (базовый образ Alpine Linux) запускает бесконечный цикл, который каждые 2 секунды дописывает новую строку в файл /var/log/app/output.log. Эта директория смонтирована как Docker Volume (shared-logs). Тот же самый Volume подключен к контейнеру promtail. Таким образом, один контейнер пишет в файл, а другой в реальном времени его читает.

    Запуск и верификация компонентов

    Находясь в директории с файлами, выполните команду запуска в фоновом режиме:

    Прежде чем переходить в графический интерфейс, необходимо убедиться, что база данных Loki успешно стартовала и готова принимать запросы. Loki предоставляет специальный HTTP-endpoint для проверки состояния. Откройте браузер или используйте утилиту curl:

    Если система инициализировалась корректно (обычно это занимает 3–5 секунд), вы получите текстовый ответ ready. Если ответ отличается или соединение сбрасывается, необходимо проверить логи самого контейнера Loki (docker logs loki). Чаще всего проблемы на этом этапе связаны с опечатками в YAML-конфигурации.

    Настройка Grafana и первый запрос

    Когда все контейнеры работают, настало время замкнуть контур: подключить визуализатор к базе данных.

  • Откройте в браузере http://localhost:3000.
  • Авторизуйтесь с учетными данными по умолчанию (логин: admin, пароль: admin). Grafana предложит сменить пароль — для локального стенда этот шаг можно пропустить.
  • В левом меню перейдите в раздел ConnectionsData sources и нажмите Add data source.
  • Выберите Loki из списка.
  • В поле URL введите http://loki:3100. Обратите внимание: мы используем внутреннее DNS-имя контейнера (loki), так как Grafana и Loki находятся в одной Docker-сети loki-net. Использовать localhost здесь нельзя, так как для контейнера Grafana localhost — это он сам.
  • Прокрутите вниз и нажмите Save & test. Должно появиться зеленое уведомление «Data source successfully connected».
  • !Жизненный цикл лога от контейнера до дашборда

    Интеграция завершена. Теперь можно переходить к чтению данных. В левом меню Grafana выберите иконку компаса — раздел Explore. Это основной аналитический интерфейс для работы с неструктурированными данными.

    В верхней части экрана убедитесь, что в выпадающем списке выбран источник данных Loki. Ниже расположена строка ввода запросов. Введите следующий базовый запрос:

    {job="dummy-generator"}

    Нажмите кнопку Run query (или сочетание Shift+Enter). В нижней части экрана появится гистограмма распределения логов по времени, а под ней — сами текстовые строки, которые генерирует наш Alpine-контейнер.

    Разбор механики: что мы получили

    Наблюдая строки логов в интерфейсе Grafana, важно осознать, как именно отработала архитектура, заложенная на предыдущих этапах изучения.

    Выполнив запрос {job="dummy-generator"}, вы обратились к инвертированному индексу Loki. Loki не сканировал текст файлов на диске, чтобы найти эти логи. Он мгновенно определил, в каких сжатых чанках лежат данные, привязанные к метке job="dummy-generator", загрузил только их, распаковал и отдал в Grafana.

    Кликните на любую строку лога в интерфейсе Explore. Откроется панель деталей (Log details). Вы увидите секцию Labels, в которой будут значения env: local и job: dummy-generator. Это те самые метаданные, которые мы жестко зафиксировали в файле promtail-config.yaml. Независимо от того, какой текст написал генератор, эти ярлыки намертво приклеены к потоку (stream).

    Также в деталях лога можно заметить, что текст содержит динамические данные: временную метку и случайный ID (ID=12345). На текущем этапе Loki воспринимает всю эту строку как единый монолитный кусок текста. Если потребуется найти события с конкретным ID, придется использовать фильтрацию текста (grep), которая будет выполняться на лету при чтении.

    Развернутый стенд доказывает работоспособность концепции: агент читает файл, прикрепляет метаданные, отправляет в хранилище, а UI позволяет их извлекать. Однако в реальной инфраструктуре на сотни контейнеров никто не монтирует общие тома для чтения текстовых файлов. Для автоматического сбора логов со всех сервисов и динамического назначения лейблов (на основе имен контейнеров, их статусов и принадлежности к сетям) применяются более продвинутые механизмы интеграции с рантаймом, которые требуют отдельного глубокого изучения.