Els tres projectes anteriors miren cap endins del servidor: el seu estat, els seus registres, les seves dades. Aquest mira cap enfora. Quan algú a Veloz Envíos diu «la xarxa va malament», normalment vol dir que l'aplicació triga, i la feina d'operacions és convertir aquesta frase en una mesura. monitor-xarxa.sh fa exactament això: defineix de manera mesurable què significa que la xarxa i el servei estan bé, ho comprova en paral·lel cada pocs minuts, alerta només quan alguna cosa canvia i desa un històric del qual surt la disponibilitat del mes.

Contingut

  1. Què significa «la xarxa va bé», de forma mesurable
  2. Comprovació puntual davant de tendència
  3. Disseny: catàleg d'objectius i una funció per tipus
  4. Les comprovacions, una a una
  5. El despatxador i el resultat normalitzat
  6. Paral·lelisme: per què aquí sí que compensa
  7. Reintents amb retrocés
  8. Estat i alertes només en les transicions
  9. Finestres de manteniment
  10. Històric i subcomandament resum
  11. Mode directe, timer i posada en producció

  1. Què significa «la xarxa va bé», de forma mesurable

«Va bé» no és monitoritzable. Aquestes cinc frases sí:

Afirmació mesurable Com es comprova Llindar a Veloz Envíos
L'amfitrió respon ping -c1 -W2 Pèrdua 0 %, RTT < 50 ms
El nom resol dig +short Retorna com a mínim una A, < 500 ms
El port està obert nc -z -w2 o /dev/tcp Connexió en < 2 s
L'API respon correctament curl -w '%{http_code} %{time_total}' 200 i < 800 ms
El certificat no ha caducat openssl s_client Més de 15 dies de marge

Cada fila és una comprovació independent, amb el seu llindar i la seva criticitat. La distinció importa: un ping que respon no diu res sobre si l'API funciona, i una API que retorna 200 en quatre segons està trencada encara que el codi sigui correcte. Un servei es declara sa per la seva resposta útil, no per la seva accessibilitat.

  1. Comprovació puntual davant de tendència

Són dos productes diferents que comparteixen dades:

  • La comprovació puntual respon ara: està caigut? La seva sortida és un estat i la seva conseqüència possible és una alerta.
  • La tendència respon al llarg del mes: quant ha estat disponible, com evoluciona la latència? La seva sortida és un número d'informe i la seva conseqüència és una decisió d'arquitectura.

Confondre-les produeix les dues fallades clàssiques: alertar per una latència que fa tres setmanes que puja (tard) o dibuixar gràfiques sense que ningú s'assabenti d'una caiguda (inútil). El disseny les separa: cada execució alerta si escau i afegeix una línia a l'històric; el subcomandament resum només llegeix l'històric.

  1. Disseny: catàleg d'objectius i una funció per tipus

Igual que els perfils de còpia (09-03), els objectius viuen en un fitxer:

# etc/monitor-xarxa.d/objectius.conf
# nom|tipus|desti|llindar|criticitat
api-salut|http|http://localhost:8080/salud|800|critica
api-enviaments|http|http://localhost:8080/envios?ciudad=Valencia|1500|alta
db-port|port|10.0.0.20:5432|2000|critica
dns-intern|dns|api.veloz.local|500|mitjana
srv-02|ping|srv-veloz-02|50|alta
passarella|ping|10.0.0.1|30|critica

El separador és | i no pas la coma perquè les URL porten comes amb facilitat i les barres verticals no. Es llegeix amb IFS='|' read (03-06), ignorant comentaris i línies buides:

llegeix_objectius() {
  local nom tipus desti llindar crit
  while IFS='|' read -r nom tipus desti llindar crit; do
    [[ $nom == \#* || -z $nom ]] && continue
    printf '%s|%s|%s|%s|%s\n' "$nom" "$tipus" "$desti" "$llindar" "${crit:-mitjana}"
  done < "$OBJECTIUS"
}

Cada tipus té la seva funció comprova_<tipus> i retorna el conveni dels connectors clàssics (07-04): 0 = OK, 1 = AVÍS, 2 = CRÍTIC, 3 = DESCONEGUT. Aquest conveni és el que permet que la resta de l'script no sàpiga res de ping ni de curl.

  1. Les comprovacions, una a una

comprova_ping() {  # $1 desti, $2 llindar ms -> imprimeix "ms missatge"
  local sortida rtt
  sortida=$(ping -c1 -W2 -n "$1" 2>/dev/null) || { printf '0 sense resposta\n'; return 2; }
  rtt=${sortida#*time=}; rtt=${rtt%% *}
  printf '%s rtt=%sms\n' "$rtt" "$rtt"
  awk -v r="$rtt" -v u="$2" 'BEGIN {exit !(r+0 > u)}' && return 1 || return 0
}

comprova_dns() {
  local ini final ip
  ini=$(date +%s%3N); ip=$(timeout 3 dig +short +time=2 +tries=1 "$1" A | head -1)
  final=$(date +%s%3N)
  [[ -n $ip ]] || { printf '0 no resol\n'; return 2; }
  printf '%s resol a %s\n' "$((final - ini))" "$ip"
  (( final - ini > $2 )) && return 1 || return 0
}

comprova_port() {
  local host=${1%:*} port=${1##*:} ini final
  ini=$(date +%s%3N)
  timeout 2 bash -c "exec 3<>/dev/tcp/$host/$port" 2>/dev/null ||
    { printf '0 tancat o filtrat\n'; return 2; }
  final=$(date +%s%3N); printf '%s obert\n' "$((final - ini))"
}

comprova_http() {
  local resp cod ms
  resp=$(curl -sS -o /dev/null --max-time 5 -w '%{http_code} %{time_total}' "$1" 2>/dev/null) \
    || { printf '0 sense connexio\n'; return 2; }
  read -r cod ms <<< "$resp"
  ms=$(awk -v t="$ms" 'BEGIN {printf "%d", t * 1000}')
  [[ $cod == 200 ]] || { printf '%s codi %s\n' "$ms" "$cod"; return 2; }
  printf '%s 200 en %sms\n' "$ms" "$ms"
  (( ms > $2 )) && return 1 || return 0
}

Detalls que vénen de 06-04 i 06-05. ping -n evita la resolució inversa, que pot afegir segons d'espera per un DNS lent i falsejar la mesura. /dev/tcp amb timeout no necessita nc instal·lat, però és un bashisme pur (08-07): si l'script hagués de córrer en dash, aquí hi aniria nc -z -w2. curl -w dóna el codi i el temps en una sola invocació, en lloc de mesurar-ho des de fora amb date; --max-time és obligatori, perquè una comprovació sense límit pot penjar tot el cicle. I l'HTTP 200 s'exigeix explícitament: un 302 cap a una pàgina de manteniment no és servei. Cada funció imprimeix <mil·lisegons> <missatge> i retorna l'estat pel codi de sortida: separar la dada (stdout) del veredicte (codi) és la mateixa divisió de 09-01 entre recollir i decidir.

  1. El despatxador i el resultat normalitzat

executa_objectiu() {  # nom|tipus|desti|llindar|criticitat -> linia TSV
  local nom tipus desti llindar crit
  IFS='|' read -r nom tipus desti llindar crit <<< "$1"
  local fn=comprova_$tipus sortida estat
  declare -F "$fn" >/dev/null || { veloz_log_error "tipus desconegut: $tipus"; return 3; }
  sortida=$(amb_reintents "$fn" "$desti" "$llindar"); estat=$?
  printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
    "$(date -Is)" "$nom" "$tipus" "$estat" "${sortida%% *}" "${sortida#* }"
}

declare -F com a llista blanca (08-03): el tipus del fitxer es converteix en nom de funció només si aquesta funció existeix. Un case explícit seria igual de vàlid (04-05) i més llegible per a qui no conegui el truc; l'avantatge de declare -F és que afegir un tipus nou és escriure una funció i res més. El resultat normalitzat —data ISO, nom, tipus, estat, mil·lisegons, missatge— és el format intern que consumeixen l'informe, les alertes i l'històric: un altre cop la mateixa arquitectura dels tres projectes anteriors.

  1. Paral·lelisme: per què aquí sí que compensa

Amb dotze objectius i una mitjana de 700 ms cadascun, en sèrie són vuit segons i mig. I no són vuit segons de CPU: són vuit segons d'espera. Aquest és el criteri de 08-02 per paral·lelitzar: quan el procés està bloquejat esperant la xarxa, llançar-ne vint alhora no costa pràcticament res.

executa_tots() {
  local tmp; tmp=$(mktemp -d); trap 'rm -rf "$tmp"' RETURN
  local i=0 obj
  while IFS= read -r obj; do
    executa_objectiu "$obj" > "$tmp/$(printf '%03d' "$i")" &
    ((i++))
    (( $(jobs -rp | wc -l) >= PARALLELISME )) && wait -n
  done < <(llegeix_objectius)
  wait
  cat "$tmp"/*
}

Tres coses fan que això sigui correcte i no un caos. Cada treball escriu en el seu propi fitxer temporal numerat (05-02): si tots escrivissin en el mateix, les línies s'entrellaçarien. El nom porta un índex amb zeros a l'esquerra (%03d) perquè el cat final retorni l'ordre del catàleg, no l'ordre d'acabament —un informe les files del qual ballen a cada execució és illegible i fa impossible comparar dues sortides—. I wait -n limita els treballs simultanis: espera que n'acabi un qualsevol abans de llançar el següent, que és l'equivalent d'xargs -P amb processos de fons. Amb xargs -0 -P "$PARALLELISME" el codi seria més curt, a canvi d'haver d'exportar les funcions; amb vint objectius, qualsevol de les dues serveix.

  1. Reintents amb retrocés

Un paquet perdut no és una caiguda. Alertar per la primera comprovació fallida genera el soroll que fa que la gent ignori les alertes, que és l'avaria pitjor possible d'un sistema de monitoratge.

amb_reintents() {  # $1 = funcio, resta = arguments
  local fn=$1 intent espera=1 sortida estat; shift
  for intent in 1 2 3; do
    sortida=$("$fn" "$@"); estat=$?
    (( estat == 0 )) && { printf '%s' "$sortida"; return 0; }
    (( intent < 3 )) && { sleep "$espera"; espera=$((espera * 2)); }
  done
  printf '%s' "$sortida"; return "$estat"
}

Retrocés exponencial (05-03): 1 s, 2 s. Només es reintenta la fallada i se surt al primer èxit, així que el cas normal no costa res. Tres intents i un màxim de tres segons d'espera són suficients per filtrar el soroll sense endarrerir el cicle. Compte amb multiplicar: tres reintents per dotze objectius seqüencials serien un cicle de minut i mig en una apagada general; amb el paral·lelisme de l'apartat anterior, continuen sent quatre segons.

  1. Estat i alertes només en les transicions

El fitxer d'estat desa l'última situació coneguda de cada objectiu, i l'alerta es dispara únicament quan canvia (07-04):

processa_estats() {
  declare -A previ
  [[ -r $ESTAT ]] && while IFS=$'\t' read -r n e; do previ[$n]=$e; done < "$ESTAT"
  local nou=$ESTAT.parcial ant; : > "$nou"
  local data nom tipus estat ms msg
  while IFS=$'\t' read -r data nom tipus estat ms msg; do
    printf '%s\t%s\n' "$nom" "$estat" >> "$nou"
    ant=${previ[$nom]:-0}
    (( ant == 0 && estat != 0 )) && alerta FALLADA "$nom" "$msg" "$ms"
    (( ant != 0 && estat == 0 )) && alerta RECUPERAT "$nom" "$msg" "$ms"
  done
  mv "$nou" "$ESTAT"
}

Només dues transicions generen avís: OK→FALLADA i FALLADA→OK. Un objectiu que fa sis hores que és caigut no torna a avisar cada cinc minuts, i l'avís de recuperació és tan important com el de fallada, perquè tanca l'incident sense que ningú hagi d'anar a mirar. L'escriptura de l'estat fa servir un altre cop .parcial + mv (08-03): si l'script mor a mitges, el fitxer d'estat anterior queda intacte, i un estat corrupte provocaria una tempesta de falses transicions a l'execució següent.

L'enviament fa servir jq -n per construir el cos (06-05), mai printf amb el missatge interpolat:

alerta() {
  local classe=$1 objectiu=$2 detall=$3 ms=$4
  veloz_log_info "alerta $classe $objectiu: $detall"
  en_manteniment "$objectiu" && { veloz_log_info "silenciada (manteniment)"; return 0; }
  [[ -n ${WEBHOOK:-} ]] || return 0
  jq -n --arg c "$classe" --arg o "$objectiu" --arg d "$detall" --arg h "$(hostname -s)" \
     '{text: "[\($c)] \($o) a \($h): \($d)"}' |
    curl -sS --max-time 10 -X POST -H 'Content-Type: application/json' -d @- "$WEBHOOK" >/dev/null ||
    veloz_log_error "no s'ha pogut enviar l'alerta"
}

La fallada del webhook es registra però no avorta el monitor: el sistema d'avisos caigut no s'ha d'endur per davant la vigilància. I el missatge sempre s'escriu primer al registre local, perquè en quedi constància encara que no surti.

  1. Finestres de manteniment

# etc/monitor-xarxa.d/manteniment.conf -> objectiu|inici ISO|final ISO
en_manteniment() {
  local obj=$1 ara o ini final; ara=$(date +%s)
  [[ -r $MANTENIMENT ]] || return 1
  while IFS='|' read -r o ini final; do
    [[ $o == "$obj" || $o == '*' ]] || continue
    (( ara >= $(date -d "$ini" +%s) && ara <= $(date -d "$final" +%s) )) && return 0
  done < "$MANTENIMENT"
  return 1
}

El silenciament suprimeix l'alerta, no la comprovació: l'històric continua registrant l'estat real, així que la disponibilitat del mes no es falseja. És una distinció que molts sistemes comercials fan malament, i la diferència entre «no em molestis ara» i «fem veure que no ha passat».

  1. Històric i subcomandament resum

Cada execució afegeix les seves línies a logs/xarxa-AAAA-MM.tsv. Un fitxer per mes manté la mida acotada sense necessitat de logrotate i fa trivial l'informe mensual.

resum() {  # $1 = fitxer mensual
  awk -F'\t' '
    { n[$2]++; if ($4 == 0) ok[$2]++; if ($5 + 0 > 0) { suma[$2] += $5; m[$2]++ }
      if ($4 != 0) fallades[$2]++ }
    END {
      printf "%-14s %8s %8s %10s %8s\n", "OBJECTIU", "MOSTRES", "DISPON.", "LAT.MITJ.", "FALLADES"
      for (k in n)
        printf "%-14s %8d %7.2f%% %8.0fms %8d\n", k, n[k], ok[k]*100/n[k],
               (m[k] ? suma[k]/m[k] : 0), fallades[k]+0
    }' "$1" | (read -r capc; printf '%s\n' "$capc"; sort -k3 -n)
}

La disponibilitat és un quocient entre mostres, no entre minuts: amb un cicle de cinc minuts, cada mostra fallida representa cinc minuts d'indisponibilitat, i convé dir-ho a l'informe perquè ningú confongui la precisió amb l'exactitud. La latència mitjana exclou les mostres amb 0 ms, que són fallades i no mesures —incloure-les abaixaria artificialment la mitjana justament quan el servei va pitjor, l'error d'interpretació més comú en monitoratge—. El (read -r capc; ...) conserva la capçalera a dalt mentre ordena la resta per disponibilitat ascendent, és a dir, els pitjors primer.

  1. Mode directe, timer i posada en producció

Per a una sessió de diagnòstic va bé un bucle propi, que és preferible a watch perquè conserva l'històric i les alertes:

mode_directe() {  # \033[H\033[J neteja la pantalla a cada volta
  while true; do printf '\033[H\033[J'; executa_tots | formata_text; sleep "${INTERVAL:-30}"; done
}

Però en producció mana un timer de systemd (07-05) cada cinc minuts, per tres raons concretes: sobreviu al tancament de la sessió SSH, journalctl -u conserva la traça de cada execució, i systemctl list-timers mostra si de debò s'està executant. Un bucle en un terminal oblidat sembla monitoratge fins al dia que algú tanca el portàtil.

Errors Habituals i Consells

  • Monitoritzar el ping i dir-ne servei. L'amfitrió respon i l'API retorna 500: la comprovació que importa és la que fa el que fa l'usuari.
  • Alertar a cada execució mentre dura la fallada. És la via ràpida perquè s'ignorin les alertes. Només transicions, i avís de recuperació sempre.
  • Comprovacions sense timeout. Un objectiu que no respon mai bloqueja el cicle sencer; amb paral·lelisme, esgota els processos. --max-time, -W, timeout: sempre.
  • Paral·lelitzar escrivint tots al mateix fitxer. Les línies s'entrellacen: un temporal per treball i consolidació ordenada. I durant el manteniment, silencia l'alerta, mai la comprovació, o falsejaràs la disponibilitat de l'informe mensual.
  • Consell: monitoritza també el mateix monitor. Si veloz-monitor.timer no s'executa, ningú no se n'adona; una comprovació d'«antiguitat de l'última línia de l'històric» a vigilant.sh tanca aquest forat.

Exercicis

  1. Caducitat del certificat TLS. Afegeix el tipus tls, que avisi quan faltin menys de 15 dies per a la caducitat i sigui crític per sota de 5.
  2. Traça de ruta davant d'una fallada. Quan un objectiu ping passi a FALLADA, adjunta automàticament al registre una traça de ruta cap al destí, sense endarrerir el cicle normal.
  3. Informe de tendència. Estén resum amb --comparar MES, que mostri la variació de disponibilitat i latència respecte al mes anterior, marcant els empitjoraments.

Solucions

1. openssl s_client necessita -servername per a SNI i una entrada per stdin que tanqui la connexió:

comprova_tls() {
  local host=${1%:*} port=${1##*:} caducitat dies
  caducitat=$(echo | timeout 5 openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null |
        openssl x509 -noout -enddate 2>/dev/null) || { printf '0 sense certificat\n'; return 2; }
  caducitat=${caducitat#notAfter=}; dies=$(( ( $(date -d "$caducitat" +%s) - $(date +%s) ) / 86400 ))
  printf '%s caduca en %s dies (%s)\n' "$dies" "$dies" "$caducitat"
  (( dies < 5 )) && return 2; (( dies < 15 )) && return 1; return 0
}

L'echo | del principi és imprescindible: sense ell, s_client deixa la connexió oberta esperant entrada i el timeout acaba matant-lo. L'aritmètica de dates és la de 04-06, restant segons d'època.

2. La traça no ha de bloquejar el cicle, així que es llança en segon pla i amb límit:

diagnostic_extra() { { timeout 20 traceroute -n -w1 -q1 "$1" 2>&1 | veloz_log_info_stdin; } & disown; }

S'invoca des d'alerta només en la transició a FALLADA —mai mentre dura— per no llançar una traça cada cinc minuts durant una caiguda de dues hores.

3. S'executa resum sobre els dos fitxers mensuals, es carreguen en dos arrays associatius (04-03) indexats per objectiu i es recorren comparant. La regla de presentació: mostrar la diferència amb signe i marcar només el que empitjora més d'un llindar (per exemple, 0,5 punts de disponibilitat o un 20 % de latència), perquè un informe que ho marca tot no marca res.

Conclusió

monitor-xarxa.sh converteix «la xarxa va bé» en cinc afirmacions mesurables amb el seu llindar, i les comprova des d'un catàleg en fitxer que creix sense tocar codi. Les idees transferibles són quatre. El conveni 0/1/2/3 dels connectors clàssics, que permet que el despatxador no sàpiga res de ping ni de curl i que afegir un tipus sigui escriure una funció. Paral·lelitzar el que espera: dotze objectius en quatre segons en lloc de vuit i mig, amb un temporal per treball i consolidació en ordre fix perquè l'informe sigui comparable. Alertar en les transicions, amb avís de recuperació i finestres que silencien l'avís però mai la mesura. I separar la comprovació puntual de la tendència: la primera alerta, la segona informa, i totes dues surten de la mateixa línia TSV. Pel camí: reintents amb retrocés (05-03), curl -w i /dev/tcp amb timeout (06-04), jq -n per al cos del webhook (06-05), wait -n per limitar la concurrència (05-02), escriptura atòmica de l'estat (08-03) i awk per al resum mensual (06-01).

Queden cinc scripts que funcionen, cadascun amb les seves opcions, la seva configuració i la seva manera d'invocar-se. A 09-05, el tancament del curs, deixen de ser una col·lecció i es converteixen en un producte: veloz-ops, una única ordre amb subordres, configuració unificada, instal·lador idempotent, versionat amb Git, bateria de proves, desplegament a la flota, tornada enrere i documentació.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats