# Exporter de la cuota de los dos offsites: MEGA y B2. # # Contexto: el 2026-07-25 la papelera de MEGA (que cuenta cuota) llenó los 20GB # de la cuenta y tumbó rclone-mega-backup y reddit-intel-backup con "Request # over quota". El aviso llegó del síntoma (jobs fallidos), no de la causa, que # llevaba meses engordando en silencio. Esto expone la cuota a Prometheus para # poder alertar con días de margen. # # mega_trash_bytes = usado - visible: es la métrica que habría cazado aquello. # Con --mega-hard-delete en las rotaciones debería quedarse cerca de 0; si # vuelve a crecer, es que algún borrado se está yendo a la papelera otra vez. # # B2 tiene el MISMO fallo con otro nombre: al borrar/espejar no elimina, OCULTA # la versión vieja y la sigue facturando. El mismo día las versiones ocultas de # B2 estaban al ~88% del tramo gratis de 10GB. b2_hidden_bytes = todas las # versiones - visible: con --b2-hard-delete en los scripts debería quedar en ~0. --- apiVersion: v1 kind: ConfigMap metadata: name: mega-quota-exporter namespace: backup-system data: export.sh: | #!/bin/sh # Imagen BusyBox (rclone) — sin GNU-ismos. # Exporta la cuota de MEGA (papelera) y el uso REAL de B2 (versiones ocultas). set -u CONF=/rclone/rclone.conf OUT=/metrics/metrics INTERVAL="${INTERVAL:-900}" B2_REMOTE="b2:chemavx-k3s-backups/" # Extrae el entero que sigue a "clave". Hay que anclar en la clave y no # limpiar la línea entera: `rclone size --json` saca {"count":N,"bytes":M} # en una sola línea y un sed global concatenaría los dos números. num() { sed -n 's/.*"'"$1"'"[^0-9]*\([0-9][0-9]*\).*/\1/p' "$2" 2>/dev/null | head -1; } # Últimos valores buenos por proveedor: si un scrape falla preferimos # republicarlos con success=0 antes que dejar la serie en NoData (falso positivo). L_TOTAL=""; L_USED=""; L_FREE=""; L_VB=""; L_VC="" LB_VIS=""; LB_ALL=""; LB_OBJ="" while true; do # --- MEGA (about da la cuota; la papelera = usado - visible) --- MOK=1 rclone about mega: --json --config "$CONF" > /tmp/about.json 2>/tmp/about.err || MOK=0 rclone size mega: --json --config "$CONF" > /tmp/size.json 2>/tmp/size.err || MOK=0 TOTAL=$(num total /tmp/about.json) USED=$(num used /tmp/about.json) FREE=$(num free /tmp/about.json) VB=$(num bytes /tmp/size.json) VC=$(num count /tmp/size.json) for v in "$TOTAL" "$USED" "$FREE" "$VB" "$VC"; do [ -n "$v" ] || MOK=0 done if [ "$MOK" = "1" ]; then L_TOTAL="$TOTAL"; L_USED="$USED"; L_FREE="$FREE"; L_VB="$VB"; L_VC="$VC" else echo "[$(date '+%Y-%m-%d %H:%M:%S')] MEGA scrape fallido: $(cat /tmp/about.err /tmp/size.err 2>/dev/null | tr '\n' ' ')" fi # --- B2 (sin about: el tramo gratis no lo reporta. Medimos visible vs # TODAS las versiones, que es lo que B2 factura de verdad) --- BOK=1 rclone size "$B2_REMOTE" --json --config "$CONF" > /tmp/b2vis.json 2>/tmp/b2vis.err || BOK=0 rclone size "$B2_REMOTE" --b2-versions --json --config "$CONF" > /tmp/b2all.json 2>/tmp/b2all.err || BOK=0 BVIS=$(num bytes /tmp/b2vis.json) BALL=$(num bytes /tmp/b2all.json) BOBJ=$(num count /tmp/b2vis.json) for v in "$BVIS" "$BALL" "$BOBJ"; do [ -n "$v" ] || BOK=0 done if [ "$BOK" = "1" ]; then LB_VIS="$BVIS"; LB_ALL="$BALL"; LB_OBJ="$BOBJ" else echo "[$(date '+%Y-%m-%d %H:%M:%S')] B2 scrape fallido: $(cat /tmp/b2vis.err /tmp/b2all.err 2>/dev/null | tr '\n' ' ')" fi { echo "# HELP mega_quota_scrape_success 1 si el ultimo sondeo a MEGA funciono." echo "# TYPE mega_quota_scrape_success gauge" echo "mega_quota_scrape_success $MOK" echo "# HELP b2_scrape_success 1 si el ultimo sondeo a B2 funciono." echo "# TYPE b2_scrape_success gauge" echo "b2_scrape_success $BOK" echo "# HELP mega_quota_last_scrape_timestamp_seconds Momento del ultimo sondeo." echo "# TYPE mega_quota_last_scrape_timestamp_seconds gauge" echo "mega_quota_last_scrape_timestamp_seconds $(date +%s)" if [ -n "$L_TOTAL" ]; then TRASH=$((L_USED - L_VB)) [ "$TRASH" -lt 0 ] && TRASH=0 echo "# HELP mega_quota_bytes_total Cuota contratada en MEGA." echo "# TYPE mega_quota_bytes_total gauge" echo "mega_quota_bytes_total $L_TOTAL" echo "# HELP mega_quota_bytes_used Espacio consumido (incluye la papelera)." echo "# TYPE mega_quota_bytes_used gauge" echo "mega_quota_bytes_used $L_USED" echo "# HELP mega_quota_bytes_free Espacio libre segun MEGA." echo "# TYPE mega_quota_bytes_free gauge" echo "mega_quota_bytes_free $L_FREE" echo "# HELP mega_visible_bytes Tamano de los ficheros visibles." echo "# TYPE mega_visible_bytes gauge" echo "mega_visible_bytes $L_VB" echo "# HELP mega_visible_objects Numero de ficheros visibles." echo "# TYPE mega_visible_objects gauge" echo "mega_visible_objects $L_VC" echo "# HELP mega_trash_bytes Estimacion de la papelera (usado - visible)." echo "# TYPE mega_trash_bytes gauge" echo "mega_trash_bytes $TRASH" fi if [ -n "$LB_ALL" ]; then BHID=$((LB_ALL - LB_VIS)) [ "$BHID" -lt 0 ] && BHID=0 echo "# HELP b2_visible_bytes Tamano de los ficheros visibles en B2." echo "# TYPE b2_visible_bytes gauge" echo "b2_visible_bytes $LB_VIS" echo "# HELP b2_billed_bytes Tamano real facturado por B2 (todas las versiones)." echo "# TYPE b2_billed_bytes gauge" echo "b2_billed_bytes $LB_ALL" echo "# HELP b2_hidden_bytes Versiones ocultas/antiguas (facturado - visible)." echo "# TYPE b2_hidden_bytes gauge" echo "b2_hidden_bytes $BHID" echo "# HELP b2_visible_objects Numero de ficheros visibles en B2." echo "# TYPE b2_visible_objects gauge" echo "b2_visible_objects $LB_OBJ" fi } > "${OUT}.tmp" && mv "${OUT}.tmp" "$OUT" sleep "$INTERVAL" done nginx.conf: | worker_processes 1; error_log /dev/stderr warn; pid /tmp/nginx.pid; events { worker_connections 64; } http { access_log off; client_body_temp_path /tmp/client_body; proxy_temp_path /tmp/proxy; fastcgi_temp_path /tmp/fastcgi; uwsgi_temp_path /tmp/uwsgi; scgi_temp_path /tmp/scgi; server { listen 8080; # El fichero no tiene extension: sin esto nginx lo sirve como # application/octet-stream y Prometheus se queja del formato. default_type text/plain; location = /metrics { alias /metrics/metrics; } location / { return 404; } } } --- apiVersion: apps/v1 kind: Deployment metadata: name: mega-quota-exporter namespace: backup-system spec: replicas: 1 selector: matchLabels: app: mega-quota-exporter template: metadata: labels: app: mega-quota-exporter spec: containers: - name: rclone # OJO: imagen BusyBox — el script debe evitar GNU-ismos image: docker.io/rclone/rclone:1.74.3 command: ["/bin/sh", "/scripts/export.sh"] env: - name: INTERVAL value: "900" resources: requests: cpu: 10m memory: 32Mi limits: cpu: 200m memory: 128Mi volumeMounts: - mountPath: /rclone name: rclone-conf - mountPath: /scripts name: scripts - mountPath: /metrics name: metrics - name: nginx image: docker.io/library/nginx:1.27-alpine ports: - name: metrics containerPort: 8080 resources: requests: cpu: 10m memory: 16Mi limits: cpu: 100m memory: 64Mi volumeMounts: - mountPath: /etc/nginx/nginx.conf name: scripts subPath: nginx.conf - mountPath: /metrics name: metrics readOnly: true readinessProbe: httpGet: path: /metrics port: metrics initialDelaySeconds: 10 periodSeconds: 20 volumes: # RCLONE_CONF (Infisical /backup-system) contiene mega+b2 - name: rclone-conf secret: secretName: rclone-conf-infisical items: - key: RCLONE_CONF path: rclone.conf - name: scripts configMap: name: mega-quota-exporter defaultMode: 0755 - name: metrics emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: mega-quota-exporter namespace: backup-system labels: app: mega-quota-exporter spec: selector: app: mega-quota-exporter ports: - name: metrics port: 8080 targetPort: metrics --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: mega-quota-exporter namespace: backup-system labels: # Requerido por serviceMonitorSelector de kube-prometheus-stack release: kube-prometheus-stack spec: selector: matchLabels: app: mega-quota-exporter endpoints: - port: metrics path: /metrics interval: 5m