Operador de plataforma (logs y observabilidad)¶
Runbook del operador de la plataforma: cómo revisar los logs del cluster con Loki (vía Grafana o MCP), detectar problemas reales, y mantener el ruido a raya para que, el día que haya que diagnosticar un problema concreto, los logs solo contengan lo relevante.
Objetivo de higiene: si un día entras a mirar los logs por un problema, no quieres toparte con miles de warnings/errores de cosas que no tienen nada que ver. Este runbook describe cómo conseguirlo y mantenerlo.
1. Arquitectura de logs (resumen)¶
pods (stdout/stderr) ─┐
├─▶ Alloy (DaemonSet, 3 nodos) ─▶ Loki (single-binary) ─▶ Grafana (datasource "Loki")
journald de nodos ─────┘ loki.source.kubernetes monitoring ns :3100 Explore / MCP
- Alloy (
monitoring, DaemonSet) descubre pods vía API de k8s (loki.source.kubernetes) y lee el journald de cada nodo (loki.source.journal). - Loki (
monitoring, single-binary) agrega y sirve los logs. Sin auth (auth_enabled: false), acceso intra-cluster. - Grafana (desplegada por kube-prometheus-stack) tiene el datasource
Loki.
Acceso:
| Cosa | Valor |
|---|---|
| Grafana (UI) | https://grafana.local.gorkarevilla.com |
| Datasource Loki | uid 51e7573e-2948-4636-9a2b-014c6d87ce9c |
| Datasource Prometheus | uid 04a2c368-db7a-47db-9927-39cbbd9d3c6b |
| Logs en vivo | kubectl -n <ns> logs <pod> -c <container> |
2. Esquema de labels¶
Los logs llegan con dos familias de labels según su origen.
Logs de pods (loki.source.kubernetes.pods)¶
| Label | Contenido | Ejemplo |
|---|---|---|
namespace |
namespace del pod | argocd |
pod |
nombre del pod | argocd-repo-server-7c4c665579-rbgxx |
node |
nodo donde corre | kobe / kyrie / kawhi |
instance |
namespace/pod:container — identificador del contenedor |
argocd/argocd-repo-server-...:repo-server |
job |
siempre loki.source.kubernetes.pods |
— |
service_name |
idem (label interno del componente) | loki.source.kubernetes.pods |
stream |
stdout / stderr |
— |
⚠️ No existe label
container(el relabel que lo mapea no llega a materializarse como label de stream). Para filtrar por contenedor usainstance=~"namespace/pod:container"o directamentepod.
Logs de journald (loki.source.journal.journal)¶
| Label | Contenido | Ejemplo |
|---|---|---|
node |
nodo del journal | kobe |
unit |
unidad systemd | k3s.service, snapd.service, ssh.service |
job |
loki.source.journal.journal |
— |
Nivel de log (detected_level)¶
Loki infiere el nivel y lo expone como structured metadata
(detected_level: debug/info/warn/error/critical). Se filtra en el
pipeline (no en el selector):
{namespace="argocd"} | detected_level="error"
{namespace="argocd"} | detected_level=~"error|warn"
Los logs sin nivel reconocible (p. ej. muchas líneas de k8s client-go)
directamente carecen de la metadata detected_level (no se les asigna
info). Para pillarlos hay que filtrar por texto del mensaje (|= "Warning:",
etc.), no por detected_level.
3. Consultas básicas (LogQL)¶
En Grafana Explore, datasource
Loki, modo code. O vía MCP.
# Últimos logs de un namespace
{namespace="argocd"}
# Errores de un namespace
{namespace="argocd"} | detected_level="error"
# Errores y warnings de un servicio concreto (por pod)
{pod=~"argocd-repo-server-.*"} | detected_level=~"error|warn"
# Contar errores por namespace en 24h (instant)
sum by (namespace) (count_over_time({namespace=~".+"} | detected_level="error" [24h]))
# Contar volumen por pod en un namespace (encuentra el pod verboso)
sum by (pod) (count_over_time({namespace="monitoring"} [24h]))
# Logs de un contenedor concreto (via instance)
{instance=~"argocd/argocd-repo-server-.*:repo-server"}
# journald de un nodo/unidad (p. ej. k3s)
{unit="k3s.service"}
{node="kobe"} | detected_level="error"
4. Checklist de operación (paso a paso)¶
-
Visión global (24h) — volumen y errores por namespace:
Lo normal es quesum by (namespace) (count_over_time({namespace=~".+"} [24h])) sum by (namespace) (count_over_time({namespace=~".+"} | detected_level="error" [24h]))monitoring(el propio stack de observabilidad) domine el volumen; eso es ruido, no un problema. -
Localizar el pod verboso dentro del namespace que interese:
sum by (pod) (count_over_time({namespace="<ns>"} [24h])) -
Muestrear mensajes distintos (no las últimas N líneas, que se repiten):
o el tool{namespace="<ns>", pod="<pod>"} | detected_level=~"error|warn"query_loki_patternspara ver los patrones que más se repiten. -
Clasificar cada mensaje repetido: ¿error real, o ruido conocido? Consultar §6.
-
Actuar:
- Error real → abrir issue / arreglar (y actualizar la página del servicio).
- Ruido conocido → ver si tiene fix (§7) o simplemente ignorarlo en el filtro de la consulta.
5. Cómo se identifica un problema real vs ruido¶
- Ruido: se repite con cadencia fija, no coincide con ninguna caída, el
servicio funciona bien (dashboards verdes, endpoints 200), y el mensaje es
cosmético (
context canceled, deprecation warnings, health checks, access logs). No correlaciona condetected_levelque suba en el resto. - Problema real: aparece tras un cambio (deploy/config), se acompaña de
errores en cascada en servicios dependientes, o correlaciona con métricas
malas (crashloops, restarts, peticiones 5xx). Un pico de
detected_level="error"nuevo en un servicio que antes estaba limpio es la señal más clara.
6. Catálogo de ruido conocido¶
Cosas que ya están catalogadas y se pueden ignorar. Si encuentras un mensaje que se repite y no está aquí, añádelo (es la forma de mantener la higiene).
| Servicio | Mensaje (fragmento) | Nivel | Por qué es ruido | Acción |
|---|---|---|---|---|
CoreDNS (kube-system) |
No files matching import glob pattern: /etc/coredns/custom/*.server / *.override |
warn | k3s incluye import /etc/coredns/custom/* en el Corefile pero no crea el directorio |
Arreglado (ConfigMap coredns-custom) |
Longhorn (longhorn-system) |
Warning: v1 Endpoints is deprecated in v1.33+ |
warn | Deprecation warning de client-go en longhorn-manager | Se corrige al actualizar Longhorn |
| Longhorn instance-manager | Error reading from wire ... use of closed network connection |
error | Cierre normal de conexiones gRPC de datos al attach/detach | Ignorar |
Loki (monitoring, loki-0) |
error processing requests from scheduler ... context canceled |
error | Cancelación normal de queries (Grafana Live/Explore) | Ignorar |
| Loki | Error receiving response from grpc tail client ... context canceled + reached tail max duration limit |
error | Tail de Grafana Live que se cancela | Ignorar |
Grafana (monitoring) |
Error writing to response ... does not allow body |
error | Respuestas 204 sin body (avatar, etc.) | Ignorar |
| Argo CD repo-server | Error serving health check request ... context canceled |
error | Health-check gRPC que cancela al estar el repo aún listándose | Ignorar (conocido) |
k3s (k3s.service / k3s-agent.service) |
Proxy error: write failed: io: read/write on closed pipe |
warn | Proxy TLS del API server al cerrar una conexión | Ignorar |
k3s (kube-controller-manager) |
Failed to list /registry/events/ ... mvcc: required revision has been compacted |
error | Reflector de events que quedó atrás tras compactar etcd | Ignorar (conocido) |
| MetalLB speaker | memberlist: Failed fallback TCP ping: timeout ... i/o timeout |
error | memberlist sí se usa en L2 (lista de speakers activos → elección del anunciador y failover). El fallback TCP puntual con UDP OK es menor, pero si se repite conviene revisar la conectividad del puerto 7946 entre nodos | Investigar si sube la cadencia |
| CNPG operator | Error retrieving lease lock ... context canceled |
error | Leader election que cancela al reconciliar | Ignorar |
| Reloader | KUBERNETES_NAMESPACE is unset, will detect changes in all namespaces |
warn | Aviso de arranque; es el comportamiento deseado | Ignorar (o setear namespace) |
| VKS | HealthChecks.DefaultHealthCheckService[103] |
warn | Health-check de .NET que falla de forma puntual | Investigar si sube la cadencia |
| Traefik | SafeNaming is not explicitly set ... |
warn | Aviso de migración v3.6, una vez por restart | Arreglado (safeNaming: true) |
| Traefik | Traefik can reject some encoded characters in the request path ... |
warn | Aviso incondicional de arranque (v3.7.12, cmd/traefik/traefik.go); no se silencia por config |
Ignorar (el hardening encodedCharacters: false ya está aplicado) |
7. Cómo reducir / silenciar ruido¶
Orden de preferencia (de más limpio a más invasivo):
- Arreglar la causa (mejor): p. ej. crear el dir de
coredns/custom, actualizar Longhorn, setearSafeNamingen Traefik. - Bajar el nivel de log del servicio ruidoso (Loki, Alloy).
- Desactivar access logs (loki-gateway nginx).
- Ajustar frecuencia (loki-canary push interval).
- Drop en origen (Alloy): como último recurso, descartar un patrón en el pipeline de Alloy antes de enviarlo a Loki (solo si no se puede arreglar en el servicio).
Los cambios de config van en clusters/luka/apps/<svc>/application.yaml
(Helm valuesObject) o en k3s/ansible/roles/<svc>/ (plataforma core
instalada por Ansible), con su PR. Cada cambio de ruido debe reflejarse en el
§6 y en la página del servicio (docs/services/<svc>.md → ## Monitorización
o ## Notas / TODO).
Aplicado (estado actual)¶
| Servicio | Cambio | Fichero |
|---|---|---|
| Loki | log_level: warn (mata el log de cada query) |
clusters/luka/apps/loki/application.yaml |
| Loki | gateway.verboseLogging: false (mata el access log nginx) |
ídem |
| Loki | lokiCanary.extraArgs: ["-interval=1m"] (canary cada minuto) |
ídem |
| Alloy | logging { level = "warn" } (mata el opened log stream) |
clusters/luka/apps/alloy/application.yaml |
| Kured | sysctl fs.inotify.max_user_instances=512 + max_user_watches=524288 (fix too many open files) |
k3s/ansible/roles/longhorn-prereqs/tasks/main.yml |
| CoreDNS | ConfigMap coredns-custom con placeholders .server/.override |
k3s/ansible/roles/coredns-custom/ |
| Traefik | providers.kubernetesCRD.safeNaming: true |
k3s/ansible/roles/traefik/files/values.yaml |
Pendiente de aplicar (requieren decisión/manual)¶
| Servicio | Ruido | Fix propuesto |
|---|---|---|
| Longhorn | v1 Endpoints deprecation |
Sin fix aún: ya está en la última versión (1.12.1); lo arreglará una futura release de Longhorn con client-go actualizado |
8. Incidencias detectadas (no son ruido)¶
Hallazgos que requieren atención, fuera del ruido rutinario:
- Kured:
failed to create fsnotify watcher: too many open files— root cause confirmado:fs.inotify.max_user_instances(default 128) agotado (kawhi 143 en uso, kobe 134, kyrie 93).inotify_init()devuelve EMFILE y kured no puede vigilar el sentinel de reinicio. Resuelto:fs.inotify.max_user_instances=512 -
max_user_watches=524288vía/etc/sysctl.d/90-homelab-inotify.conf(role Ansiblelonghorn-prereqs), ya aplicado en los 3 nodos y verificado. Verdocs/services/kured.md. -
Corte transitorio ~11:10–11:16 (07/09) — fallos de DNS (
lookup api.github.com ... connection refuseden ARC), VaultwardenCan't connect to database, CNPGcontext canceledy restarts de varios pods (Traefik, Reloader, cloudflared). Consistente con un reinicio de nodo/CoreDNS puntual. No persistente; si se repite, correlacionar con reinicios de kured o eventos de nodo. -
Incidentes del 24/09 — errores de I/O en los volúmenes iSCSI de Longhorn en
kyrie(cascada de restarts), 502 en Heimdall porauthentik-serverevictado durante los drenajes fallidos de kured (kernel pendiente), y VKS en crash-loop. Documentados por separado en la sección Incidencias.
9. Análisis por servicio (referencia)¶
Cada servicio tiene su página en
docs/services/. Aquí, el dato de log mínimo para consultarlo: qué namespace/pod y qué buscar.
| Servicio | Namespace | Qué mirar |
|---|---|---|
| k3s | kube-system + journald unit=k3s*.service |
errores de nodo, mvcc compacted, Proxy error |
| CoreDNS | kube-system (pod coredns-*) |
warnings de import glob (ruido §6) |
| MetalLB | metallb-system |
speaker/controller, memberlist (ruido §6) |
| Traefik | traefik |
access/error logs del gateway, SafeNaming |
| Argo CD | argocd |
application-controller (sync), repo-server (health-check §6), notifications-controller |
| cert-manager | cert-manager |
renovación de certificados, errores de ACME |
| Longhorn | longhorn-system |
longhorn-manager (deprecation §6), csi-plugin, instance-manager |
| CloudNativePG | cnpg-system |
operador, backups (vaultwarden-db-daily-backup) |
| Reloader | reloader |
reinicios por cambio de Secret/ConfigMap |
| Kured | kured |
fsnotify (incidencia §8), reinicios de nodo |
| VKS | vaultwarden-kubernetes-secrets |
sync colección Homelab → Secrets |
| cloudflared | cloudflared |
estado del túnel |
| kube-prometheus-stack | monitoring |
Prometheus, Grafana, node-exporter, kube-state-metrics |
| Loki | monitoring (loki-0) |
errores de ingesta/consulta |
| Alloy | monitoring (alloy-*) |
errores de write a Loki |
| grafana-operator | monitoring |
reconciliación de CRs de Grafana |
| ARC | arc-systems + arc-runners |
runners self-hosted, jobs de CI |
| pgAdmin | pgadmin |
conexiones a Postgres |
| Vaultwarden | vaultwarden |
errores de auth/DB |
| Heimdall | heimdall |
poco tráfico |
10. Notas de operación¶
- Tus propias consultas generan logs: cada query a Loki produce varias
líneas
metrics.goenloki-0. No te asustes al ver picos demonitoringmientras exploras; es normal. - Retención: Loki 30 días, Prometheus 15 días. Los logs no se backupean.
- Verbosidad esperada por diseño:
monitoring,argocdyarc-runnersson los namespaces más ruidosos; es inherente al stack. Antes de "optimizar" un servicio, mira si su ruido ya está catalogado en §6. - Tras cualquier cambio de config de logging, verificar el impacto repitiendo el checklist (§4) y actualizar este runbook.