--- title: "Cómo crear un exporter de Prometheus en Python y Go" description: "Desarrolla un exporter de Prometheus en Python con prometheus_client, ejecútalo con systemd en Ubuntu 24.04 y crea la misma versión en Go con client_golang." category: "Stack de Observabilidad" language: es source: https://cubepath.com/es/docs/stack-de-observabilidad/desarrollo-de-exportadores-prometheus-personalizados updated: 2026-09-25 --- # Cómo crear un exporter de Prometheus en Python y Go Cuando ningún exporter existente mide lo que necesitas, puedes escribir el tuyo: un pequeño servicio que calcula unas métricas y las publica por HTTP en el formato que Prometheus entiende. En este tutorial crearás un exporter real que vigila un directorio de copias de seguridad (cuántas hay, cuánto ocupan y cuándo se hizo la última), lo ejecutarás como servicio de systemd en Ubuntu 24.04, lo conectarás a Prometheus con una alerta de copia atrasada y verás la misma implementación en Go. ## Requisitos previos Para seguir esta guía necesitas: - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con Python 3.12 (incluido en Ubuntu 24.04). - Un usuario no root con privilegios `sudo`. - Un directorio con copias de seguridad que quieras vigilar. En los ejemplos es `/var/backups/app` con archivos `*.tar.gz`. - Un servidor Prometheus con su configuración en `/etc/prometheus/prometheus.yml`, para el paso 6. - Para la sección de Go, Go 1.25 o posterior en tu equipo de desarrollo. ## Conceptos: tipos de métrica e instrumentación frente a exporter Prometheus distingue cuatro tipos de métrica: | Tipo | Qué representa | Ejemplo | |---|---|---| | Counter | Valor que solo crece (o vuelve a 0 al reiniciar) | Peticiones atendidas, errores | | Gauge | Valor que sube y baja | Archivos en un directorio, memoria usada | | Histogram | Distribución de observaciones en rangos (buckets) | Duración de peticiones | | Summary | Cuantiles calculados en el cliente | Latencia p99 de un único proceso | Hay dos formas de generar métricas propias: - **Instrumentación directa**: tu aplicación crea contadores e histogramas y los actualiza cuando ocurre algo. Es lo adecuado para código propio. En Python sería: ```python from prometheus_client import Counter, Histogram REQUESTS = Counter("myapp_requests", "Peticiones procesadas.", ["method", "status"]) LATENCY = Histogram("myapp_request_duration_seconds", "Duración de las peticiones.") REQUESTS.labels(method="GET", status="200").inc() with LATENCY.time(): handle_request() ``` - **Exporter con collector propio**: un proceso aparte que, en cada scrape, consulta un sistema que no controlas (un directorio, una API, un dispositivo) y devuelve el estado actual. Es lo que construirás en esta guía. Sigue las convenciones de nombres de Prometheus: minúsculas con guiones bajos, un prefijo común para todo el exporter (`backup_`), unidades base en el nombre (`_seconds`, `_bytes`) y el sufijo `_total` solo en counters. Evita etiquetas con valores ilimitados, como nombres de archivo o IDs de usuario: cada combinación de etiquetas crea una serie temporal nueva. ## Paso 1: Preparar el entorno de Python Instala el módulo `venv`, crea un directorio para el exporter y un entorno virtual con la librería oficial `prometheus-client`: ```bash sudo apt update sudo apt install python3-venv sudo mkdir -p /opt/backup_exporter sudo python3 -m venv /opt/backup_exporter/venv sudo /opt/backup_exporter/venv/bin/pip install prometheus-client ``` Comprueba la versión instalada: ```bash /opt/backup_exporter/venv/bin/pip show prometheus-client | grep Version ``` ```output Version: 0.26.0 ``` ## Paso 2: Escribir el exporter El exporter usa un collector propio: una clase con un método `collect()` que la librería llama en cada petición a `/metrics`. Así las métricas siempre reflejan el estado del directorio en el momento del scrape, sin hilos en segundo plano ni valores desfasados. Crea el archivo: ```bash sudo nano /opt/backup_exporter/backup_exporter.py ``` ```python #!/usr/bin/env python3 """Exporter de Prometheus con el estado de las copias de seguridad de un directorio.""" import argparse import os import time from pathlib import Path from prometheus_client import start_http_server from prometheus_client.core import REGISTRY, GaugeMetricFamily from prometheus_client.registry import Collector def gauge(name, documentation, value): metric = GaugeMetricFamily(name, documentation) metric.add_metric([], value) return metric class BackupCollector(Collector): def __init__(self, directory: Path, pattern: str) -> None: self.directory = directory self.pattern = pattern def collect(self): start = time.monotonic() readable = self.directory.is_dir() and os.access(self.directory, os.R_OK | os.X_OK) yield gauge("backup_directory_up", "1 si el directorio de copias se puede leer.", int(readable)) if not readable: return files = [] for path in self.directory.glob(self.pattern): try: st = path.stat() except FileNotFoundError: continue # el archivo se borró entre el listado y el stat if path.is_file(): files.append(st) yield gauge("backup_files", "Número de copias en el directorio.", len(files)) yield gauge("backup_files_size_bytes", "Tamaño total de las copias en bytes.", sum(st.st_size for st in files)) if files: newest = max(files, key=lambda st: st.st_mtime) yield gauge("backup_last_modified_timestamp_seconds", "Fecha de modificación de la copia más reciente (tiempo Unix).", newest.st_mtime) yield gauge("backup_last_size_bytes", "Tamaño de la copia más reciente en bytes.", newest.st_size) yield gauge("backup_exporter_collect_duration_seconds", "Tiempo empleado en recoger las métricas.", time.monotonic() - start) def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--directory", type=Path, required=True) parser.add_argument("--pattern", default="*.tar.gz") parser.add_argument("--listen-address", default="127.0.0.1") parser.add_argument("--port", type=int, default=9900) args = parser.parse_args() REGISTRY.register(BackupCollector(args.directory, args.pattern)) start_http_server(args.port, addr=args.listen_address) print(f"Escuchando en http://{args.listen_address}:{args.port}/metrics", flush=True) while True: time.sleep(3600) if __name__ == "__main__": main() ``` Puntos importantes del código: - `GaugeMetricFamily` crea métricas de tipo gauge con un valor calculado en ese momento. Hay equivalentes para los demás tipos (`CounterMetricFamily`, `HistogramMetricFamily`). - `backup_directory_up` indica si el directorio se puede leer. Sin ella, un directorio inaccesible se confundiría con uno vacío. - La fecha de la última copia se publica como marca de tiempo Unix y no como "horas desde la última copia". Así el cálculo de la antigüedad se hace en PromQL con `time()` y el valor es correcto aunque Prometheus lo lea con retraso. - El exporter escucha por defecto solo en `127.0.0.1`. Para que lo alcance un Prometheus remoto se pasa `--listen-address 0.0.0.0` en el servicio. - `start_http_server` también publica métricas del propio proceso (`process_*`, `python_gc_*`), útiles para vigilar el exporter. Pruébalo manualmente con un directorio temporal: ```bash mkdir -p /tmp/backups-test head -c 1000 /dev/urandom > /tmp/backups-test/app-1.tar.gz /opt/backup_exporter/venv/bin/python /opt/backup_exporter/backup_exporter.py --directory /tmp/backups-test ``` ```output Escuchando en http://127.0.0.1:9900/metrics ``` Desde otra terminal, consulta las métricas: ```bash curl -s http://127.0.0.1:9900/metrics | grep '^backup_' ``` ```output backup_directory_up 1.0 backup_files 1.0 backup_files_size_bytes 1000.0 backup_last_modified_timestamp_seconds 1.7903402298785698e+09 backup_last_size_bytes 1000.0 backup_exporter_collect_duration_seconds 0.00020041607785969973 ``` Detén el exporter con `Ctrl+C` en la primera terminal. ## Paso 3: Validar el formato de las métricas `promtool`, la herramienta de línea de comandos de Prometheus, comprueba que la salida cumple el formato de exposición y las convenciones de nombres. En Ubuntu está en el paquete `prometheus` (si Prometheus está en otro servidor, instala solo ese paquete aquí o ejecuta la comprobación allí contra la URL del exporter): ```bash curl -s http://127.0.0.1:9900/metrics | promtool check metrics ``` Si no muestra nada, el formato es correcto. Si hay problemas, `promtool` indica la métrica y el motivo, por ejemplo un counter sin el sufijo `_total` o una unidad no estándar. ## Paso 4: Ejecutar el exporter como servicio Crea un usuario de sistema para el exporter: ```bash sudo useradd --system --no-create-home --shell /usr/sbin/nologin backup_exporter ``` Este usuario necesita permiso de lectura y de acceso al directorio de copias. Si el directorio pertenece a root con permisos restrictivos, la forma más limpia de dárselo sin cambiar el propietario es una ACL: ```bash sudo apt install acl sudo setfacl -m u:backup_exporter:rx /var/backups/app ``` Crea la unidad de systemd: ```bash sudo nano /etc/systemd/system/backup_exporter.service ``` ```ini [Unit] Description=Prometheus backup exporter Wants=network-online.target After=network-online.target [Service] User=backup_exporter Group=backup_exporter ExecStart=/opt/backup_exporter/venv/bin/python /opt/backup_exporter/backup_exporter.py \ --directory /var/backups/app \ --pattern "*.tar.gz" \ --listen-address 0.0.0.0 \ --port 9900 Restart=on-failure NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true [Install] WantedBy=multi-user.target ``` `ProtectSystem=strict` monta todo el sistema de archivos en solo lectura para el servicio, lo que es suficiente porque el exporter nunca escribe nada. Arranca el servicio y comprueba que responde: ```bash sudo systemctl daemon-reload sudo systemctl enable --now backup_exporter curl -s http://127.0.0.1:9900/metrics | grep '^backup_directory_up' ``` ```output backup_directory_up 1.0 ``` Si devuelve `0.0`, el usuario `backup_exporter` no puede leer el directorio: revisa la ACL con `getfacl /var/backups/app`. El puerto 9900 no está asignado a ningún exporter conocido, pero si ejecutas muchos exporters consulta la [lista de puertos por defecto](https://github.com/prometheus/prometheus/wiki/Default-port-allocations) del proyecto para evitar colisiones. Ábrelo solo al servidor de Prometheus: ```bash sudo ufw allow from prometheus_server_ip to any port 9900 proto tcp ``` ## Paso 5: Implementar el mismo exporter en Go Go genera un único binario estático sin dependencias en el servidor, lo que simplifica el despliegue. La librería oficial es `client_golang`, y la versión actual requiere Go 1.25 o posterior (el paquete `golang-go` de Ubuntu 24.04 es más antiguo, así que instala Go desde [go.dev/dl](https://go.dev/dl/)). En tu equipo de desarrollo, crea el módulo: ```bash mkdir backup_exporter && cd backup_exporter go mod init backup_exporter go get github.com/prometheus/client_golang@latest ``` Crea `main.go`: ```go package main import ( "flag" "log" "net/http" "os" "path/filepath" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( upDesc = prometheus.NewDesc("backup_directory_up", "1 si el directorio de copias se puede leer.", nil, nil) filesDesc = prometheus.NewDesc("backup_files", "Número de copias en el directorio.", nil, nil) sizeDesc = prometheus.NewDesc("backup_files_size_bytes", "Tamaño total de las copias en bytes.", nil, nil) lastDesc = prometheus.NewDesc("backup_last_modified_timestamp_seconds", "Fecha de modificación de la copia más reciente (tiempo Unix).", nil, nil) ) type backupCollector struct { dir string pattern string } func (c backupCollector) Describe(ch chan<- *prometheus.Desc) { prometheus.DescribeByCollect(c, ch) } func (c backupCollector) Collect(ch chan<- prometheus.Metric) { if info, err := os.Stat(c.dir); err != nil || !info.IsDir() { ch <- prometheus.MustNewConstMetric(upDesc, prometheus.GaugeValue, 0) return } ch <- prometheus.MustNewConstMetric(upDesc, prometheus.GaugeValue, 1) // El patrón se valida al arrancar, así que Glob no puede devolver error. matches, _ := filepath.Glob(filepath.Join(c.dir, c.pattern)) var count, total, newest float64 for _, m := range matches { info, err := os.Stat(m) if err != nil || !info.Mode().IsRegular() { continue } count++ total += float64(info.Size()) if t := float64(info.ModTime().Unix()); t > newest { newest = t } } ch <- prometheus.MustNewConstMetric(filesDesc, prometheus.GaugeValue, count) ch <- prometheus.MustNewConstMetric(sizeDesc, prometheus.GaugeValue, total) if count > 0 { ch <- prometheus.MustNewConstMetric(lastDesc, prometheus.GaugeValue, newest) } } func main() { dir := flag.String("directory", "", "directorio de las copias") pattern := flag.String("pattern", "*.tar.gz", "patrón de los archivos") addr := flag.String("listen-address", "127.0.0.1:9900", "dirección de escucha") flag.Parse() if *dir == "" { log.Fatal("falta --directory") } if _, err := filepath.Match(*pattern, ""); err != nil { log.Fatalf("patrón no válido: %v", err) } reg := prometheus.NewRegistry() reg.MustRegister( backupCollector{dir: *dir, pattern: *pattern}, collectors.NewGoCollector(), collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), ) http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) log.Printf("Escuchando en http://%s/metrics", *addr) log.Fatal(http.ListenAndServe(*addr, nil)) } ``` En Go, un collector implementa `Describe` y `Collect`. `MustNewConstMetric` crea una métrica con el valor calculado en ese momento, el equivalente a `GaugeMetricFamily` en Python. Al usar un registro propio (`prometheus.NewRegistry()`) solo se publican las métricas que registras de forma explícita, en este caso las del collector y las del runtime de Go y del proceso. Resuelve dependencias y compila para Linux x86_64 (funciona desde cualquier sistema operativo): ```bash go mod tidy GOOS=linux GOARCH=amd64 go build -o backup_exporter . ``` Copia el binario al servidor en `/usr/local/bin/backup_exporter` y cambia la línea `ExecStart` de la unidad por: ```ini ExecStart=/usr/local/bin/backup_exporter --directory /var/backups/app --listen-address 0.0.0.0:9900 ``` Las métricas `backup_*` tienen los mismos nombres que en Python, así que Prometheus y las alertas no cambian. ## Paso 6: Añadir el exporter a Prometheus y crear una alerta En el servidor de Prometheus, añade el trabajo dentro de `scrape_configs` en `/etc/prometheus/prometheus.yml`, sustituyendo `backup_server_ip` por la IP del servidor con las copias: ```yaml - job_name: "backups" static_configs: - targets: ["backup_server_ip:9900"] ``` Crea el archivo de reglas: ```bash sudo nano /etc/prometheus/backup_alerts.yml ``` ```yaml groups: - name: backups rules: - alert: BackupTooOld expr: time() - backup_last_modified_timestamp_seconds > 26 * 3600 for: 15m labels: severity: critical annotations: summary: "No hay copias recientes en {{ $labels.instance }}" description: "La última copia tiene {{ $value | humanizeDuration }} de antigüedad." - alert: BackupDirectoryUnreadable expr: backup_directory_up == 0 for: 15m labels: severity: warning annotations: summary: "El exporter no puede leer el directorio de copias en {{ $labels.instance }}" ``` `BackupTooOld` salta cuando la copia más reciente tiene más de 26 horas, un margen razonable para una copia diaria. Regístralo en `rule_files` dentro de `/etc/prometheus/prometheus.yml`: ```yaml rule_files: - "backup_alerts.yml" ``` Valida y reinicia Prometheus: ```bash promtool check rules /etc/prometheus/backup_alerts.yml promtool check config /etc/prometheus/prometheus.yml sudo systemctl restart prometheus ``` ```output Checking /etc/prometheus/backup_alerts.yml SUCCESS: 2 rules found ``` En **Status > Targets** el trabajo `backups` debe aparecer como `UP`. Para ver la antigüedad de la última copia en horas, ejecuta en la pestaña **Graph**: ```text (time() - backup_last_modified_timestamp_seconds) / 3600 ``` ## Solución de problemas **`backup_directory_up` vale 0 aunque el directorio existe.** El usuario del servicio no tiene permisos `r` y `x` sobre el directorio. Pruébalo con `sudo -u backup_exporter ls /var/backups/app`. **El servicio falla con `ModuleNotFoundError: No module named 'prometheus_client'`.** `ExecStart` está usando el Python del sistema en lugar del del entorno virtual. Revisa que la ruta empieza por `/opt/backup_exporter/venv/bin/python`. **Prometheus marca el destino como `DOWN` con `connection refused`.** El exporter escucha en `127.0.0.1` o UFW bloquea el puerto. Comprueba con `sudo ss -ltnp | grep 9900` que escucha en `0.0.0.0:9900` y revisa `sudo ufw status`. **Los scrapes tardan mucho.** Recorrer directorios con miles de archivos en cada scrape es costoso. Consulta `backup_exporter_collect_duration_seconds` y, si supera un segundo, restringe el `--pattern` o sube el `scrape_interval` de ese trabajo. ## Conclusión Has creado un exporter de Prometheus con un collector propio en Python, lo has desplegado como servicio de systemd con permisos mínimos, has visto la implementación equivalente en Go y has configurado Prometheus para alertar cuando las copias se quedan atrasadas. El mismo patrón sirve para exponer cualquier dato que hoy solo consultas a mano: una API interna, una cola de trabajos o el estado de un dispositivo. Como siguientes pasos, puedes añadir etiquetas acotadas (por ejemplo, una por aplicación si vigilas varios directorios), publicar un panel en Grafana con estas métricas o enviar las alertas a Alertmanager.