Este sitio web, junto con algún otro, se muestran a la 🙌 World Wide Web 🙌 a traves de un servidor Nginx

Este nginx, funciona como proxy inverso, redirigiendo todo el trafico entrante a su destino correspondiente, además de gestionar los certificados para el https.

Los logs que se generan en nginx, son (como todos los logs) infumables a primera vista, así que se me ocurrió hacer un dashboard en grafana para hacerlos un poco mas interesantes.

Stack#

El conjunto de tecnologías que uso consiste en:

Nginx -> Alloy -> Loki -> Grafana

Primero hace falta saber que es lo que queremos exactamente, en este caso lo que quiero es que los logs de nginx, sean mas _leíbles? por otros elementos, supongo que por convenio hay que pasarlos a JSON. Para hacerlo, se puede definir el formato de estos logs dentro de la configuración de Nginx ⤵️

Dentro del archivo nginx.conf:

# Este fragmento permite cambiar los logs feos, a unos JSON bonitos
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # Define el formato de log en JSON UNA SOLA VEZ
    log_format json_combined escape=json
      '{'
        '"time_local":"$time_local",'
        '"remote_addr":"$remote_addr",'
        '"request_method":"$request_method",'
        '"request_uri":"$request_uri",'
        '"server_protocol":"$server_protocol",'
        '"status": "$status",'
        '"body_bytes_sent":"$body_bytes_sent",'
        '"http_referer":"$http_referer",'
        '"http_user_agent":"$http_user_agent",'
        '"http_x_forwarded_for":"$http_x_forwarded_for",'
        '"server_name":"$server_name",' 
        '"request_time":"$request_time",'
        '"upstream_addr":"$upstream_addr",'
        '"upstream_status":"$upstream_status",'
        '"upstream_response_time":"$upstream_response_time"'
      '}';
    access_log /var/log/nginx/access.log json_combined;
}

Ahora la definición de este contenedor Nginx, dentro de un docker-compose.yaml, podría ser algo así:

nginx:
  image: nginx:latest 
  container_name: nginx
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    - ./nginx/conf.d:/etc/nginx/conf.d:ro
    - nginx-logs:/var/log/nginx
  depends_on:
    - certbot
  # Este comando, es lo que me ha servido para que nginx genere logs en archivos existentes, no en stdout
  command: /bin/sh -c "rm -f /var/log/nginx/access.log /var/log/nginx/error.log && exec nginx -g 'daemon off;'"
  restart: unless-stopped

El siguiente eslabón en la cadena imaginaria de la monitorización, es Alloy, es el componente desarrollado por Grafana para consumir estos logs y mandarlos a otro sitio con el ajuste que se necesite:

# La parte comentada, sirve para levantar una webui, para hacer debug si es necesario
alloy:
  image: grafana/alloy:v1.9.2
  container_name: alloy
  volumes:
    - ./alloy/config.alloy:/etc/alloy/config.alloy:ro
    - nginx-logs:/var/log/nginx:ro
  #ports:
  #  - 12345
  command: run /etc/alloy/config.alloy #--server.http.listen-addr=0.0.0.0:12345
  depends_on:
    - loki
  restart: unless-stopped

Esto ha sido un poco doloroso la verdad, esta configuración hace que todo funcione como debería, pero no acabo de entender por que:

loki.source.file "nginx_logs" {
    targets    = [{"__path__" = "/var/log/nginx/access.log"}]
    forward_to = [loki.process.nginx.receiver]
}
loki.process "nginx" {
    stage.json {
        expressions = {        
            "server_name" = "server_name",
            "status_code" = "status",
        }
    }
    stage.labels {
        values = {
            "server_name" = "",
            "status_code" = "",
        }
    }
    forward_to = [loki.write.default.receiver]
}
loki.write "default" {
    endpoint {
        url = "http://loki:3100/loki/api/v1/push"
    }
    external_labels = {
        job = "nginx",
    }
}

Next Stop: Loki
Esto actúa como pilar fundamental entre todos los otros componentes, permitiendo a Grafana leer lo que manda Alloy.

  loki:
    image: grafana/loki:3.4.4
    container_name: loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/config.yaml
    volumes:
      - ./loki/loki-config.yaml:/etc/loki/config.yaml:ro
      - loki-data:/loki/data

Con su respectiva configuracion:

auth_enabled: false
server:
  http_listen_port: 3100
common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory
schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h
ruler:
  alertmanager_url: http://localhost:9093

Por ultimo, Grafana, esta parte no tiene nada de complejidad comparada con las anteriores, su contenedor con su volumen y poco mas:

  grafana:
    image: grafana/grafana:12.0.2
    container_name: grafana
    ports:
      - 3000
    depends_on:
      - loki
    volumes:
      - grafana-data:/var/lib/grafana

Una vez estén todos los ficheros en su sitio, construimos todo con un:

docker compose up -d 

Y si todo a ido bien, los contenedores pasaran a estar up. 😎

Grafana#

Ahora ya esta todo listo, falta crear un datasource de Loki que apunte a la URL del contenedor (http://loki:3100) y con eso ya se puede empezar a construir un dashboard a medida.
Por ejemplo esta query para devolver el total de peticiones agrupando por código de estado, todo junto en un grafico tipo tarta:

sum by(status_code) (count_over_time({job="nginx"}[$__range]))

Grafico de Codigos de estado

O también esta otra para ver el numero total de peticiones por minuto:

sum(rate({job="nginx"}[1m]))

Grafico de Peticiones

Otro que me ha parecido interesante es un ‘heatmap’ que muestra el ‘calor’ que genera una IP en función de sus peticiones al servidor:

sum by (remote_addr) (count_over_time({job="nginx"} | json [$__interval]))

Grafico Heatmap