358 lines
19 KiB
Markdown
358 lines
19 KiB
Markdown
# Стандарт централизованного сбора логов: 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`).
|