---
weight: 4
title: "Monitorización de contenedores Docker con cAdvisor, Prometheus y Grafana"
date: 2022-01-06T22:40:20+0100
draft: false
summary: "Notas sobre monitorización de contenedores"
resources:
- name: "featured-image"
  src: "featured-image.png"
- name: "featured-image-preview"
  src: "featured-image-preview" 
categories:
  - notes
tags:
  - docker
  - cAdvisor
  - prometheus
  - grafana
  - alertmanager
---

{{< admonition type=warning title="Work in progress" open=true >}}

Apuntes incompletos de monitorización de contenedores Docker

{{< /admonition >}}


## Monitorización

Después de conseguir instalar _Jam Session_ en mi Docker y ver como montan la monitorización del sistema me han entrado ganas de probar a montar algo para todos los contenedores instalados en mi VPS.


{{< admonition type=tip title="Referencias" open=true >}}
- [Monitoring Docker Services with Prometheus](https://www.ctl.io/developers/blog/post/monitoring-docker-services-with-prometheus/)
- [Monitoring setup with docker-compose](https://dev.to/ablx/minimal-prometheus-setup-with-docker-compose-56mp)
- [Un template básico](https://github.com/vegasbrianc/prometheus) en Github
- [Otro docker-compose programado por Eisteinish](https://github.com/Einsteinish/Docker-Compose-Prometheus-and-Grafana) Muy completo, también en Github
{{< /admonition >}}


## Prometheus y cAdvisor

Inspirándonos en la configuración de monitorización que venía con _Jam Session_, vamos a intentar configurar un sistema propio. Vamos a usar también [Grafana](https://grafana.com/) y [Prometheus](https://prometheus.io/), pero añadiendo [cAdvisor](https://github.com/google/cadvisor) a la receta como [nos proponen](https://prometheus.io/docs/guides/cadvisor/) en la propia web de _Prometheus_ (la combinación de _cAdvisor_, _Prometheus_ y _Grafana_ parece un estándar _de facto_ en la industria) 

_cAdvisor_ es un software de Google, escrito en Go, y programado específicamente para la captura de métricas de contenedores; necesita acceso a varios directorios del host (que mapearemos con _bind-mounts_ en Docker) para capturar los datos. _cAdvisor_ expone el puerto `8080` (interfaz web y REST api) y por defecto permite a _Prometheus_ acceder a [varias métricas](https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md#prometheus-container-metrics).

_Prometheus_ es un _toolkit_ de monitorización de métricas. _Prometheus_ almacena las métricas como series temporales, a las que se pueden asignar pares etiqueta valor opcionales (parece un funcionamiento análogo al de _InfluxDB_) Soporta un lenguaje de consultas denominado _PromQL_. Puede capturar métricas por _pulling_ o por _pushing_. Normalmente, en procesos de vida larga, _Prometheus_ hace _scrapping_ en los orígenes de métricas definidos, pero también implementa un _push-gateway_ para procesos de vida muy corta.

_Prometheus_, igual que _cAdvisor_, expone un interfaz web a través del cual podemos hacer consultas con _PromQL_ y ver los resultados en modo tabla o modo gráfico.

Pero esto es solo la punta del iceberg. Entre otras cosas:

- _Prometheus_ nos da la posibilidad de instalar otras dos aplicaciones: _Alertmanager_ y _Node Exporter_
    - _Alertmanager_  se encarga de gestionar las Alertas que _Prometheus_ genere a partir de las métricas para canalizarlas a distintos sistemas de comunicación (nos da muchísimas opciones)
    - _Node Explorer_ está especializado en monitorizar métricas de la máquina host.
- Hay todo un [ecosistema de bibliotecas y clientes](https://prometheus.io/docs/instrumenting/clientlibs/) para exportar métricas compatibles con _Prometheus_ desde multitud de sistemas.

### cAdvisor

Empezamos por configurar el contenedor de _cAdvisor_. Creamos un directorio `mon` para nuestros nuevos contenedores de monitorización, y en ese directorio nuestro fichero `docker-compose.yml`: 

```yaml
version: '3.9'
services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
#    ports:
#    - 8080:8080
    volumes:
    - /:/rootfs:ro
    - /var/run:/var/run:rw
    - /sys:/sys:ro
    - /var/lib/docker/:/var/lib/docker:ro
    networks:
      backend:
        ipv4_address: 172.20.0.10
networks:
  backend:
    external: true
```

La configuración no tiene mucho misterio, hacemos _bind-mounts_ de todos los recursos del host que necesita _cAdvisor_ para recolectar métricas. Como queremos consumir todas las métricas en local comentamos el mapeo de puertos por que no queremos que el VPS deje acceso desde internet a las métricas. Por la misma razón _Traefik_ no necesita saber nada de este contenedor (ya que no será accesible desde fuera)

En principio entiendo que _cAdvisor_ captura las métricas directamente desde _Docker_ así que de momento lo conectamos solo a la red `backend`.

Con esto tenemos todo listo para lanzar el servicio _cAdvisor_ con `dcupd` (mi alias para `docker-compose up -d`). Para comprobar el contenedor desde fuera del VPS podemos hacerlo por un túnel ssh:

```bash
ssh -L 9081:172.20.0.10:8080 dockadmin@fomalhaut
```

Ahora podemos conectar nuestro navegador a <http://localhost:9081> y comprobar que _cAdvisor_ está funcionando y detecta nuestros contenedores en el VPS.

{{< figure src="cAdvisor_web_a.png" title="Web de cAdvisor" >}}

Aunque podemos ver métricas (e incluso gráficas) de todos los contenedores no es muy "amistoso" y está bastante limitado. Pero su función principal es recolectar las métricas de los contenedores y eso lo hace perfectamente.

{{< figure src="cAdvisor_web_b.png" title="Gráficas en la web de cAdvisor" >}}


### Prometheus

Ya tenemos _cAdvisor_ funcionando. Vamos a configurar _Prometheus_ creando su fichero de configuración:

```bash
mkdir mon/prometheus
touch mon/prometheus/prometheus.yml
```

Con el contenido:

```yaml
global:
  # How frequently to scrape targets (by default 1m)
  scrape_interval: 15s
  # How long until a scrape request times out (by default 10s)
  # scrape_timeout: 10s
  # How frequently evaluate the rules (by default 1m)
  evaluation_interval: 15s
  
# Alerting section specifies settings related to Alertmanager
alerting:
  alertmanagers:
  - static_configs:
    - targets:
    # whatever you want

# Rule files specifies a list of globs. Rules and alerts are read from all matching files
# rule_files:
    
# List of scrape configurations
scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['prometheus:9090']
      labels:
        alias: 'prometheus'
  - job_name: 'cadvisor'
    static_configs:
    - targets: ['cadvisor:8080']
      labels:
        alias: 'cadvisor'
```

En la sección `global` configuramos los períodos de polling a los origenes de métricas y el período de evaluación de reglas de alarmas.

Dejamos preparada la sección `alerting` donde configuraremos las reglas de alarmas. Y la sección `rule_files`.

En la seccion `scrape_configs` configuramos orígenes de métricas que queremos capturar. De momento configuramos el propio contenedor `prometheus`, que publicará sus metricas en el puerto 9090, y configuramos también el contenedor `cadvisor` que como ya vimos expone sus métricas en el puerto 8080. Al definir los orígenes de las métricas usamos los nombres de red de los contenedores (dentro de la red Docker) y asignamos a cada uno una etiqueta que nos permita distinguir los datos más tarde.

Ahora nos toca levantar el contenedor de _Prometheus_, modificamos el fichero `docker-compose.yml`:

```yaml
version: '3.9'

networks:
  frontend:
    external: true
  backend:
    external: true

volumes:
  prometheus_data:
    name: prometheus_data
  grafana_data:
    name: grafana_data

services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
#    ports:
#      - 8080:8080
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    networks:
      backend:
        ipv4_address: 172.20.0.10

  nodeexporter:
    image: prom/node-exporter:v1.3.1
    container_name: nodeexporter
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
    restart: unless-stopped
    networks:
      backend:
        ipv4_address: 172.20.0.11

 prometheus:
    image: prom/prometheus:v2.32.1
    container_name: prometheus
#    ports:
#      - 9090:9090
    volumes:
#      - ${PWD}/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ${PWD}/prometheus/:/etc/prometheus/:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/usr/share/prometheus/console_libraries'
      - '--web.console.templates=/usr/share/prometheus/consoles'
    depends_on:
      - cadvisor
    networks:
      backend:
        ipv4_address: 172.20.0.12
```

Igual que con _cAdvisor_ no queremos publicar el puerto de _Prometheus_ en internet. En todo caso, si después nos interesa, lo expondremos via _Traefik_.

Igual que con _cAdvisor_ podemos probar el acceso a _Prometheus_ via túnel ssh. Una vez conectados al interfa web de _Prometheus_ podemos comprobar el estado de los dos origenes de datos definidos para ver que los dos están activos (deberían).

Ejecutando:

```bash
ssh -L 9082:172.20.0.10:9090 dockadmin@fomalhaut
```

Podremos acceder a nuestro nuevo _Prometheus_ en <http://localhost:9082>. Si queremos hacer pruebas de consultas en la documentación oficial tenemos unos cuantos ejemplos.

{{< figure src="prometheus_targets.png" title="Orígenes de datos en la web de Prometheus" >}}

## Grafana

Vamos a añadir _Grafana_ a nuestra plataforma de monitorización. _Grafana_ es un software especializado en la elaboración de cuadros de mando (__Dashboards_) y representación gráfica de valores. Y lo hace francamente bien. Permite definir tanto los orígenes de datos como los propios _Dashboards_ mediante ficheros de configuración o desde el interfaz web. Además los _Dashboards_ pueden exportarse e importarse como ficheros `.json`. Incluso hay una [página web](https://grafana.com/grafana/dashboards) donde se comparten los _Dashboards_ entre usuarios.

La sección de _Grafana_ en el fichero `docker-compose.yml` quedaría

```yaml
grafana:
    image: grafana/grafana:8.3.3
    container_name: grafana
    user: "472"
    depends_on:
      - prometheus
#    ports:
#      - 3000:3000
    volumes:
      - grafana_data:/var/lib/grafana
      - ${PWD}/grafana/provisioning/:/etc/grafana/provisioning/
    env_file:
      - ${PWD}/grafana/config.env
    networks:
      backend:
        ipv4_address: 10.20.0.15
      frontend:
        ipv4_address: 10.21.0.15
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=frontend"
      - "traefik.http.routers.grafana.rule=Host(`grafana.midominio.com`)"
      - "traefik.http.routers.grafana.entrypoints=https"
      - "traefik.http.routers.grafana.service=grafana"
      - "traefik.http.services.grafana.loadbalancer.server.port=3000"
```

Tenemos que preparar el fichero de entorno `mon/grafana/grafana.env` con los datos confidenciales de acceso a _Grafana_.

```yaml
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=unaBuenaPassword
GF_USERS_ALLOW_SIGN_UP=false
# GF_SERVER_ROOT_URL=https://grafana.midomino.com
```

Con esta configuración ya podemos levantar el contenedor de _Grafana_ y comprobar que podemos acceder sin problemas. (Claro que antes tenemos que declarar la ruta en la _DNS Zone_)

{{< figure src="grafana_home.png" title="Web de Grafana" >}}

Como ya hemos comentado podríamos configurar completamente _Grafana_ desde el interfaz web, pero vamos a crear la fuente de datos asociada a nuestro _Prometheus_ desde el fichero de configuración de _Grafana_.

Para eso creamos el fichero `mon/grafana/provisioning/datasources/datasource.yml` con el contenido:

```yaml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    orgId: 1
    url: http://prometheus:9090
    basicAuth: false
    isDefault: true
    editable: true
```

Una vez tengamos el fichero podemos reiniciar el contenedor de _Grafana_ con `dco restart grafana` (`docker-compose restart grafana` si no usas mis alias). Después del reinicio podremos ver la nueva fuente de datos en la web de _Grafana_.

{{< figure src="grafana_promsrc.png" title="Grafana, datasource de Prometheus" >}}

Nuestro primer _Dashboard_ lo importaremos directamente desde la web. Vamos a usar como ejemplo [este Dashboard](https://grafana.com/grafana/dashboards/193) de la página web de _Grafana_. Vemos que tiene el `id=193`. Podríamos descargarlo como fichero `.json` pero nos basta con el `id` para probarlo. Clickamos en el signo `+` a la izquierda en la página web de nuestro _Grafana_ y escogemos la opción `Import`. Basta con teclear el id `193` para que tengamos el _Dashboard_ definido en _Grafana_. 

{{< figure src="grafana_contdash.png" title="Grafana: Dashboard de Contenedores" >}}

## Prometheus: Añadiendo Node Exporter

Si queremos que _Prometheus_ capture también métricas de la maquina host, podemos instalar _Node Exporter_. La sección del fichero `docker-compose.yml` para el _Node Exporter_ sería:

```yaml
  nodeexporter:
    image: prom/node-exporter:v0.18.1
    container_name: nodeexporter
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
    restart: unless-stopped
    networks:
      backend:
        ipv4_address: 172.20.0.11
```

Tenemos que añadir el nuevo origen de datos en el fichero `mon/prometheus/prometheus.yml` (lineas 31 y siguientes) que nos quedaría:

```yaml
global:
  # How frequently to scrape targets (by default 1m)
  scrape_interval: 15s
  # How long until a scrape request times out (by default 10s)
  # scrape_timeout: 10s
  # How frequently evaluate the rules (by default 1m)
  evaluation_interval: 15s

# Alerting section specifies settings related to Alertmanager
alerting:
  alertmanagers:
  - static_configs:
    - targets:
    # whatever you want

# Rule files specifies a list of globs. Rules and alerts are read from all matching files
# rule_files:

# List of scrape configurations
scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['prometheus:9090']
      labels:
        alias: 'prometheus'
  - job_name: 'cadvisor'
    static_configs:
    - targets: ['cadvisor:8080']
      labels:
        alias: 'cadvisor'
  - job_name: 'node-exporter'
    scrape_interval: 5s
    static_configs:
    - targets: ['nodeexporter:9100']
      labels:
        alias: 'nodeexporter'
```

Con la nueva fuente de datos definida podemos reiniciar el contenedor _Prometheus_ con `dco restart prometheus` (`docker-compose restart prometheus`)

Podemos volver a conectarnos a _Prometheus_ via túnel ssh como ya describimos y comprobar que los Targets son ahora tres y están todos arriba.

{{< figure src="prometheus_targets_b.png" title="Prometheus con tres targets activos" >}}

Para ver los nuevos datos que aporta el _Node Exporter_ a nuestro contenedor _Prometheus_ tenemos que definir un nuevo _Dashboard_ que nos los muestre en el contenedor _Grafana_.

Vamos a añadir el nuevo _Dashboard_ definiendolo con un fichero `.json` para ello definiremos dos ficheros en nuestro directorio `mon/grafana/provisioning/dashboards` ([más info](https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards))

El primer fichero `dashboard.yml` define el _provider_ y tiene el siguiente contenido:

```yaml
apiVersion: 1

providers:
- name: 'Prometheus'
  orgId: 1
  folder: ''
  type: file
  disableDeletion: false
  editable: true
  options:
    path: /etc/grafana/provisioning/dashboards
```

En el segundo fichero `dco_prom.json` definiremos el _Dashboard_ que hemos copiado de [aquí](https://github.com/vegasbrianc/prometheus/blob/master/grafana/provisioning/dashboards/Docker%20Prometheus%20Monitoring-1571332751387.json).

Si reiniciamos ahora el contenedor de _Grafana_, `dco restart grafana` veremos que ya tenemos los dos _Dashboard_ disponibles; el que añadimos desde la red y el que hemos añadido ahora mismo por fichero.

En el nuevo _Dashboard_ podemos ver ya métricas del host, como la memoria usada, la carga de CPU del host y la capacidad del sistema de ficheros.

Vemos también que el nuevo _Dashboard_ viene preparado para visualizar _Alerts_ pero no tenemos ninguna definida (todavía).

{{< figure src="grafana_nexporter.png" title="Dashboard con datos de Node Exporter" >}}


## Prometheus: Definición de Alarmas

Vamos a definir condiciones de Alarma en _Prometheus_. Necesitamos crear un nuevo fichero `mon/prometheus/alert.rules`, donde definiremos un par de alarmas:

```yaml
groups:
- name: example
  rules:

  # Alert for any instance that is unreachable for >2 minutes.
  - alert: service_down
    expr: up == 0
    for: 2m
    labels:
      severity: page
    annotations:
      summary: "Instance {{ $labels.instance }} down"
      description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 2 minutes."

  - alert: high_load
    expr: node_load1 > 0.5
    for: 2m
    labels:
      severity: page
    annotations:
      summary: "Instance {{ $labels.instance }} under high load"
      description: "{{ $labels.instance }} of job {{ $labels.job }} is under high load."
```

Tenemos que editar también el fichero `mon/prometheus/prometheus.yml` para añadir nuestro fichero `alert.rules` en la sección de `rule_files` que quedaría así:

```yaml
# Rule files specifies a list of globs. Rules and alerts are read from all matching files
rule_files:
  - 'alert.rules'
```

Si rearrancamos _Prometheus_ (ya sabes `dco restart prometheus` o `docker-compose restart prometheus`) y se cumplen las condiciones para la alarma podremos verla en el _Dashboard_:

{{< figure src="grafana_alarm.png" title="Dashboard con Alarma" >}}

Las alarmas que hemos definido no tienen demasiado sentido, pero antes de profundizar en como definir alarmas significativas vamos a configurar las notificaciones a través de _Alert Manager_.





<!--


<https://medium.com/@mertcan.simsek276/docker-monitoring-with-cadvisor-prometheus-and-grafana-adefe1202bf8>
<https://grafana.com/docs/grafana-cloud/quickstart/docker-compose-linux/>

<https://medium.com/skedler/monitoring-servers-and-docker-containers-using-prometheus-with-grafana-87cf961fe1e0>

- [Prometheus Alertmanager Telegram](https://tienbm90.medium.com/prometheus-alertmanager-send-alerts-to-admin-via-telegram-81b4f547089f)


## Traefik and Prometheus

- <https://medium.com/platform-engineering/monitoring-traefik-with-grafana-1d037af5b952>
- <https://mwunderling.com/blog/traefik_metrics_logs.html>
- <https://docs.fuga.cloud/how-to-monitor-your-traefik-ingress-with-prometheus-and-grafana>
- <https://stackoverflow.com/questions/58554731/traefik-v2-0-metrics-with-prometheus>

`ssh -L 9081:172.20.0.40:8384 dockadmin@siriodock` 



## Next

- <https://dzone.com/articles/playing-with-docker-mqtt-grafana-influxdb-python-a>

-->

