Saltar a contenido

Grafana - Visualización de series temporales

Antes de empezar

Este documento da por supuesto que ya tienes datos en InfluxDB. Si aún no es así, revisa la sesión sobre InfluxDB y comprueba en el Data Explorer que la medida sensores contiene puntos.

Grafana es una plataforma de código abierto para la visualización y el análisis de datos operacionales. Nació en 2014 precisamente para representar series temporales procedentes de sistemas de monitorización, y hoy es el estándar de facto para cuadros de mando de infraestructura, DevOps e IoT.

Su característica más relevante es que no almacena datos. Grafana no es una base de datos: es una capa de visualización que se conecta a orígenes de datos externos (más de 150 disponibles: InfluxDB, Prometheus, MySQL, PostgreSQL, Elasticsearch, Loki, ficheros CSV...) y consulta cada uno con su propio lenguaje. Esto le permite reunir en un mismo cuadro de mando información procedente de sistemas completamente distintos.

En nuestro proyecto, Grafana es la última capa antes del usuario: los sensores publican en MQTT, Telegraf ingiere en InfluxDB, y Grafana convierte esos puntos en algo que un operario de planta puede mirar de un vistazo.

Grafana OSS, Cloud y Enterprise

  • Grafana OSS (licencia AGPLv3) es la versión que usamos, completamente funcional.
  • Grafana Cloud es el servicio gestionado, con capa gratuita limitada.
  • Grafana Enterprise añade orígenes de datos comerciales, informes programados y control de acceso granular.

Para todo lo que haremos en el proyecto, la versión OSS es más que suficiente.

Primeros pasos

Grafana forma parte del stack OTITIA y se expone en el puerto 3000. Si accedemos a http://localhost:3000 nos aparecerá la pantalla de acceso, donde entraremos con las credenciales configuradas en el .env (admin / admin).

Acceso a Grafana
Acceso a Grafana

El menú lateral organiza toda la herramienta:

  • Dashboards: los cuadros de mando, organizados en carpetas.
  • Explore: un banco de pruebas para lanzar consultas sueltas sin crear paneles. Es donde conviene depurar una consulta antes de llevarla a un cuadro de mando.
  • Alerting: reglas de alerta, puntos de contacto y políticas de notificación.
  • Connections: gestión de los orígenes de datos.
  • Administration: usuarios, equipos, preferencias y organizaciones.

Orígenes de datos

Un origen de datos (data source) es la conexión entre Grafana y un sistema que contiene información. Cada origen se configura una sola vez y luego queda disponible para todos los paneles.

InfluxDB 2.7 con Flux

Es el origen principal del proyecto. Desde Connections → Add new connection → InfluxDB:

Campo Valor Comentario
Query language Flux Determina el lenguaje de las consultas
URL http://influxdb:8086 Nombre del contenedor, no localhost
Organization iabd El de la variable INFLUXDB_ORG
Token token-v2-para-curso El de INFLUXDB_TOKEN
Default Bucket telemetria Bucket que se propone por defecto

Al pulsar Save & test debe aparecer un mensaje de conexión correcta. Si falla, el 90 % de las veces es una de estas tres causas: has puesto localhost en lugar de influxdb, el token no coincide con el del .env, o el contenedor de Grafana no está en la red otitia-it.

Nombres de host entre contenedores

Grafana se ejecuta dentro de un contenedor. Cuando le pides que se conecte a localhost:8086, se está buscando a sí mismo. La URL correcta siempre es el nombre de servicio del docker-compose.yml: http://influxdb:8086.

Esto también aplica a MySQL (mysql:3306) e InfluxDB 3 (http://influxdb3:8181).

InfluxDB 3 Core con SQL

Como anexo comparativo, podemos añadir un segundo origen apuntando a la versión 3, seleccionando SQL como lenguaje, la URL http://influxdb3:8181 y la base de datos telemetria. Los mismos datos, consultados con SQL en lugar de Flux.

MySQL

Grafana OSS incluye el conector de MySQL de serie, no hay que instalar ningún complemento. Se configura con host mysql:3306, base de datos iot y las credenciales del .env. Lo usaremos para cruzar las series temporales con datos maestros (catálogo de dispositivos, alarmas confirmadas por un operario).

Aprovisionamiento

Configurar orígenes de datos a mano no es reproducible: si mañana ejecutas down -v, los pierdes. Grafana permite definirlos como archivos YAML que se cargan al arrancar, en grafana/provisioning/datasources/datasources.yml:

apiVersion: 1

datasources:
  - name: InfluxDB-v2
    type: influxdb
    access: proxy
    url: http://influxdb:8086
    jsonData:
      version: Flux
      organization: iabd
      defaultBucket: telemetria
    secureJsonData:
      token: $INFLUXDB_TOKEN
    isDefault: true

De la misma forma, grafana/provisioning/dashboards/dashboards.yml indica una carpeta de la que cargar cuadros de mando en formato JSON. Con esto, todo el trabajo de visualización queda versionado en Git y se reconstruye solo.

Anatomía de un cuadro de mando

La jerarquía de Grafana es sencilla:

  • Carpeta (Folder): agrupa cuadros de mando y sirve de unidad de permisos.
  • Cuadro de mando (Dashboard): una pantalla completa. Tiene un rango temporal y un intervalo de refresco propios.
  • Fila (Row): agrupación plegable de paneles dentro del cuadro de mando.
  • Panel (Panel): cada elemento visual. Contiene una o más consultas, un tipo de visualización y sus opciones de formato.

Un detalle clave: el rango temporal es global al cuadro de mando. Cuando el usuario selecciona "últimas 6 horas" en la esquina superior derecha, todas las consultas de todos los paneles se reejecutan con ese rango. Por eso las consultas nunca llevan fechas fijas: llevan variables que Grafana sustituye.

Nuestro primer panel

Vamos a representar la evolución de la temperatura. Desde Dashboards → New → New dashboard → Add visualization, seleccionamos el origen InfluxDB-v2 y, en el editor de consultas, cambiamos al modo de código para escribir Flux directamente:

from(bucket: "telemetria")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "sensores")
  |> filter(fn: (r) => r._field == "valor")
  |> filter(fn: (r) => r.magnitud == "temperatura")
  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
  |> yield(name: "media")

Las tres variables que inyecta Grafana son la clave de todo:

Variable Contenido
v.timeRangeStart Inicio del rango seleccionado en el selector superior
v.timeRangeStop Fin del rango seleccionado
v.windowPeriod Intervalo de agregación calculado automáticamente según el ancho del panel en píxeles

v.windowPeriod merece una explicación. Si pides seis horas de datos con lecturas cada 10 segundos, son más de 2000 puntos para un panel de 400 píxeles de ancho: estarías enviando por la red diez veces más datos de los que se pueden dibujar. Grafana calcula un intervalo de agregación adecuado y aggregateWindow reduce la serie a un punto por intervalo. Esta línea es lo que separa un cuadro de mando fluido de uno que tarda diez segundos en cargar.

Primer panel de series temporales en Grafana
Primer panel de series temporales en Grafana

Depura en Explore, construye en Dashboard

Escribir Flux directamente en el editor del panel es incómodo: cada error obliga a rehacer el ciclo. Es mucho más ágil probar la consulta en Explore, donde ves la tabla de resultados en crudo, y llevarla al panel cuando ya devuelve lo que esperas.

Etiquetar las series

Por defecto, Grafana nombra cada serie con toda la información de sus etiquetas, produciendo leyendas ilegibles del tipo sensores valor {zona="aula", dispositivo="sensor01"}. Se arregla en la consulta, quedándonos solo con las columnas que interesan:

  |> keep(columns: ["_time", "_value", "zona"])
  |> rename(columns: {_value: "temperatura"})

O bien en las opciones del panel, en Standard options → Display name, usando plantillas como ${__field.labels.zona}.

Tipos de panel

Grafana ofrece muchos tipos de visualización, pero en un cuadro de mando de telemetría se usan casi siempre los mismos seis:

Tipo Cuándo usarlo
Time series Evolución de una magnitud. El caballo de batalla
Stat Un valor único destacado: última lectura, media, máximo
Gauge Un valor dentro de un rango conocido, con zonas de color
Bar gauge Comparación de varios valores actuales entre sí
Table Listados: últimas alertas, catálogo de dispositivos
State timeline Estados discretos a lo largo del tiempo: on/off, ok/aviso/crítico
Text Texto libre en Markdown o HTML. Nos servirá para el informe del LLM

Para un panel de tipo Stat que muestre la última temperatura de cada zona:

from(bucket: "telemetria")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "sensores" and r._field == "valor")
  |> filter(fn: (r) => r.magnitud == "temperatura")
  |> last()
  |> group(columns: ["zona"])

Transformaciones

Las transformaciones son operaciones que Grafana aplica al resultado de la consulta, ya en su propio motor, antes de dibujarlo. Se configuran en la pestaña Transform del editor de paneles.

Son especialmente útiles cuando el trabajo se puede hacer en el origen pero resulta más incómodo:

  • Organize fields: renombrar, ocultar y reordenar columnas.
  • Filter data by values: descartar filas según un valor.
  • Join by field: combinar resultados de dos consultas, incluso de orígenes distintos. Aquí está la gracia: puedes unir una serie de InfluxDB con el catálogo de dispositivos de MySQL en un mismo panel.
  • Add field from calculation: columnas calculadas, como una diferencia entre dos series.
  • Reduce: colapsar una serie temporal a un único valor estadístico.

Regla práctica

Si la operación reduce el volumen de datos que viaja por la red (filtros, agregaciones), hazla en la consulta. Si es cosmética o combina resultados de orígenes distintos, hazla en la transformación.

Variables de cuadro de mando

Un cuadro de mando con un panel por cada zona no escala: si mañana añades dos zonas más, hay que rehacerlo. Las variables permiten parametrizarlo y que el usuario elija qué está mirando mediante desplegables.

Se definen en Dashboard settings → Variables. La más útil es la de tipo Query, que obtiene los valores posibles del propio origen de datos:

import "influxdata/influxdb/schema"

schema.tagValues(bucket: "telemetria", tag: "zona")

Esto genera un desplegable con todas las zonas existentes, que se actualiza solo cuando aparece una nueva. Una vez definida la variable zona, se usa en las consultas con ${zona}:

from(bucket: "telemetria")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "sensores" and r._field == "valor")
  |> filter(fn: (r) => r.zona == "${zona}")
  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)

Si activas Multi-value para permitir seleccionar varias zonas a la vez, la comparación simple deja de funcionar, porque ${zona} pasa a ser una lista. Hay que usar contains:

  |> filter(fn: (r) => contains(value: r.zona, set: ${zona:json}))

Otros tipos de variable que conviene conocer:

  • Custom: lista de valores fijos escrita a mano.
  • Interval: intervalos de agregación (1m,5m,15m,1h) para que el usuario elija la granularidad.
  • Constant: un valor fijo que no se muestra, útil para el nombre del bucket.
  • Text box: entrada libre de texto.

También se pueden encadenar: una variable dispositivo cuya consulta filtre por ${zona}, de forma que al cambiar de zona el desplegable de dispositivos se actualice solo.

Variables de cuadro de mando en Grafana
Variables de cuadro de mando en Grafana

Formato: unidades, umbrales y anulaciones

Un panel que muestra 23.456789 sin más no es un cuadro de mando profesional. En la pestaña de opciones del panel:

  • Unit: unidad de la magnitud (Celsius (°C), Percent (0-100), bar). Grafana ajusta las escalas y los sufijos automáticamente.
  • Decimals: número de decimales.
  • Min / Max: límites del eje o del indicador.
  • Thresholds (umbrales): rangos con color asociado. Por ejemplo, verde hasta 25, amarillo hasta 30, rojo por encima. Los umbrales afectan al color del valor en Stat, a las zonas del Gauge y pueden dibujarse como líneas en Time series.
  • Overrides (anulaciones): reglas que aplican opciones distintas a una serie concreta dentro del mismo panel. Es lo que necesitas cuando pintas temperatura y humedad juntas: la humedad va a un segundo eje, con otra unidad y otro color.

Alertas

Las alertas de Grafana permiten que el sistema avise sin que nadie esté mirando la pantalla. Desde Grafana 9, el sistema unificado de alertas se compone de tres piezas:

  1. Regla de alerta (Alert rule): una consulta, una condición y una frecuencia de evaluación.
  2. Punto de contacto (Contact point): a dónde va la notificación (correo, Slack, Telegram, webhook...).
  3. Política de notificación (Notification policy): qué alertas van a qué contacto, con qué agrupación y silenciamientos.

Para crear una regla desde Alerting → Alert rules → New alert rule:

  1. Consulta (A): la serie a vigilar. Debe devolver datos numéricos en un rango reciente, por ejemplo los últimos 5 minutos.
  2. Expresión de reducción (B): Reduce con función Last o Mean, para colapsar la serie a un único número.
  3. Expresión de umbral (C): Threshold, del tipo IS ABOVE 30.
  4. Evaluación: cada cuánto se comprueba y cuánto tiempo debe mantenerse la condición antes de disparar (pending period).
  5. Etiquetas y anotaciones: el texto que acompañará a la notificación.

El periodo de espera evita el ruido

Si disparas la alerta en cuanto un único punto supera el umbral, un pico de ruido del sensor genera una notificación falsa. Configurando un pending period de, por ejemplo, 2 minutos, la condición debe mantenerse durante ese tiempo antes de que la alerta pase a estado Firing.

Este es exactamente el tipo de decisión de diseño que diferencia un sistema de monitorización utilizable de uno que la gente acaba silenciando.

Los estados por los que pasa una alerta son: Normal → Pending (la condición se cumple pero aún no ha pasado el periodo de espera) → Firing (se notifica) → de vuelta a Normal cuando se resuelve.

Exportar y versionar

Un cuadro de mando es un documento JSON. Desde Dashboard settings → JSON Model puedes verlo, y desde el menú de compartir puedes exportarlo a un archivo.

Al exportar conviene marcar la opción Export for sharing externally: sustituye las referencias fijas a tus orígenes de datos por variables de entrada, de forma que quien lo importe pueda seleccionar los suyos.

Ese JSON es lo que colocamos en grafana/dashboards/ para que el aprovisionamiento lo cargue automáticamente. Tu cuadro de mando debe estar en el repositorio del proyecto: si solo existe dentro del volumen de Docker, un down -v se lo lleva por delante.

Grafana frente a las alternativas

Durante el proyecto habremos visto tres formas de pintar los mismos datos. Conviene tener claro para qué sirve cada una:

Herramienta Fortaleza Limitación
InfluxDB UI Integrada, sin configuración. Ideal para explorar y validar una consulta Solo ve InfluxDB. Cuadros de mando básicos
FlowFuse Dashboard (Node-RED) Interactiva: botones y controles que actúan sobre los flujos Es un panel de control, no una herramienta de análisis histórico
Grafana Multiorigen, alertado, variables, versionable, estándar del sector Solo lectura: observa, no actúa

La distinción importante es que el cuadro de mando de Node-RED es de control (el operario pulsa un botón y algo pasa en la planta) mientras que Grafana es de observación. En una instalación real conviven: Node-RED para la interacción con el proceso, Grafana para el seguimiento y el histórico.

Preparando el terreno para el LLM

En la sesión sobre integración con modelos de lenguaje generaremos informes en texto a partir de los datos. Para mostrarlos en el cuadro de mando usaremos un panel de tipo Text, que admite Markdown.

Hay dos formas de alimentarlo: escribir el informe generado en MySQL y leerlo desde un panel de tabla, o guardarlo en InfluxDB como campo de texto. Lo veremos en detalle entonces; por ahora basta con que sepas que ese panel existe y que un cuadro de mando puede contener texto además de gráficas.

Buenas prácticas

  • Un cuadro de mando responde a una pregunta. Si intentas meterlo todo, nadie lo mira. Mejor tres cuadros bien enfocados que uno con treinta paneles.
  • Coloca arriba el estado actual (paneles Stat y Gauge) y debajo la evolución histórica. La vista de un vistazo primero, el detalle después.
  • Agrega siempre con aggregateWindow(every: v.windowPeriod, ...).
  • Pon unidades y umbrales a todo. Un número sin contexto no informa.
  • Nombra los paneles con lo que responden ("Temperatura por zona"), no con lo que contienen ("Gráfica 1").
  • Exporta el JSON al repositorio cada vez que hagas un cambio relevante.
  • Ajusta el auto-refresco a la frecuencia real del dato: refrescar cada 5 segundos una serie que se actualiza cada minuto solo castiga a la base de datos.

Referencias

Actividades

Para las siguientes actividades se valorará la corrección de las consultas, la claridad del diseño del cuadro de mando y la justificación de las decisiones tomadas.

  1. (RABDA.1 / CEBDA.1d / 0.5p) Configura el origen de datos hacia InfluxDB 2.7 con Flux y comprueba la conexión. Documenta qué URL has utilizado y explica por qué no funciona localhost.

  2. (RABDA.1 / CEBDA.1d / 1p) Construye un cuadro de mando llamado Telemetría OTITIA que contenga, como mínimo:

    1. Un panel Stat con la última lectura de cada magnitud.
    2. Un panel Time series con la evolución de la temperatura de todas las zonas en el rango seleccionado.
    3. Un panel Gauge con la humedad actual, con umbrales de color.
    4. Un panel Table con las últimas 20 alertas registradas.
  3. (RABDA.1 / CEBDA.1d / 0.5p) Añade dos variables encadenadas, zona y dispositivo, obtenidas dinámicamente del propio bucket, y parametriza con ellas al menos dos paneles. El desplegable de dispositivos debe mostrar únicamente los de la zona seleccionada.

  4. (RABDA.1 / CEBDA.1d / 0.5p) Crea una regla de alerta que se dispare cuando la temperatura media de los últimos 5 minutos de cualquier zona supere los 30 °C, con un periodo de espera de 2 minutos. Explica qué ocurriría sin ese periodo de espera y aporta un ejemplo con tus propios datos.

  5. (opcional) Añade el origen de datos hacia InfluxDB 3 Core y reproduce el panel de evolución de temperatura usando SQL en lugar de Flux. Compara ambas consultas: ¿cuál te resulta más legible? ¿Qué ventajas ves en cada enfoque?

  6. (opcional) Exporta tu cuadro de mando con la opción de compartición externa, colócalo en grafana/dashboards/, ejecuta docker compose down -v y comprueba que el aprovisionamiento lo restaura automáticamente al volver a levantar el stack.