# Стандарт централизованного сбора логов: PLG + Prometheus **Стек:** Loki · Grafana Alloy · Grafana · kube-prometheus-stack **Namespace:** `monitoring` **Доступ:** `http://10.203.0.96:10003` (Traefik port 10003) **Исследование:** [`research/logging/plg.md`](../../research/logging/plg.md) --- ## Архитектура ``` ┌──────────────────────────────────────────────────────────────────┐ │ k8s-worker-01 / k8s-master-01 │ │ [Pod stdout] → /var/log/pods/ ← Alloy: loki.source.k8s │ │ [journald] → kubelet, containerd ← Alloy: loki.source.journal│ │ [node-exporter DaemonSet] ← CPU / RAM / диск / сеть │ │ │ │ [Grafana Alloy DaemonSet] (namespace: monitoring) │ └────────────┬──────────────────────────────────────────────────────┘ │ HTTP push (loki.write) ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Loki single-binary (namespace: monitoring) │ │ S3 backend: MinIO bucket loki-chunks │ │ 10 GiB PVC longhorn — только WAL/temp (не логи) │ │ service/loki:3100 │ └─────────────────────┬────────────────────────────────────────────┘ │ S3 API ▼ ┌──────────────────────────────────────────────────────────────────┐ │ MinIO (namespace: minio) — bucket: loki-chunks │ │ StorageClass: longhorn-minio (Retain, 1 TiB) │ │ Пользователь: loki (только bucket loki-chunks) │ └──────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────────┐ │ kube-prometheus-stack (namespace: monitoring) │ │ Prometheus PVC 20 GiB · node-exporter · kube-state-metrics │ │ ~27 готовых дашбордов кластера → ConfigMaps → Grafana sidecar │ └─────────────────────┬────────────────────────────────────────────┘ │ datasource ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Grafana (namespace: monitoring) │ │ Datasource: Loki (isDefault) + Prometheus (provisioned) │ │ Sidecar: подхватывает ConfigMaps grafana_dashboard=1 (ALL ns) │ │ PVC: 5 GiB longhorn │ │ Traefik → port 10003 → http://10.203.0.96:10003 │ └──────────────────────────────────────────────────────────────────┘ ``` ### Компоненты | Компонент | Chart | Версия | Примечание | |---|---|---|---| | Loki | `grafana/loki` | 7.0.0 | single-binary, S3 backend, retention 31d | | Grafana Alloy | `grafana/alloy` | 1.8.2 | DaemonSet; **Promtail EOL с 2026-03-02** | | kube-prometheus-stack | `prometheus-community/kube-prometheus-stack` | 68.4.4 | Prometheus + node-exporter + kube-state-metrics | | Grafana | `grafana/grafana` | 10.5.15 | provisioned Loki + Prometheus datasources, sidecar | > **Promtail не используется** — объявлен EOL 2 марта 2026. Grafana Alloy — официальная замена. --- ## Предварительные условия ### 1. Создать namespace и Secret с паролем Grafana ```bash kubectl create namespace monitoring kubectl create secret generic grafana-admin-secret \ --from-literal=admin-user=admin \ --from-literal=admin-password='СИЛЬНЫЙ_ПАРОЛЬ' \ -n monitoring ``` Секрет создаётся **вручную** — пароль не хранится в Ansible-переменных и не попадает в Git. Если секрет не создать до запуска playbook, задача `Grafana | Assert grafana-admin-secret exists` упадёт с подробным сообщением об ошибке. ### 2. Задать переменную окружения | Переменная | Описание | |---|---| | `LOKI_MINIO_PASSWORD` | Пароль пользователя `loki` в MinIO — Ansible создаст пользователя с этим паролем | | `GRAFANA_ADMIN_PASSWORD` | Не читается Ansible — только для справки при ручном создании Secret выше | Пароль `LOKI_MINIO_PASSWORD` можно посмотреть в любой момент: ```bash kubectl get secret loki-minio-secret -n monitoring \ -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d ``` --- ## Запуск ```bash # Локально (с заданной переменной окружения) LOKI_MINIO_PASSWORD='ПАРОЛЬ_LOKI' \ ansible-playbook -i inventory/prod playbooks/setup_logging.yml # Через GitLab CI/CD # → Pipelines → setup:logging (ручной запуск, ветка main) ``` Playbook выполняет задачи в порядке: 1. Создаёт namespace `monitoring` 2. Создаёт bucket `loki-chunks` в MinIO, пользователя `loki`, политику доступа, Secret `loki-minio-secret` 3. Устанавливает Loki (Helm) 4. Устанавливает Grafana Alloy (Helm) 5. Устанавливает kube-prometheus-stack (Helm) — Prometheus + node-exporter + kube-state-metrics + ~27 дашбордов 6. Устанавливает Grafana (Helm) — проверяет наличие `grafana-admin-secret` перед установкой Playbook идемпотентен: повторный запуск пропустит уже выполненные шаги. --- ## Конфигурация ### Loki: параметры хранения Loki работает в режиме **single-binary**. Логи хранятся в MinIO (`loki-chunks`), не на диске. PVC 10 GiB используется только для WAL и временных файлов компактора. ```yaml # roles/logging/defaults/main.yml (переопределять в group_vars при необходимости) loki_retention_days: 31 # срок хранения логов loki_wal_storage_size: "10Gi" # PVC только для WAL loki_wal_storage_class: longhorn loki_minio_bucket: loki-chunks ``` Расчёт объёма для текущего кластера (2 ноды, ~20 подов): - ~170 MB/day после сжатия Loki (~10:1) - 31 дней → ~5 GB в bucket `loki-chunks` - MinIO PVC 1 TiB → запас до ~100 нод ### Alloy: сбор логов Alloy запускается как **DaemonSet** (по одному поду на каждую ноду, включая control plane). Что собирает: - `/var/log/pods/**/*.log` — все контейнеры через `loki.source.kubernetes` - `journald` — kubelet, containerd, sshd через `loki.source.journal` Фактические метки на каждой записи в Loki: | Метка | Источник | Описание | |---|---|---| | `namespace` | Kubernetes API | namespace пода | | `pod` | Kubernetes API | имя пода | | `container` | Kubernetes API | имя контейнера | | `app` | label пода | значение `app` label | | `level` | JSON-парсинг | уровень лога (info/warn/error) | | `job` | константа | `systemd-journal` для journald-логов | | `node` | journald | hostname ноды (только journald) | | `unit` | journald | systemd unit (только journald) | | `stream` | containerd | `stdout` или `stderr` | Debug-записи (label `level=debug`) отбрасываются на уровне агента — не попадают в Loki. ### Prometheus: метрики кластера ```yaml # roles/logging/defaults/main.yml prometheus_stack_chart_version: "68.4.4" prometheus_storage_size: "20Gi" prometheus_storage_class: longhorn prometheus_retention: "30d" ``` kube-prometheus-stack устанавливается с отключённой встроенной Grafana (`grafana.enabled: false`) и `forceDeployDashboards: true` — создаёт ~27 ConfigMaps с дашбордами, которые Grafana sidecar подхватывает автоматически. > **Rocky Linux 9 / kubeadm gotcha:** `controller-manager` и `scheduler` по умолчанию слушают только `127.0.0.1` → их метрики в Prometheus будут недоступны (таргеты Down). Остальные таргеты работают без изменений. Для исправления нужен ручной патч `/etc/kubernetes/manifests/kube-controller-manager.yaml` и `kube-scheduler.yaml`: замена `--bind-address=127.0.0.1` на `--bind-address=0.0.0.0`. ### Grafana: доступ и дашборды URL: `http://10.203.0.96:10003` Логин: `admin` / пароль из Secret `grafana-admin-secret` Datasources provisioned при старте — не требуют ручной настройки: - **Loki** — `http://loki.monitoring.svc.cluster.local:3100` (isDefault) - **Prometheus** — `http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090` **Дашборды (загружаются автоматически через sidecar):** | Дашборд | Источник | Описание | |---|---|---| | **Kubernetes Logs** | ConfigMap `grafana-dashboard-kubernetes-logs` | Фильтрация по namespace / pod / container / level, график интенсивности + поток логов | | Kubernetes / Compute Resources / Cluster | kube-prometheus-stack | CPU/RAM по namespace | | Kubernetes / Compute Resources / Node (Pods) | kube-prometheus-stack | Ресурсы по подам на ноде | | Node Exporter / Nodes | kube-prometheus-stack | CPU, RAM, диски, сеть нод | | Kubernetes / Persistent Volumes | kube-prometheus-stack | Статус и заполнение PVC | | Kubernetes / API server | kube-prometheus-stack | Latency, error rate, RPS | | ...и ещё ~22 дашборда | kube-prometheus-stack | Kubelet, etcd, сеть, workloads | > **Дашборды Grafana.com с ID 15141, 13639, 18748 не работают** с Grafana 12.x — используют устаревший API плагина. Не импортировать. --- ## Диагностика ### Проверка после установки ```bash # Статус всех подов стека kubectl get pod -n monitoring # Loki готов (запрос через временный под — в контейнере нет curl/wget) kubectl run loki-check --image=curlimages/curl --restart=Never -n monitoring \ -- curl -s http://loki:3100/ready sleep 5 && kubectl logs -n monitoring loki-check kubectl delete pod loki-check -n monitoring # Метки, которые реально есть в Loki kubectl run loki-labels --image=curlimages/curl --restart=Never -n monitoring \ -- curl -s 'http://loki:3100/loki/api/v1/labels' sleep 5 && kubectl logs -n monitoring loki-labels kubectl delete pod loki-labels -n monitoring # Alloy открыл потоки логов со всех подов (нет level=error) kubectl logs -n monitoring -l app.kubernetes.io/name=alloy --tail=50 | grep -v "opened log stream" # Данные появились в MinIO kubectl exec -n minio deployment/minio -- \ mc ls local/loki-chunks --recursive | head -10 ``` ### Loki CrashLoopBackOff при старте **Симптом 1:** `mkdir /loki/compactor: read-only file system` Причина: неверный путь compactor. Должен быть `/var/loki/compactor`, не `/loki/compactor`. Проверить в ConfigMap: `kubectl get configmap loki -n monitoring -o jsonpath='{.data.config\.yaml}' | grep working_directory` **Симптом 2:** `SignatureDoesNotMatch` или `InvalidAccessKeyId` Причина: env var expansion `${VAR}` в Loki 3.x не работает для S3 credentials в config-файле. Решение: пароль передаётся через `--set loki.storage.s3.secret_access_key=...` в helm-команде (реализовано в `tasks/loki.yml`). **Симптом 3:** `error running loki` при повторном запуске после CrashLoop + `another operation in progress` Причина: helm upgrade завис в предыдущей попытке. ```bash helm rollback loki -n monitoring # затем повторить helm upgrade ``` **Предупреждение `loki-memberlist: no such host`** — некритично для single-binary, можно игнорировать. ### Loki 500 при запросе через Grafana ```bash # Проверить пользователя loki в MinIO MINIO_POD=$(kubectl get pod -n minio -l app=minio -o jsonpath='{.items[0].metadata.name}') kubectl exec -n minio $MINIO_POD -- mc admin user info local loki # Проверить Secret и его содержимое kubectl get secret loki-minio-secret -n monitoring kubectl get secret loki-minio-secret -n monitoring \ -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d # Тест доступа с теми же учётными данными SECRET=$(kubectl get secret loki-minio-secret -n monitoring \ -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d) kubectl exec -n minio $MINIO_POD -- mc alias set lokitest http://localhost:9000 loki "$SECRET" kubectl exec -n minio $MINIO_POD -- mc ls lokitest/ ``` ### Alloy не отправляет логи ```bash # Ошибки в логах Alloy kubectl logs -n monitoring -l app.kubernetes.io/name=alloy --tail=100 \ | grep -i "error\|level=error" # Loki endpoint доступен из пода Alloy kubectl exec -n monitoring -l app.kubernetes.io/name=alloy -c alloy -- \ wget -qO- http://loki.monitoring.svc.cluster.local:3100/ready 2>&1 ``` ### Grafana не видит данные (No Data) ```bash # Проверить что Loki datasource здоров после рестарта Grafana kubectl rollout restart deployment/grafana -n monitoring kubectl rollout status deployment/grafana -n monitoring # Проверить UID datasource (нужен для ручного исправления импортированных дашбордов) GRAFANA_PASS=$(kubectl get secret grafana-admin-secret -n monitoring \ -o jsonpath='{.data.admin-password}' | base64 -d) kubectl run gf-check --image=curlimages/curl --restart=Never -n monitoring \ -- curl -s -u "admin:${GRAFANA_PASS}" http://grafana/api/datasources sleep 5 && kubectl logs -n monitoring gf-check | python3 -m json.tool | grep -E '"uid"|"name"' kubectl delete pod gf-check -n monitoring ``` > Если Grafana была запущена в момент когда Loki находился в CrashLoop — datasource кэширует нерабочее состояние. Перезапуск пода исправляет. ### Grafana не открывается ```bash # Состояние подов kubectl get pod -n monitoring -l app.kubernetes.io/name=grafana # IngressRoute создан kubectl get ingressroute route-grafana -n traefik # Если IngressRoute отсутствует — перезапустить setup_traefik.yml # (порт 10003 уже есть в Traefik DaemonSet) ansible-playbook -i inventory/prod playbooks/setup_traefik.yml ``` ### MinIO `kubectl cp` завершается ошибкой `tar not found` MinIO-контейнер — минимальный образ без `tar`. `kubectl cp` использует `tar` внутри. Решение: использовать `kubectl exec -i ... -- mc ... /dev/stdin` для передачи файлов. Уже реализовано в `tasks/minio-user.yml`. --- ## Масштабирование ### До 5+ нод: Loki distributed mode Триггер: объём > 50 GB/day или > 50 одновременных запросов. Изменить в `loki-values.yml.j2`: `deploymentMode: Distributed` + задать реплики ingester/querier/distributor. **Данные в MinIO не мигрируют** — bucket `loki-chunks` остаётся тем же. ### Prometheus: Thanos для long-term storage Триггер: retention > 30d или несколько кластеров. Thanos sidecar к Prometheus pod + remote_write в объектное хранилище. ### Alloy и node-exporter Не требуют изменений — DaemonSet автоматически запускается на новых нодах. --- ## Известные ограничения | Проблема | Описание | |---|---| | `deploymentMode` обязателен на верхнем уровне | В Loki chart 7.x `deploymentMode: SingleBinary` должен быть вне блока `loki:`, иначе деплоится distributed-режим с 0 репликами | | Env var expansion не работает для S3 credentials | Loki 3.x не раскрывает `${VAR}` в полях `access_key_id` / `secret_access_key`. Пароль передаётся через `helm --set` | | Controller-manager / Scheduler метрики недоступны | kubeadm привязывает их к `127.0.0.1`. Требует ручного патча static pod манифестов | | Дашборды Grafana.com 15141 / 13639 / 18748 сломаны | Несовместимы с Grafana 12.x. Использовать встроенный дашборд **Kubernetes Logs** | --- ## Переход на Graylog Alloy поддерживает dual-write: одновременная отправка в Loki и Graylog. Порядок переезда описан в [`research/logging/plg.md`](../../research/logging/plg.md) (раздел 11). MinIO остаётся востребованным при Graylog — как S3 snapshot repository для OpenSearch (bucket `backups`).