El mòdul 4 va acabar amb una frase incòmoda: està tot muntat, i ningú no ho està mirant. MercadoFresco té una botiga que escala sola, una base de dades replicada, una CDN que serveix el 96 % del trànsit, identitats mínimes, dades xifrades i dos tallafocs d'aplicació. I tanmateix, si divendres a les 19:10 la botiga comença a retornar errors, la primera persona a assabentar-se'n serà un client per Twitter.

Amazon CloudWatch és el servei que resol això. No és «els gràfics d'AWS»: és el repositori central on tots els serveis que has muntat escriuen el que els passa, el lloc on s'emmagatzemen els registres de la teva aplicació, i el motor que decideix quan despertar algú. La Marta el fa servir de reüll des de 02-01 —cada vegada que hem mirat CPUUtilization o hem creat una alarma érem a CloudWatch— però mai no l'hem muntat de debò.

Aquesta lliçó el munta de debò. Al final, MercadoFresco tindrà un tauler que la Marta obre cada matí amb el cafè, mètriques de negoci pròpies, tots els seus registres en un sol lloc consultables amb un llenguatge tipus SQL i —el més important— la certesa comprovada que un avís arriba a un telèfon a les quatre de la matinada.

Avís de cost. CloudWatch és dels serveis d'AWS que més fàcilment es descontrolen, i gairebé mai per les mètriques: pels registres. Un grup de registres amb retenció infinita, o una aplicació amb DEBUG activat en producció, poden costar més que les instàncies EC2 que els generen. Cada secció d'aquesta lliçó n'indica el cost, i hi ha una secció sencera dedicada a això.

Contingut

  1. Què és CloudWatch i què no és
  2. Els tres pilars
  3. Mètriques: espais de noms i dimensions
  4. Resolució estàndard enfront d'alta resolució
  5. Estadístiques: per què la mitjana menteix
  6. Períodes, agregació i retenció
  7. Les mètriques que importen de debò del que ja està muntat
  8. Per què EC2 no publica l'ús de memòria
  9. Publicar mètriques personalitzades amb boto3
  10. PutMetricData per lots i valors estadístics
  11. L'agent unificat de CloudWatch a les instàncies de l'ASG
  12. Embedded Metric Format des de Lambda
  13. Registres: grups, fluxos i retenció
  14. Els registres que MercadoFresco té dispersos
  15. Logs Insights: sintaxi i consultes reals
  16. Respondre a «per què va trigar vuit segons aquella comanda?»
  17. Filtres de mètriques: d'un patró de registre a una alarma
  18. Subscripcions i exportació a S3
  19. Alarmes: llindars, avaluació i dades absents
  20. Alarmes compostes
  21. Detecció d'anomalies
  22. Accions d'alarma
  23. Comprovar que l'avís arriba de debò
  24. El tauler mercadofresco-produccion
  25. ServiceLens i Synthetics
  26. Cost de CloudWatch i com es dispara
  27. Neteja

Què és CloudWatch i què no és

CloudWatch és un servei regional d'observabilitat que fa tres coses: emmagatzemar sèries temporals numèriques (mètriques), emmagatzemar text amb marca de temps (registres), i avaluar condicions sobre això per disparar accions (alarmes).

Allò que no és, i convé fixar-ho des del principi perquè evita molta confusió:

CloudWatch no és Això ho fa Es veu a
Un registre de qui ha cridat l'API d'AWS CloudTrail 05-03
Un rastrejador de peticions entre serveis X-Ray 05-02
Un avaluador del compliment de configuracions AWS Config 05-04
Un bus d'esdeveniments per automatitzar reaccions EventBridge 07-03
Un magatzem analític per a consultes històriques grans Athena / Redshift 05-03, 06-04

La confusió més habitual és la primera. Si el Luis busca «qui ha esborrat el bucket», això no és a CloudWatch: és a CloudTrail. CloudWatch desa el que la teva aplicació i els serveis diuen d'ells mateixos, no qui ha parlat amb l'API d'AWS.

Històricament el bus d'esdeveniments es va dir «CloudWatch Events» i encara veuràs aquest nom en documentació antiga i en algunes respostes de la CLI. Avui és EventBridge i s'estudia a 07-03; aquesta lliçó no el toca.

Els tres pilars

flowchart LR
    subgraph Origenes["Origens"]
        EC2["EC2 / ASG"]
        ALB["ALB"]
        RDS["RDS"]
        LAM["Lambda"]
        CF["CloudFront"]
        APP["Codi de la botiga"]
    end

    EC2 --> M["METRIQUES<br/>series temporals"]
    ALB --> M
    RDS --> M
    LAM --> M
    CF --> M
    APP --> M

    EC2 --> L["REGISTRES<br/>text amb marca de temps"]
    LAM --> L
    APP --> L
    ALB -.->|"a S3, no a Logs"| S3["S3"]

    L -->|"filtre de metriques"| M
    M --> A["ALARMES<br/>llindar + avaluacio"]
    A --> SNS["SNS alertas-mercadofresco"]
    A --> ASG["Politica d'autoescalat"]
    A --> EC2ACT["Accio sobre la instancia"]

Fixa't en la fletxa que va de registres a mètriques: un filtre de mètriques converteix un patró de text en una sèrie numèrica. És el pont que permet alarmar sobre alguna cosa que només apareix en un registre, com ERROR pago rechazado. I fixa't també que l'ALB escriu els registres d'accés a S3, no a CloudWatch Logs: això tindrà conseqüències a la secció de Logs Insights.

Mètriques: espais de noms i dimensions

Una mètrica és una sèrie temporal identificada per tres coses:

  • Espai de noms (namespace): el contenidor. Els d'AWS comencen per AWS/ (AWS/EC2, AWS/ApplicationELB, AWS/RDS, AWS/Lambda, AWS/S3, AWS/CloudFront). El teu no pot començar per AWS/: MercadoFresco fa servir MercadoFresco/Tienda.
  • Nom de la mètrica: CPUUtilization, TargetResponseTime, PedidosConfirmados.
  • Dimensions: fins a 30 parells clau/valor que identifiquen de què és la mesura. InstanceId=i-0abc..., LoadBalancer=app/alb-mercadofresco-tienda/..., DBInstanceIdentifier=mercadofresco-pedidos.

El detall que més despista al principi: cada combinació diferent de dimensions és una mètrica diferent i es factura per separat. Aquestes són tres mètriques, no una:

MercadoFresco/Tienda  PedidosConfirmados  (sin dimensiones)
MercadoFresco/Tienda  PedidosConfirmados  Entorno=produccion
MercadoFresco/Tienda  PedidosConfirmados  Entorno=produccion, Provincia=Madrid

I CloudWatch no agrega automàticament entre elles. Si publiques només la tercera i després consultes la primera, no hi haurà dades. Això té dues conseqüències pràctiques:

  1. Si vols el total i també el desglossament, publica'ls tots dos.
  2. No facis servir mai com a dimensió res de cardinalitat alta. Posar PedidoId com a dimensió crea una mètrica nova per comanda: 900 mètriques a l'hora, 650.000 al mes, a 0,30 USD cadascuna. Això són més de 190.000 USD mensuals per un descuit d'una línia. La identitat d'una comanda va en un registre o en una anotació d'X-Ray (05-02), mai en una dimensió.

Regla de MercadoFresco: les dimensions són categories tancades i petites —entorn, component, tipus de pagament, província— mai identificadors.

Resolució estàndard enfront d'alta resolució

Estàndard Alta resolució
Granularitat mínima 60 segons 1 segon
Com es demana Per defecte StorageResolution=1 en publicar
Períodes d'alarma 60 s i múltiples 10 s i 30 s també
Cost de la mètrica 0,30 USD/mes 0,30 USD/mes (igual)
Cost de l'alarma 0,10 USD/mes 0,30 USD/mes
Retenció de les dades d'1 s 3 hores, després s'agrega

L'alta resolució serveix per a processos que canvien en segons: una cua que s'omple, un pic de latència de 20 segons que a resolució de minut queda diluït en la mitjana. MercadoFresco no la fa servir: els seus problemes es veuen perfectament a un minut, i les alarmes de 10 segons generen soroll nocturn. És una eina de diagnòstic puntual, no un valor per defecte.

Les mètriques bàsiques d'EC2 són de 5 minuts llevat que activis el monitoratge detallat (2,10 USD per instància i mes a eu-west-1, aproximadament), que les baixa a 1 minut. Per a l'ASG de MercadoFresco sí que val la pena: amb dades de 5 minuts, l'escalat reacciona tard al pic dels divendres.

Estadístiques: per què la mitjana menteix

CloudWatch no desa cada punt individual: desa agregats per període. En consultar tries quin agregat vols:

Estadística Què respon Quan fer-la servir
Sum Quant en total? Comptadors: RequestCount, PedidosConfirmados, Errors
Average Quant de mitjana? Utilització: CPUUtilization, CacheHitRate
Minimum / Maximum L'extrem Capacitat: FreeStorageSpace mínim, connexions màximes
SampleCount Quants punts? Diagnòstic de buits
p50, p90, p95, p99 Què experimenta el percentil N? Latència, sempre
TM(5%:95%), TC, WM Mitjana retallada, ignorant extrems Anàlisi fina

L'error clàssic —i el que amaga més incidents— és alarmar sobre l'Average d'una latència. Un minut real de la botiga de MercadoFresco:

Peticions Temps de resposta
950 0,08 s (pàgines a la memòria cau)
40 0,4 s (fitxes de producte)
10 9,5 s (confirmació de comanda)

Average = 0,17 s. Perfecte. Verd. No salta cap alarma.

p99 = 9,4 s. Deu clients per minut estan veient una roda girar durant gairebé deu segons mentre intenten pagar. Són exactament els clients que importen: els que estan comprant.

Regla: per a qualsevol mètrica de latència, alarma sobre p95 o p99, mai sobre Average. La mitjana et diu com va el servidor; el percentil et diu com ho viu el client.

Un matís important: els percentils només estan disponibles si la mètrica es publica amb dades suficients. Les mètriques de l'ALB els admeten de manera nativa. Per a mètriques pròpies, cal publicar valors individuals o fer servir StatisticValues amb compte (ho veurem: els StatisticValues no permeten percentils, perquè ja arriben agregats).

Períodes, agregació i retenció

El període és la finestra d'agregació de la consulta o de l'alarma: 60 s, 300 s, 3600 s… És independent de la freqüència amb què publiques. Si publiques cada 10 segons i consultes amb període 300, CloudWatch agrega 30 punts en un.

La retenció és automàtica, gratuïta i no configurable:

Antiguitat de la dada Resolució conservada
0 – 3 hores 1 segon (només alta resolució)
0 – 15 dies 1 minut
15 – 63 dies 5 minuts
63 – 455 dies 1 hora
Més de 15 mesos S'esborra

Dues conseqüències operatives:

  • No pots consultar amb període 60 un dia de fa dos mesos. Les dades ja estan agregades a 5 minuts. Si necessites comparar el Black Friday de l'any passat al minut, l'has d'haver exportat abans.
  • La Marta exporta cada mes les mètriques de negoci (PedidosConfirmados) a mercadofresco-informes-analitica amb get-metric-data, precisament perquè la Sara pugui comparar campanyes d'anys diferents.

Les mètriques que importen de debò del que ja està muntat

Aquesta és la taula que la Marta s'ha imprès. Dels centenars de mètriques que publiquen els serveis de MercadoFresco, aquestes són les que de debò decideixen alguna cosa:

Servei Mètrica Estadística Què significa Llindar de MercadoFresco
EC2 / ASG CPUUtilization Average Ús de CPU Escalat al 60 % (política cpu-objetivo-60)
EC2 / ASG StatusCheckFailed_Instance Maximum La instància està trencada ≥ 1 durant 2 períodes
EC2 / ASG GroupInServiceInstances Maximum Instàncies actives = 4 → mercadofresco-asg-al-maximo
ALB TargetResponseTime p95 Latència vista pel client > 2 s durant 3 min
ALB HTTPCode_ELB_5XX_Count Sum Errors del balancejador > 10 en 5 min
ALB HTTPCode_Target_5XX_Count Sum Errors de la teva aplicació > 25 en 5 min
ALB HealthyHostCount Minimum Instàncies sanes per grup < 2
ALB UnHealthyHostCount Maximum Instàncies que fallen /salud ≥ 1
ALB RejectedConnectionCount Sum L'ALB rebutja per manca de capacitat > 0
RDS DatabaseConnections Maximum Connexions obertes > 160 de 200
RDS FreeStorageSpace Minimum Disc lliure en bytes < 20 GB
RDS ReadLatency / WriteLatency Average Latència de disc en segons > 0,02 s
RDS CPUUtilization Average CPU de la instància > 80 %
RDS ReplicaLag Maximum Retard de la rèplica de lectura > 30 s
RDS FreeableMemory Minimum Memòria disponible < 500 MB
Lambda Errors Sum Invocacions fallides > 5 en 5 min
Lambda Duration p99 Temps d'execució > 80 % del timeout
Lambda Throttles Sum Invocacions rebutjades per concurrència > 0
Lambda ConcurrentExecutions Maximum Execucions simultànies > 70 % de la quota
Lambda IteratorAge Maximum Retard en orígens de flux > 60 s
S3 BucketSizeBytes Average Mida (diària) Tendència, no alarma
S3 NumberOfObjects Average Objectes (diari) Tendència
S3 4xxErrors / 5xxErrors Sum Requereix mètriques de petició (de pagament) > 1 %
CloudFront CacheHitRate Average % servit des de la vora < 50 % → alarma
CloudFront 5xxErrorRate Average Errors retornats > 1 %
CloudFront OriginLatency p95 El que triga el teu origen > 1 s
WAF BlockedRequests Sum Peticions bloquejades Anomalia

Tres observacions que separen qui entén això de qui copia llindars:

  • HTTPCode_ELB_5XX_Count i HTTPCode_Target_5XX_Count no són el mateix. El primer significa que el balancejador no va poder lliurar la petició a ningú (no hi ha destins sans, temps d'espera exhaurit). El segon significa que la teva aplicació ha respost 500. Confondre'ls fa perdre hores buscant al lloc equivocat.
  • FreeStorageSpace d'RDS està en bytes. El llindar de 20 GB s'escriu 21474836480. Un zero de més i l'alarma no salta mai.
  • Throttles de Lambda amb llindar > 0, no > 10. Un sol estrangulament significa que has tocat el sostre de concurrència i hi ha clients veient errors. No hi ha un «poc de throttling acceptable».

Per què EC2 no publica l'ús de memòria

És la pregunta que fa tothom en arribar aquí, i la resposta explica com funciona el núvol.

L'hipervisor d'AWS veu la màquina virtual des de fora. Pot mesurar CPU consumida, trànsit de xarxa, operacions de disc del volum EBS: tot això passa per la seva capa. Però la memòria lliure és un concepte del sistema operatiu convidat. Només Linux sap quanta RAM hi ha a buff/cache i es podria alliberar, i això l'hipervisor no ho pot saber sense entrar-hi.

El mateix passa amb l'espai lliure del disc: EBS és un dispositiu de blocs, i només el sistema de fitxers de dins sap què està ocupat.

Conseqüència pràctica: per a memòria i disc d'EC2 necessites instal·lar un agent dins de la instància. Es fa més avall. I conseqüència de disseny: per això Lambda, Fargate i RDS —on AWS gestiona el sistema operatiu— sí que publiquen memòria sense que facis res.

Publicar mètriques personalitzades amb boto3

Les mètriques d'infraestructura diuen com va la màquina. Les de negoci diuen com va l'empresa, i són les que de debò detecten incidents. A 04-04 vam veure per què: si arriben 5.000 peticions per segon i PedidosPorHora no es mou, no són clients.

MercadoFresco ja publicava PedidosPorHora. Ara hi afegim les dues que falten:

Mètrica Unitat Estadística útil Per a què
PedidosConfirmados Count Sum Volum de negoci en temps real
TiempoConfirmacionPedido Milliseconds p95, p99 Experiència de compra

El codi, a la botiga:

"""Publicacio de metriques de negoci de MercadoFresco.

S'executa dins de les instancies de l'ASG, que assumeixen el rol
rol-mercadofresco-tienda. Aquest rol te cloudwatch:PutMetricData
restringit a l'espai de noms MercadoFresco/Tienda (04-01).
"""
import time
import boto3
from botocore.config import Config

# Reintents en mode adaptatiu: PutMetricData es idempotent per al
# nostre cas i no volem que una fallada de xarxa tombi una compra.
cw = boto3.client(
    "cloudwatch",
    region_name="eu-west-1",
    config=Config(retries={"max_attempts": 3, "mode": "adaptive"}),
)

ESPAI = "MercadoFresco/Tienda"


def registrar_comanda_confirmada(import_eur, milisegons, metode_pagament, provincia):
    """Publica les metriques d'una comanda acabada de confirmar."""
    dimensions_base = [
        {"Name": "Entorno", "Value": "produccion"},
        {"Name": "Componente", "Value": "tienda"},
    ]

    cw.put_metric_data(
        Namespace=ESPAI,
        MetricData=[
            # 1. Comptador global, sense mes dimensions que les base.
            {
                "MetricName": "PedidosConfirmados",
                "Dimensions": dimensions_base,
                "Value": 1,
                "Unit": "Count",
                "Timestamp": time.time(),
            },
            # 2. Desglossament per metode de pagament: cardinalitat baixa i tancada.
            {
                "MetricName": "PedidosConfirmados",
                "Dimensions": dimensions_base + [
                    {"Name": "MetodoPago", "Value": metode_pagament},
                ],
                "Value": 1,
                "Unit": "Count",
            },
            # 3. Latencia de negoci: el temps que el client ha esperat.
            {
                "MetricName": "TiempoConfirmacionPedido",
                "Dimensions": dimensions_base,
                "Value": milisegons,
                "Unit": "Milliseconds",
            },
            # 4. Import, per detectar comandes anomales.
            {
                "MetricName": "ImportePedido",
                "Dimensions": dimensions_base,
                "Value": import_eur,
                "Unit": "None",
            },
        ],
    )

Detalls que importen i que no són obvis:

  • Timestamp és opcional; si no el poses, CloudWatch fa servir l'hora de recepció. Pots publicar dades de fins a 2 setmanes d'antiguitat i fins a 2 hores en el futur. Això permet recuperar mètriques d'un procés per lots que ha fallat.
  • La unitat importa poc a CloudWatch però molt a les alarmes. Si publiques en Milliseconds i l'alarma espera Seconds, l'alarma no troba dades i es queda en INSUFFICIENT_DATA per sempre. Sigues coherent.
  • Publicar Value: 1 repetidament és correcte. CloudWatch suma els valors del període quan consultes amb Sum. No cal que l'aplicació mantingui un comptador.
  • provincia no es fa servir com a dimensió en aquest exemple. Espanya té 52 províncies: seria acceptable, però multiplicaria per 52 el nombre de mètriques (15,60 USD/mes només per això). La Marta va decidir que aquest desglossament viu als informes de la Sara sobre mercadofresco-informes-analitica, no a CloudWatch.

L'error de rendiment que gairebé ningú no veu venir

El codi anterior fa una crida HTTPS a l'API de CloudWatch dins de la ruta crítica d'una compra. Amb 900 comandes/hora funciona; amb un pic seriós, aquesta crida de 40 ms se suma al temps de resposta que el client percep, i si l'API de CloudWatch s'alenteix, la teva botiga s'alenteix.

Tres solucions, de pitjor a millor:

  1. Embolcallar-ho en un try/except i no fallar mai. Mínim indispensable: una mètrica no ha de tombar una venda.
  2. Acumular en memòria i publicar per lots cada 20 segons des d'un fil a part (secció següent).
  3. Escriure la mètrica al registre en format EMF i deixar que CloudWatch l'extregui (secció de Lambda). Cost zero a la ruta crítica.

PutMetricData per lots i valors estadístics

PutMetricData accepta fins a 1.000 punts per crida (amb un límit d'1 MB de cos). Publicar per lots redueix el cost d'API (0,01 USD per cada 1.000 crides) i treu la latència de la ruta crítica.

"""Publicador per lots: acumula en memoria i buida cada 20 segons."""
import threading
import queue
import boto3

cw = boto3.client("cloudwatch", region_name="eu-west-1")
_cua = queue.Queue(maxsize=10000)


def encuar(nom, valor, unitat="Count", dimensions=None):
    """No bloqueja mai: si la cua es plena, es descarta el punt."""
    try:
        _cua.put_nowait({
            "MetricName": nom,
            "Value": valor,
            "Unit": unitat,
            "Dimensions": dimensions or [
                {"Name": "Entorno", "Value": "produccion"},
                {"Name": "Componente", "Value": "tienda"},
            ],
        })
    except queue.Full:
        pass  # Perdre una metrica es preferible a bloquejar una compra.


def _buidar():
    while True:
        lot = []
        # Espera bloquejant per al primer element; la resta sense esperar.
        lot.append(_cua.get())
        while len(lot) < 1000:
            try:
                lot.append(_cua.get_nowait())
            except queue.Empty:
                break
        try:
            for i in range(0, len(lot), 1000):
                cw.put_metric_data(
                    Namespace="MercadoFresco/Tienda",
                    MetricData=lot[i:i + 1000],
                )
        except Exception as e:  # noqa: BLE001
            print(f"No s'han pogut publicar metriques: {e}")


threading.Thread(target=_buidar, daemon=True).start()

Valors estadístics: 900 punts en un

Si només necessites Sum, Average, Min i Max, pots preagregar-ho tu i enviar un únic punt amb StatisticValues. Una crida en lloc de nou-centes:

cw.put_metric_data(
    Namespace="MercadoFresco/Tienda",
    MetricData=[{
        "MetricName": "TiempoConfirmacionPedido",
        "Dimensions": [{"Name": "Entorno", "Value": "produccion"}],
        "StatisticValues": {
            "SampleCount": 900,     # 900 comandes en l'ultima hora
            "Sum": 1_620_000.0,     # suma de milisegons
            "Minimum": 410.0,
            "Maximum": 9_480.0,
        },
        "Unit": "Milliseconds",
    }],
)

El preu d'aquesta optimització: amb StatisticValues perds els percentils. CloudWatch només rep quatre números; no pot calcular un p99 a partir d'ells. Com que el p95 de TiempoConfirmacionPedido és precisament el que la Marta vol vigilar, MercadoFresco no fa servir StatisticValues per a aquesta mètrica. Sí que la fa servir per a ImportePedido, on el Sum i el Maximum són tot el que interessa.

Una tercera opció, intermèdia, és el camp Values + Counts, que envia valors diferents amb la seva multiplicitat i sí que conserva percentils:

cw.put_metric_data(
    Namespace="MercadoFresco/Tienda",
    MetricData=[{
        "MetricName": "TiempoConfirmacionPedido",
        "Values": [410.0, 520.0, 610.0, 9480.0],
        "Counts": [300.0, 450.0, 140.0, 10.0],   # 900 mostres en total
        "Unit": "Milliseconds",
    }],
)

L'agent unificat de CloudWatch a les instàncies de l'ASG

Per a memòria i disc cal un agent dins de la instància. L'agent unificat de CloudWatch (amazon-cloudwatch-agent) fa dues feines alhora: publica mètriques del sistema i envia fitxers de registre a CloudWatch Logs. Substitueix els antics scripts de Perl i l'awslogs, ja en desús.

  1. Permisos

El rol rol-mercadofresco-tienda necessita això afegit a pol-mercadofresco-tienda:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicarMetricasDelAgente",
      "Effect": "Allow",
      "Action": "cloudwatch:PutMetricData",
      "Resource": "*",
      "Condition": {
        "StringEquals": { "cloudwatch:namespace": "MercadoFresco/Sistema" }
      }
    },
    {
      "Sid": "EscribirRegistros",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogStreams"
      ],
      "Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/mercadofresco/*"
    },
    {
      "Sid": "LeerLaConfiguracionDelAgente",
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": "arn:aws:ssm:eu-west-1:111122223333:parameter/mercadofresco/produccion/cloudwatch-agent"
    }
  ]
}

Fixa't en dues coses. cloudwatch:PutMetricData no admet ARN de recurs —per això "Resource": "*" amb la condició d'espai de noms, exactament el patró que vam estudiar a 04-01—. I la configuració de l'agent es desa a Parameter Store (04-03), que és la manera neta que totes les instàncies de l'ASG en comparteixin la mateixa sense coure-la a l'AMI.

  1. El fitxer de configuració

{
  "agent": {
    "metrics_collection_interval": 60,
    "run_as_user": "cwagent",
    "region": "eu-west-1"
  },
  "metrics": {
    "namespace": "MercadoFresco/Sistema",
    "append_dimensions": {
      "AutoScalingGroupName": "${aws:AutoScalingGroupName}",
      "InstanceId": "${aws:InstanceId}"
    },
    "aggregation_dimensions": [
      ["AutoScalingGroupName"],
      []
    ],
    "metrics_collected": {
      "mem": {
        "measurement": [
          { "name": "mem_used_percent", "rename": "MemoriaUsadaPorcentaje", "unit": "Percent" },
          { "name": "mem_available", "unit": "Bytes" }
        ],
        "metrics_collection_interval": 60
      },
      "disk": {
        "resources": ["/", "/var/log"],
        "measurement": [
          { "name": "used_percent", "rename": "DiscoUsadoPorcentaje", "unit": "Percent" },
          { "name": "inodes_free" }
        ],
        "ignore_file_system_types": ["sysfs", "devtmpfs", "tmpfs", "overlay"],
        "metrics_collection_interval": 300
      },
      "swap": {
        "measurement": ["swap_used_percent"]
      },
      "procstat": [
        {
          "pattern": "gunicorn",
          "measurement": ["cpu_usage", "memory_rss"]
        }
      ]
    }
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/mercadofresco/aplicacion.log",
            "log_group_name": "/mercadofresco/tienda/aplicacion",
            "log_stream_name": "{instance_id}",
            "retention_in_days": 30,
            "timestamp_format": "%Y-%m-%d %H:%M:%S",
            "timezone": "UTC",
            "multi_line_start_pattern": "{timestamp_format}"
          },
          {
            "file_path": "/var/log/nginx/access.log",
            "log_group_name": "/mercadofresco/tienda/nginx-acceso",
            "log_stream_name": "{instance_id}",
            "retention_in_days": 14
          },
          {
            "file_path": "/var/log/nginx/error.log",
            "log_group_name": "/mercadofresco/tienda/nginx-error",
            "log_stream_name": "{instance_id}",
            "retention_in_days": 30
          }
        ]
      }
    }
  }
}

Repàs de les decisions preses allà, perquè cadascuna té el seu motiu:

  • namespace propi MercadoFresco/Sistema, separat de MercadoFresco/Tienda. Mètriques de màquina i mètriques de negoci no es barregen: facilita els permisos i els taulers.
  • append_dimensions amb ${aws:AutoScalingGroupName}: l'agent resol aquestes variables sol, consultant les metadades de la instància. No cal generar un fitxer per instància.
  • aggregation_dimensions amb [] (llista buida) publica també la mètrica agregada de tot el grup. És la que es fa servir al tauler: a la Marta li interessa «memòria mitjana de l'ASG», no la de la instància i-0abc.
  • multi_line_start_pattern: sense això, una traça de Python de 30 línies es converteix en 30 esdeveniments de registre inconnexos i les consultes d'Insights no troben res.
  • retention_in_days a la configuració mateixa: l'agent crea el grup amb retenció finita des del primer dia. És la línia que més diners estalvia de tot el fitxer.
  • procstat vigila el procés gunicorn concret. Si el procés mor i systemd el reinicia en bucle, la CPU de la instància sembla normal però procstat el delata.

  1. Desar la configuració i desplegar-la amb user data

# Desar la configuracio a Parameter Store (04-03)
aws ssm put-parameter \
  --name /mercadofresco/produccion/cloudwatch-agent \
  --type String \
  --tier Standard \
  --value file://cloudwatch-agent.json \
  --overwrite \
  --profile mercadofresco-dev --region eu-west-1

I el fragment que s'afegeix al user data de la plantilla lt-mercadofresco-tienda (02-01):

#!/bin/bash
set -euo pipefail

# Amazon Linux 2023 porta el paquet als seus repositoris.
dnf install -y amazon-cloudwatch-agent

mkdir -p /var/log/mercadofresco

# Arrencar l'agent llegint la configuracio des de Parameter Store.
# El prefix ssm: diu a l'agent que l'argument es un parametre, no un fitxer.
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config \
  -m ec2 \
  -c ssm:/mercadofresco/produccion/cloudwatch-agent \
  -s

# Comprovacio: si l'agent no arrenca, que la instancia falli el health check
# i l'ASG la reemplaci, en lloc de quedar-se cega en silenci.
systemctl is-active amazon-cloudwatch-agent || exit 1

Després d'actualitzar el user data cal crear una nova versió de la plantilla de llançament i fer un instance refresh de l'ASG:

aws ec2 create-launch-template-version \
  --launch-template-name lt-mercadofresco-tienda \
  --source-version '$Latest' \
  --launch-template-data file://datos-plantilla.json \
  --profile mercadofresco-dev

aws autoscaling start-instance-refresh \
  --auto-scaling-group-name asg-mercadofresco-tienda \
  --preferences '{"MinHealthyPercentage": 50, "InstanceWarmup": 180}' \
  --profile mercadofresco-dev

Cost de l'agent: el programari és gratuït; es paga pel que publica. Amb 4 mètriques del sistema agregades a nivell d'ASG més les de cada instància, MercadoFresco publica unes 20 mètriques personalitzades: 6 USD al mes. La verificació importa: si deixes les dimensions per instància sense agregar i l'ASG rota instàncies tot el dia, cada InstanceId nou crea mètriques noves. Són mètriques que deixen de rebre dades —i per tant de facturar-se després del període de facturació—, però embruten els taulers.

Embedded Metric Format des de Lambda

A Lambda, cridar PutMetricData és especialment car: la funció està facturada per mil·lisegon, així que esperar 40 ms l'API de CloudWatch es paga literalment. I si la Lambda s'invoca 50.000 vegades al dia, són 50.000 crides d'API.

L'Embedded Metric Format (EMF) resol això de manera elegant: escrius un JSON amb una estructura especial a stdout, i CloudWatch Logs extreu les mètriques automàticament en ingerir-lo. Cost a la funció: el d'un print.

"""EMF a mercadofresco-generar-miniaturas.

Escriure el JSON a stdout ja n'hi ha prou: CloudWatch Logs detecta el bloc _aws
i publica les metriques a l'espai MercadoFresco/Miniaturas.
"""
import json
import time
import os


def emetre_metriques(milisegons, bytes_origen, clau_s3, correcte):
    document = {
        "_aws": {
            "Timestamp": int(time.time() * 1000),   # en milisegons
            "CloudWatchMetrics": [
                {
                    "Namespace": "MercadoFresco/Miniaturas",
                    "Dimensions": [["Entorno"], ["Entorno", "Formato"]],
                    "Metrics": [
                        {"Name": "TiempoProceso", "Unit": "Milliseconds"},
                        {"Name": "TamanoOrigen", "Unit": "Bytes"},
                        {"Name": "MiniaturasGeneradas", "Unit": "Count"},
                    ],
                }
            ],
        },
        # Dimensions: han d'apareixer tambe com a camps de primer nivell.
        "Entorno": "produccion",
        "Formato": clau_s3.rsplit(".", 1)[-1].lower(),
        # Valors de les metriques.
        "TiempoProceso": milisegons,
        "TamanoOrigen": bytes_origen,
        "MiniaturasGeneradas": 1 if correcte else 0,
        # Propietats: NO es converteixen en metriques; queden al registre
        # i son consultables amb Logs Insights. Aqui va el de cardinalitat alta.
        "claveS3": clau_s3,
        "funcion": os.environ.get("AWS_LAMBDA_FUNCTION_NAME"),
        "peticionId": os.environ.get("_X_AMZN_TRACE_ID", ""),
    }
    print(json.dumps(document))

La clau conceptual és a les dues últimes seccions del document:

Camp Es converteix en mètrica? Consultable amb Insights? Cardinalitat admesa
Llistat a Metrics
Llistat a Dimensions És dimensió Baixa
Qualsevol altre (claveS3) No Qualsevol

És a dir: EMF et permet tenir la clau de l'objecte d'S3 i l'ID de la petició al costat de la mètrica, sense pagar el preu de la cardinalitat. Quan el p99 de TiempoProceso es dispari, una consulta d'Insights et dirà exactament quins fitxers van ser els lents. Això amb PutMetricData és impossible.

Nota pràctica: la biblioteca aws-embedded-metrics d'AWS fa tot això per tu amb un decorador. Aquí s'ha escrit el JSON a mà perquè es vegi l'estructura, que és el que cal entendre.

Cost d'EMF: es paga la ingesta del registre (uns 0,63 USD/GB a eu-west-1) i les mètriques extretes (0,30 USD cadascuna). No es paguen crides d'API. Per a funcions d'alta freqüència surt clarament a compte.

Registres: grups, fluxos i retenció

La jerarquia és simple i convé tenir-la clara:

  • Grup de registres (log group): el contenidor. Aquí es configura la retenció, el xifratge amb KMS i les subscripcions. Exemple: /mercadofresco/tienda/aplicacion.
  • Flux de registres (log stream): una seqüència d'esdeveniments d'una sola font. Una instància, una execució de Lambda. Exemple: i-0abc123def456.
  • Esdeveniment: una línia amb marca de temps i missatge. Màxim 256 KB.
flowchart TD
    G["/mercadofresco/tienda/aplicacion<br/>retencio 30 dies, KMS"]
    G --> S1["i-0abc123<br/>esdeveniments"]
    G --> S2["i-0def456<br/>esdeveniments"]
    G --> S3["i-0ghi789<br/>esdeveniments"]
    G -.->|filtre de metriques| M["MercadoFresco/Tienda<br/>PedidosFallidos"]
    G -.->|subscripcio| F["Lambda / Firehose /<br/>OpenSearch"]
    G -.->|exportacio| B["s3://mercadofresco-registros-web"]

La retenció infinita, l'error més car de CloudWatch

Per defecte, un grup de registres es crea amb retenció Never expire. Ningú no ho canvia. Tres anys després, MercadoFresco tindria 400 GB de registres d'nginx de 2023 costant 0,03 USD/GB/mes —12 USD mensuals per dades que ningú no llegirà mai— i creixent.

I hi ha una cosa pitjor que el cost: una consulta de Logs Insights escaneja el que li demanis, i a 0,0063 USD per GB escanejat, una cerca descuidada sobre tres anys de registres costa més que la cerca mateixa.

La política de MercadoFresco:

Grup de registres Origen Retenció Motiu
/mercadofresco/tienda/aplicacion Agent 30 dies Diagnòstic operatiu
/mercadofresco/tienda/nginx-acceso Agent 14 dies Volum alt, poc valor passada la setmana
/mercadofresco/tienda/nginx-error Agent 30 dies
/aws/lambda/mercadofresco-generar-miniaturas Lambda 14 dies
/aws/lambda/mercadofresco-estado-pedido Lambda 30 dies Toca comandes
/aws/rds/instance/mercadofresco-pedidos/postgresql RDS 7 dies Consultes lentes
aws-waf-logs-mercadofresco WAF (04-05) 30 dies Anàlisi de falsos positius
flowlogs-mercadofresco VPC (03-01) 7 dies Enorme; s'arxiva a S3

I l'ordre que cal executar el mateix dia que es crea un grup:

aws logs put-retention-policy \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --retention-in-days 30 \
  --profile mercadofresco-dev --region eu-west-1

Una auditoria útil que la Marta executa un cop al mes, per trobar els grups que se li han colat sense retenció:

aws logs describe-log-groups \
  --query "logGroups[?retentionInDays==null].[logGroupName,storedBytes]" \
  --output table \
  --profile mercadofresco-dev --region eu-west-1

A 05-04 veurem com AWS Config converteix aquesta auditoria manual en una regla que salta sola.

Classes de registre: Standard enfront d'Infrequent Access

CloudWatch Logs té dues classes d'emmagatzematge:

Standard Infrequent Access
Ingesta ~0,63 USD/GB ~0,32 USD/GB (la meitat)
Logs Insights
Live Tail, filtres de mètriques, alarmes No
Subscripcions Limitades

Per a flowlogs-mercadofresco, que es consulta només quan hi ha una investigació de xarxa, la classe Infrequent Access estalvia la meitat. Per a /mercadofresco/tienda/aplicacion, del qual depenen filtres de mètriques i alarmes, cal quedar-se a Standard.

Els registres que MercadoFresco té dispersos

Aquesta era una de les cinc preguntes obertes del mòdul 4: «els registres de WAF, VPC, ALB i Lambda s'acumulen en cinc llocs sense correlacionar». Vegem el mapa real:

Registre On és És a CloudWatch Logs? Com es consulta
Aplicació de la botiga Agent → Logs Logs Insights
nginx accés/error Agent → Logs Logs Insights
Lambdes Automàtic Logs Insights
WAF (04-05) aws-waf-logs-mercadofresco Logs Insights
VPC Flow Logs (03-01) flowlogs-mercadofresco Logs Insights
PostgreSQL d'RDS Exportat a Logs (cal activar-ho) Logs Insights
Registres d'accés de l'ALB S3 No Athena (05-03)
Registres de CloudFront S3 No (o Logs v2) Athena
Registres d'accés d'S3 S3 No Athena
Crides a l'API d'AWS CloudTrail → S3 Opcional 05-03

L'honestedat importa aquí: CloudWatch Logs no ho centralitza tot. Els registres d'accés de l'ALB i de CloudFront van a S3 per disseny, perquè el seu volum faria prohibitiva la ingesta a Logs. La centralització real de MercadoFresco és a dos nivells:

  • Diagnòstic en calent (últims dies): CloudWatch Logs Insights.
  • Anàlisi històrica i forense: S3 + Athena, que es veu a 05-03.

Activar els registres de PostgreSQL a la instància RDS, que sí que falta:

aws rds modify-db-instance \
  --db-instance-identifier mercadofresco-pedidos \
  --cloudwatch-logs-export-configuration '{"EnableLogTypes":["postgresql","upgrade"]}' \
  --apply-immediately \
  --profile mercadofresco-dev --region eu-west-1

# I al grup de parametres, registrar les consultes de mes d'un segon
aws rds modify-db-parameter-group \
  --db-parameter-group-name pg-mercadofresco-pedidos \
  --parameters "ParameterName=log_min_duration_statement,ParameterValue=1000,ApplyMethod=immediate" \
  --profile mercadofresco-dev --region eu-west-1

Aquest log_min_duration_statement=1000 serà decisiu a la secció de la comanda de vuit segons.

Logs Insights: sintaxi i consultes reals

CloudWatch Logs Insights és un llenguatge de consulta sobre els registres. No és SQL, tot i que s'hi assembla: és una canonada d'ordres separades per |.

Ordre Per a què
fields Triar/crear camps
filter Filtrar esdeveniments
stats Agregar (count, avg, sum, pct, min, max)
sort Ordenar
limit Limitar resultats (per defecte 1.000)
parse Extreure camps de text no estructurat
dedup Eliminar duplicats
display Triar què es mostra

Els camps @timestamp, @message, @logStream i @log existeixen sempre. Si el teu registre és JSON, Insights el descompon sol i pots referir-te a nivel, pedido_id, duracion_ms directament. Aquesta és la raó número u per registrar en JSON des del primer dia.

Consulta 1: errors de la botiga agrupats

fields @timestamp, @message, nivel, pedido_id, duracion_ms
| filter nivel = "ERROR"
| stats count() as errors by bin(5m), tipo_error
| sort errors desc

bin(5m) agrupa per finestres de cinc minuts: és el que converteix una llista d'errors en un gràfic de tendència. En executar-la, Insights ofereix «Visualization» i aquest resultat es pot afegir directament al tauler.

Consulta 2: els registres de WAF (04-05)

fields @timestamp, httpRequest.clientIp as ip, httpRequest.uri as ruta,
       terminatingRuleId as regla, action
| filter action = "BLOCK"
| stats count() as bloquejos by regla, ruta
| sort bloquejos desc
| limit 25

Aquesta és la consulta que a 04-05 decidia el pas de Count a Block. Ara saps exactament on s'executa.

Consulta 3: la Lambda d'estat de comanda

fields @timestamp, @message, @duration, @billedDuration, @maxMemoryUsed
| filter @type = "REPORT"
| stats count() as invocacions,
        avg(@duration) as mitjana_ms,
        pct(@duration, 95) as p95_ms,
        pct(@duration, 99) as p99_ms,
        max(@maxMemoryUsed) / 1000000 as memoria_max_mb
  by bin(1h)

Els camps @duration, @billedDuration, @maxMemoryUsed i @initDuration els genera Lambda automàticament a la línia REPORT de cada invocació. @maxMemoryUsed enfront de la memòria configurada és la manera més directa d'ajustar la mida d'una funció: si en fa servir 80 MB de 512 configurats, estàs pagant de més.

Consulta 4: flow logs de la VPC (03-01)

fields @timestamp, srcAddr, dstAddr, dstPort, action, bytes
| filter action = "REJECT" and dstPort = 5432
| stats count() as intents, sum(bytes) as total by srcAddr
| sort intents desc

Algú intentant arribar al port 5432 i sent rebutjat per sg-mercadofresco-basedatos. Si aquesta IP és interna, és un error de configuració. Si és externa, és un escaneig.

Consulta 5: la més útil de totes, parse sobre registres sense estructura

parse @message /(?<ip>\d+\.\d+\.\d+\.\d+) .* "(?<metode>\w+) (?<ruta>\S+).*" (?<estat>\d{3}) (?<bytes>\d+) (?<temps>[\d.]+)/
| filter estat >= 500
| stats count() as errors by ruta, estat
| sort errors desc

parse amb expressió regular i grups amb nom converteix el registre d'accés d'nginx en camps consultables. És el rescat per a tot el programari que no registra en JSON.

Executar Insights des de la CLI

ID_CONSULTA=$(aws logs start-query \
  --log-group-names /mercadofresco/tienda/aplicacion \
  --start-time $(date -d '2 hours ago' +%s) \
  --end-time $(date +%s) \
  --query-string 'fields @timestamp, @message | filter nivel = "ERROR" | limit 50' \
  --query 'queryId' --output text \
  --profile mercadofresco-dev --region eu-west-1)

sleep 5
aws logs get-query-results --query-id "$ID_CONSULTA" \
  --profile mercadofresco-dev --region eu-west-1

Avís de cost: cada execució cobra per GB escanejats, no per resultats retornats. Acotar el rang temporal és el que decideix la factura: la mateixa consulta sobre 1 hora o sobre 30 dies costa 720 vegades més en el segon cas. Redueix sempre la finestra abans de refinar la consulta.

Respondre a «per què va trigar vuit segons aquella comanda?»

Aquesta és la segona pregunta que va deixar oberta el mòdul 4. Un client escriu: «dijous a la tarda vaig trigar uns vuit segons a tenir la comanda confirmada». La Marta té el seu correu i l'hora aproximada.

Requisit previ: un identificador de correlació que travessi tots els components. Sense ell, això és impossible. MercadoFresco afegeix a nginx una capçalera X-Peticion-Id que l'aplicació propaga a la Lambda i escriu a cada línia de registre.

Pas 1. Confirmar que el problema existeix i acotar-lo.

fields @timestamp, duracion_ms, ruta, peticion_id, cliente_hash
| filter ruta = "/api/pedidos/confirmar" and duracion_ms > 5000
| sort @timestamp desc
| limit 50

Apareixen 34 peticions lentes dijous entre les 18:40 i les 19:20. No era cosa d'un client.

Pas 2. Trobar la petició concreta.

fields @timestamp, peticion_id, duracion_ms, pedido_id
| filter cliente_hash = "a3f8c1e9" and duracion_ms > 5000

En surt una: peticion_id = 7f3a9b2c, duracion_ms = 8140.

Pas 3. Seguir aquest identificador per tots els grups alhora. Insights permet consultar fins a 50 grups de registres en una sola consulta, i aquesta és la resposta real a «els registres són en cinc llocs sense correlacionar»:

aws logs start-query \
  --log-group-names \
      /mercadofresco/tienda/aplicacion \
      /mercadofresco/tienda/nginx-acceso \
      /aws/lambda/mercadofresco-estado-pedido \
      /aws/rds/instance/mercadofresco-pedidos/postgresql \
  --start-time $(date -d '2026-07-30 18:55' +%s) \
  --end-time   $(date -d '2026-07-30 19:05' +%s) \
  --query-string 'fields @timestamp, @log, @message
                  | filter @message like /7f3a9b2c/
                  | sort @timestamp asc' \
  --profile mercadofresco-dev --region eu-west-1

Pas 4. Llegir la cronologia.

Hora Grup Missatge Δ
18:58:12,004 nginx-acceso POST /api/pedidos/confirmar
18:58:12,031 aplicacion inicio confirmacion pedido=48213 +27 ms
18:58:12,088 aplicacion validacion de stock ok +57 ms
18:58:12,140 aplicacion llamada a lambda estado-pedido +52 ms
18:58:12,690 estado-pedido REPORT Duration: 480 ms +550 ms
18:58:12,700 aplicacion insertando lineas de pedido +10 ms
18:58:20,110 postgresql duration: 7402.115 ms statement: SELECT ... +7.410 ms
18:58:20,144 aplicacion pedido confirmado en 8140 ms +34 ms

Trobat. No era la botiga (27 + 57 + 52 + 10 + 34 = 180 ms), ni la Lambda (550 ms): eren 7,4 segons dins de PostgreSQL. Els registres de log_min_duration_statement que vam activar abans són els que han cantat.

Pas 5. Veure quina consulta era.

fields @timestamp, @message
| filter @message like /duration:/ and @message like /7402/

I apareix una consulta que fa un SELECT de preu per cada línia de la comanda, en un bucle, en lloc d'un sol SELECT ... WHERE id IN (...). És el patró N+1: una comanda amb 38 productes executa 39 consultes.

Què acabem de fer i què no. Hem localitzat el problema amb registres correlacionats, i ha funcionat perquè MercadoFresco tenia un identificador de petició propagat a mà. Però fixa't en l'esforç: quatre consultes, una cronologia muntada manualment, i tot recolzat en el fet que algú es va recordar de propagar la capçalera. Existeix una eina dissenyada exactament per a això, que fa el pas 4 automàticament i en un gràfic: és X-Ray, i és la lliçó 05-02. Tornarem a aquesta mateixa comanda de 8,14 segons allà.

Filtres de mètriques: d'un patró de registre a una alarma

No es pot alarmar sobre un registre; sí sobre una mètrica. Un filtre de mètriques és la conversió.

aws logs put-metric-filter \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --filter-name filtro-pedidos-fallidos \
  --filter-pattern '{ $.nivel = "ERROR" && $.tipo_error = "PAGO_RECHAZADO" }' \
  --metric-transformations \
      metricName=PedidosFallidos,\
metricNamespace=MercadoFresco/Tienda,\
metricValue=1,\
defaultValue=0 \
  --profile mercadofresco-dev --region eu-west-1

Tres detalls de camp:

  • La sintaxi de --filter-pattern no és la d'Insights. Per a registres JSON fes servir { $.camp = "valor" } amb &&, ||, =, !=, >, <. Per a text pla, cometes i termes: "ERROR" -"ERROR de prueba".
  • defaultValue=0 és imprescindible. Sense ell, quan no hi ha errors no es publica cap dada, l'alarma es queda en INSUFFICIENT_DATA i la mètrica té buits que trenquen la detecció d'anomalies. Amb defaultValue=0 la sèrie és contínua.
  • Els filtres només s'apliquen a esdeveniments nous. No hi ha efecte retroactiu sobre el ja ingerit.

També es pot extreure un valor numèric del registre en lloc de comptar:

aws logs put-metric-filter \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --filter-name filtro-duracion-checkout \
  --filter-pattern '{ $.ruta = "/api/pedidos/confirmar" && $.duracion_ms > 0 }' \
  --metric-transformations \
      metricName=DuracionCheckout,\
metricNamespace=MercadoFresco/Tienda,\
metricValue='$.duracion_ms',\
unit=Milliseconds \
  --profile mercadofresco-dev --region eu-west-1

I provar el patró abans de crear-lo, contra esdeveniments reals, cosa que evita el clàssic filtre que no coincideix mai:

aws logs test-metric-filter \
  --filter-pattern '{ $.nivel = "ERROR" && $.tipo_error = "PAGO_RECHAZADO" }' \
  --log-event-messages \
      '{"nivel":"ERROR","tipo_error":"PAGO_RECHAZADO","pedido_id":48213}' \
      '{"nivel":"INFO","mensaje":"pedido ok"}' \
  --profile mercadofresco-dev --region eu-west-1

Subscripcions i exportació a S3

Dues maneres de treure registres de CloudWatch, amb propòsits diferents:

Filtre de subscripció Exportació a S3
Latència Temps real (segons) Per lots, fins a 12 h
Destins Lambda, Firehose, OpenSearch, Kinesis Només S3
Ús típic Processar, reenviar, alertar Arxivar barat, Athena
Cost Del destí Només S3
Límit 2 filtres per grup de registres

Subscripció en temps real cap a una Lambda que reenvia a Slack el que és crític:

aws logs put-subscription-filter \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --filter-name suscripcion-criticos \
  --filter-pattern '{ $.nivel = "CRITICAL" }' \
  --destination-arn arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-reenviar-criticos \
  --profile mercadofresco-dev --region eu-west-1

Exportació a S3 per a arxiu barat dels flow logs, que es miraran un cop l'any:

aws logs create-export-task \
  --task-name export-flowlogs-2026-07 \
  --log-group-name flowlogs-mercadofresco \
  --from $(date -d '2026-07-01' +%s)000 \
  --to   $(date -d '2026-08-01' +%s)000 \
  --destination mercadofresco-registros-web \
  --destination-prefix flowlogs/2026/07 \
  --profile mercadofresco-dev --region eu-west-1

El bucket necessita una política que permeti escriure a logs.eu-west-1.amazonaws.com. I compte: només pot haver-hi una tasca d'exportació activa per compte alhora, i no és incremental. Per a arxivat continu, l'opció moderna és una subscripció cap a Data Firehose amb destí S3.

Alarmes: llindars, avaluació i dades absents

Una alarma té tres estats:

Estat Significa
OK La condició no es compleix. Tot bé.
ALARM La condició es compleix.
INSUFFICIENT_DATA No hi ha dades suficients per decidir.

I quatre paràmetres que gairebé ningú no configura bé a la primera:

  • --period: la finestra d'agregació. 60, 300…
  • --evaluation-periods: quants períodes es miren.
  • --datapoints-to-alarm: quants d'aquests han d'incomplir. Si s'omet, són tots.
  • --treat-missing-data: què fer amb els buits.

La combinació evaluation-periods + datapoints-to-alarm s'anomena «M de N» i és l'eina contra el soroll. Compara:

Configuració Comportament Quan
1 d'1, període 60 Salta al primer minut dolent Només per al que és binari i greu
3 de 3, període 60 Tres minuts seguits dolents Sostingut, triga 3 min
2 de 3, període 60 2 minuts dolents dels últims 3 Equilibri recomanat
5 de 5, període 300 25 minuts Tendències lentes: disc

Amb «2 de 3», un pic d'un sol minut —un desplegament, un garbage collector, una còpia de seguretat— no desperta ningú, però un problema real que oscil·la sí que es detecta. La configuració «3 de 3» es perd exactament aquests problemes intermitents.

I --treat-missing-data, quatre opcions:

Valor Comportament Quan fer-lo servir
missing (defecte) El buit no compta Gairebé mai; genera confusió
notBreaching El buit es tracta com a OK Mètriques amb trànsit intermitent
breaching El buit es tracta com a ALARM Quan l'absència de dades ÉS el problema
ignore L'estat no canvia Mantenir l'últim estat conegut

El cas que ho explica tot: l'alarma sobre PedidosConfirmados. Si la botiga cau del tot, deixa de publicar-se la mètrica. Amb notBreaching, l'alarma es queda en OK mentre la botiga està morta. És la fallada silenciosa més habitual de CloudWatch, i per això aquesta alarma concreta porta --treat-missing-data breaching.

Les alarmes que MercadoFresco crea en aquesta lliçó:

# 1. Latencia real vista pel client: p95, no mitjana.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-alb-latencia-alta \
  --alarm-description "El p95 de resposta de l'ALB supera 2 s" \
  --namespace AWS/ApplicationELB --metric-name TargetResponseTime \
  --dimensions Name=LoadBalancer,Value=app/alb-mercadofresco-tienda/50dc6c495c0c9188 \
  --extended-statistic p95 \
  --period 60 --evaluation-periods 3 --datapoints-to-alarm 2 \
  --threshold 2 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --ok-actions      arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 2. Destins sans: si baixa de 2, hem perdut la tolerancia a fallades d'AZ.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-alb-destinos-sanos \
  --namespace AWS/ApplicationELB --metric-name HealthyHostCount \
  --dimensions Name=LoadBalancer,Value=app/alb-mercadofresco-tienda/50dc6c495c0c9188 \
               Name=TargetGroup,Value=targetgroup/tg-mercadofresco-tienda/73e2d6bc24d8a067 \
  --statistic Minimum --period 60 --evaluation-periods 2 --datapoints-to-alarm 2 \
  --threshold 2 --comparison-operator LessThanThreshold \
  --treat-missing-data breaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 3. Connexions d'RDS: 160 de les 200 del grup de parametres.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-rds-conexiones-altas \
  --namespace AWS/RDS --metric-name DatabaseConnections \
  --dimensions Name=DBInstanceIdentifier,Value=mercadofresco-pedidos \
  --statistic Maximum --period 60 --evaluation-periods 3 --datapoints-to-alarm 2 \
  --threshold 160 --comparison-operator GreaterThanThreshold \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 4. Espai lliure d'RDS: en BYTES. 20 GiB = 21474836480.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-rds-disco-bajo \
  --namespace AWS/RDS --metric-name FreeStorageSpace \
  --dimensions Name=DBInstanceIdentifier,Value=mercadofresco-pedidos \
  --statistic Minimum --period 300 --evaluation-periods 2 \
  --threshold 21474836480 --comparison-operator LessThanThreshold \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 5. Estrangulament de Lambda: llindar 0, sense tolerancia.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-lambda-estrangulada \
  --namespace AWS/Lambda --metric-name Throttles \
  --dimensions Name=FunctionName,Value=mercadofresco-estado-pedido \
  --statistic Sum --period 60 --evaluation-periods 1 \
  --threshold 0 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 6. Memoria de les instancies, gracies a l'agent instal-lat abans.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-tienda-memoria-alta \
  --namespace MercadoFresco/Sistema --metric-name MemoriaUsadaPorcentaje \
  --dimensions Name=AutoScalingGroupName,Value=asg-mercadofresco-tienda \
  --statistic Average --period 300 --evaluation-periods 3 --datapoints-to-alarm 2 \
  --threshold 85 --comparison-operator GreaterThanThreshold \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 7. Comandes fallides, sobre la metrica del filtre de registres.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-pedidos-fallidos \
  --namespace MercadoFresco/Tienda --metric-name PedidosFallidos \
  --statistic Sum --period 300 --evaluation-periods 2 --datapoints-to-alarm 2 \
  --threshold 10 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 8. La mes important: la botiga ha deixat de vendre.
#    treat-missing-data BREACHING perque l'absencia de dada ES l'incident.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-sin-pedidos \
  --alarm-description "No hi ha comandes confirmades: la botiga pot estar caiguda" \
  --namespace MercadoFresco/Tienda --metric-name PedidosConfirmados \
  --dimensions Name=Entorno,Value=produccion Name=Componente,Value=tienda \
  --statistic Sum --period 900 --evaluation-periods 1 \
  --threshold 1 --comparison-operator LessThanThreshold \
  --treat-missing-data breaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

Nota sobre --ok-actions a l'alarma 1: enviar també la recuperació. Sense ella, algú rep un avís de matinada i no té manera de saber si continua passant. Amb --ok-actions, el segon missatge diu «ja està bé». És un detall petit que canvia molt l'experiència d'estar de guàrdia.

I una advertència sobre l'alarma 8: PedidosConfirmados < 1 en 15 minuts és correcte per a l'horari comercial, però a les 4 del matí un diumenge pot ser perfectament normal que no hi hagi comandes. Aquesta alarma genera falsos positius nocturns. La manera correcta de resoldre-ho no és pujar el període: és la detecció d'anomalies, dues seccions més avall.

Alarmes compostes

Una alarma composta s'avalua sobre altres alarmes, amb AND, OR i NOT. Serveix per a dues coses diferents:

  1. Reduir soroll: avisar només si diversos senyals coincideixen.
  2. Suprimir alarmes filles durant un manteniment conegut.

El problema real de MercadoFresco: quan la base de dades se satura un divendres, salten alhora mercadofresco-rds-conexiones-altas, mercadofresco-alb-latencia-alta i mercadofresco-pedidos-fallidos. Tres SMS a les 19:15 descrivint el mateix incident.

aws cloudwatch put-composite-alarm \
  --alarm-name mercadofresco-tienda-degradada \
  --alarm-description "La botiga esta degradada: latencia alta I errors de comanda" \
  --alarm-rule "ALARM(mercadofresco-alb-latencia-alta) AND (ALARM(mercadofresco-pedidos-fallidos) OR ALARM(mercadofresco-rds-conexiones-altas))" \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --actions-enabled \
  --profile mercadofresco-dev --region eu-west-1

I ara el pas que la majoria oblida: treure l'acció d'SNS de les alarmes filles. Si no, continues rebent quatre avisos en lloc de tres.

aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-alb-latencia-alta \
  --namespace AWS/ApplicationELB --metric-name TargetResponseTime \
  --dimensions Name=LoadBalancer,Value=app/alb-mercadofresco-tienda/50dc6c495c0c9188 \
  --extended-statistic p95 --period 60 --evaluation-periods 3 --datapoints-to-alarm 2 \
  --threshold 2 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions "" \
  --profile mercadofresco-dev --region eu-west-1

L'alarma filla continua existint, continua canviant d'estat i continua veient-se al tauler: simplement ja no notifica. L'avís el dona la composta, que conté el diagnòstic.

Supressió durant manteniment: el paràmetre --actions-suppressor permet indicar una altra alarma —per exemple, una que la Marta posa en ALARM a mà durant un desplegament— que silencia la composta mentre estigui activa.

Cost: 0,50 USD per alarma composta i mes. Sol sortir a compte només per deixar de despertar algú tres vegades.

Detecció d'anomalies

En lloc d'un llindar fix, CloudWatch entrena un model amb fins a dues setmanes d'història i aprèn el patró: els pics dels divendres, les valls de la matinada, el cicle setmanal. L'alarma salta quan el valor surt de la banda esperada.

És exactament el que resol el fals positiu nocturn de mercadofresco-sin-pedidos: a les 4 de la matinada el model espera 3 comandes, i 0 és anòmal; a les 19:00 n'espera 900, i 400 també ho és. Un llindar fix no pot expressar això.

# 1. Crear el detector, que comenca a entrenar.
aws cloudwatch put-anomaly-detector \
  --namespace MercadoFresco/Tienda \
  --metric-name PedidosConfirmados \
  --dimensions Name=Entorno,Value=produccion Name=Componente,Value=tienda \
  --stat Sum \
  --profile mercadofresco-dev --region eu-west-1

# 2. L'alarma sobre la banda, amb expressio de metrica.
aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-pedidos-anomalos \
  --alarm-description "Les comandes surten de la banda esperada per a aquesta hora" \
  --comparison-operator LessThanLowerThreshold \
  --evaluation-periods 2 --datapoints-to-alarm 2 \
  --threshold-metric-id ad1 \
  --treat-missing-data breaching \
  --metrics '[
    {
      "Id": "m1",
      "MetricStat": {
        "Metric": {
          "Namespace": "MercadoFresco/Tienda",
          "MetricName": "PedidosConfirmados",
          "Dimensions": [
            {"Name": "Entorno", "Value": "produccion"},
            {"Name": "Componente", "Value": "tienda"}
          ]
        },
        "Period": 300,
        "Stat": "Sum"
      },
      "ReturnData": true
    },
    {
      "Id": "ad1",
      "Expression": "ANOMALY_DETECTION_BAND(m1, 2)",
      "Label": "PedidosConfirmados (banda esperada)",
      "ReturnData": true
    }
  ]' \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

Detalls clau:

  • LessThanLowerThreshold: només avisa si hi ha menys comandes de les esperades. Que n'hi hagi més és una bona notícia, no una alarma. També existeixen GreaterThanUpperThreshold i LessThanLowerOrGreaterThanUpperThreshold.
  • El 2 d'ANOMALY_DETECTION_BAND(m1, 2) és el nombre de desviacions. Més alt = banda més ampla = menys avisos. Comença a 2 i ajusta amb dades reals.
  • Necessita història. Els primers dies la banda és enorme i l'alarma no serveix de res. Cal crear-la i esperar.
  • Es pot excloure un període de l'entrenament amb --configuration i ExcludedTimeRanges: si hi ha hagut un incident de 6 hores, no vols que el model aprengui que això és normal.

Cost: 0,30 USD per mètrica analitzada i mes, més 0,30 USD per alarma d'anomalia.

I una advertència honesta: la detecció d'anomalies no és màgia. Detecta desviacions del patró històric, no problemes. Una degradació lenta i constant durant tres setmanes es converteix en el nou «normal» i deixa d'avisar. Complementa els llindars fixos; no els substitueix.

Accions d'alarma

Acció ARN / forma Ús a MercadoFresco
Notificar per SNS arn:aws:sns:...:alertas-mercadofresco Totes les alarmes
Autoescalat ARN de política d'escalat cpu-objetivo-60 de l'ASG
Acció sobre EC2 arn:aws:automate:eu-west-1:ec2:reboot recover, stop, terminate, reboot
Acció de Systems Manager ARN d'OpsItem o incident Crear tiquets automàtics
Cap Alarmes filles d'una composta

Les accions automàtiques d'EC2 són útils però perilloses. arn:aws:automate:eu-west-1:ec2:recover sobre StatusCheckFailed_System migra la instància a un altre amfitrió quan falla el maquinari subjacent, conservant IP i volums: això és clarament bo. En canvi arn:aws:automate:eu-west-1:ec2:reboot sobre memòria alta és una mala idea: reinicies en bucle una instància amb una fuita de memòria i amagues el problema en lloc d'arreglar-lo.

Per a les instàncies de l'ASG, a més, l'acció correcta gairebé mai és reiniciar: és deixar que fallin la comprovació de salut /salud i que el mateix ASG les reemplaci, que és el que vam muntar a 02-01 i 03-03.

Comprovar que l'avís arriba de debò

Aquí hi ha la primera pregunta oberta del mòdul 4, i la més important d'aquesta lliçó: ningú no ha comprovat que un avís d'SNS arribi a un telèfon a les quatre de la matinada.

Una alarma que dispara cap a un tema d'SNS sense subscriptors confirmats és pitjor que no tenir alarma: dona una falsa sensació de cobertura.

Pas 1. Veure qui està subscrit de debò.

aws sns list-subscriptions-by-topic \
  --topic-arn arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --query 'Subscriptions[].[Protocol,Endpoint,SubscriptionArn]' \
  --output table \
  --profile mercadofresco-dev --region eu-west-1

Si a la columna SubscriptionArn apareix PendingConfirmation, aquesta subscripció no rep res. És la troballa més freqüent quan es fa aquesta comprovació per primera vegada: algú va crear la subscripció fa mesos, el correu de confirmació va anar a la carpeta de correu brossa, i ningú no ho va saber.

Pas 2. Subscriure els destins que falten.

# Correu de l'equip (cal confirmar l'enllac del missatge rebut)
aws sns subscribe \
  --topic-arn arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --protocol email --notification-endpoint [email protected] \
  --profile mercadofresco-dev --region eu-west-1

# SMS al telefon de guardia: es confirma sol, no hi ha enllac que calgui premer
aws sns subscribe \
  --topic-arn arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --protocol sms --notification-endpoint +34600111222 \
  --profile mercadofresco-dev --region eu-west-1

Nota sobre SMS. Des de fa uns anys, enviar SMS amb SNS a moltes regions exigeix sortir del «sandbox» d'SMS i, per a números espanyols, registrar remitent i cas d'ús. Si el teu SMS de prova no arriba, revisa l'estat del sandbox abans de donar per trencada l'alarma. El detall complet d'SNS —temes FIFO, polítiques de lliurament, filtres de subscripció— és la lliçó 07-02.

Pas 3. El simulacre. Forçar l'estat de l'alarma.

Aquesta és l'ordre que tanca la pregunta del mòdul 4:

aws cloudwatch set-alarm-state \
  --alarm-name mercadofresco-alb-latencia-alta \
  --state-value ALARM \
  --state-reason "SIMULACRE $(date -u +%FT%TZ): comprovacio trimestral d'avisos" \
  --profile mercadofresco-dev --region eu-west-1

Això no toca la mètrica: força l'estat de l'alarma i dispara les seves accions de debò. L'SMS i el correu surten. És l'única manera honesta de saber que el canal funciona.

Després es torna a la realitat:

aws cloudwatch set-alarm-state \
  --alarm-name mercadofresco-alb-latencia-alta \
  --state-value OK \
  --state-reason "Fi del simulacre" \
  --profile mercadofresco-dev --region eu-west-1

Al següent període d'avaluació, CloudWatch recalcula l'estat real amb les dades de la mètrica i el corregeix sol si calgués.

Pas 4. El protocol que adopta MercadoFresco.

Quan Què es comprova Qui
Cada trimestre Simulacre amb set-alarm-state sobre 3 alarmes crítiques Marta
Cada trimestre Que no hi hagi subscripcions en PendingConfirmation Marta
En incorporar algú Se'l subscriu i es fa un simulacre amb el seu telèfon Marta
Quan algú marxa S'elimina la seva subscripció Marta
Després de cada incident Va saltar l'alarma correcta? A temps? Hi havia algú? Post-mortem

I una comprovació més, que sol descobrir el problema real: fer el simulacre a les 4 de la matinada de debò, una vegada. La Marta ho va fer. Va descobrir que el mòbil de guàrdia estava en «no molestar» i que els SMS de números curts no trencaven el silenci. La solució no va ser d'AWS: va ser configurar una excepció al telèfon. La cadena d'avís inclou el telèfon, i el telèfon també s'ha de provar.

El tauler mercadofresco-produccion

Un tauler de CloudWatch és un document JSON amb una quadrícula de 24 columnes d'ample. Els ginys es posicionen amb x, y, width, height.

El criteri amb què la Marta l'ha dissenyat —i que és el que cal copiar, més que el JSON— és: a dalt allò que respon «va bé el negoci?», al mig «quin component falla?», a baix el detall. Quan sona el telèfon, es mira de dalt a baix i en 30 segons se sap on anar.

{
  "start": "-PT3H",
  "periodOverride": "auto",
  "widgets": [
    {
      "type": "text",
      "x": 0, "y": 0, "width": 24, "height": 1,
      "properties": {
        "markdown": "# MercadoFresco - Produccio (eu-west-1) | Guardia: [email protected]"
      }
    },
    {
      "type": "metric",
      "x": 0, "y": 1, "width": 6, "height": 4,
      "properties": {
        "title": "Comandes confirmades (ultima hora)",
        "view": "singleValue",
        "region": "eu-west-1",
        "sparkline": true,
        "stat": "Sum",
        "period": 3600,
        "metrics": [
          ["MercadoFresco/Tienda", "PedidosConfirmados",
           "Entorno", "produccion", "Componente", "tienda"]
        ]
      }
    },
    {
      "type": "metric",
      "x": 6, "y": 1, "width": 6, "height": 4,
      "properties": {
        "title": "Temps de confirmacio p95 (ms)",
        "view": "singleValue",
        "region": "eu-west-1",
        "sparkline": true,
        "stat": "p95",
        "period": 300,
        "metrics": [
          ["MercadoFresco/Tienda", "TiempoConfirmacionPedido",
           "Entorno", "produccion", "Componente", "tienda"]
        ]
      }
    },
    {
      "type": "alarm",
      "x": 12, "y": 1, "width": 12, "height": 4,
      "properties": {
        "title": "Estat de les alarmes critiques",
        "alarms": [
          "arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-tienda-degradada",
          "arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-alb-latencia-alta",
          "arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-alb-destinos-sanos",
          "arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-rds-conexiones-altas",
          "arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-pedidos-anomalos"
        ]
      }
    },
    {
      "type": "metric",
      "x": 0, "y": 5, "width": 12, "height": 6,
      "properties": {
        "title": "ALB - latencia i errors",
        "view": "timeSeries",
        "stacked": false,
        "region": "eu-west-1",
        "period": 60,
        "yAxis": {
          "left":  { "label": "segons", "showUnits": false },
          "right": { "label": "errors",  "showUnits": false }
        },
        "metrics": [
          ["AWS/ApplicationELB", "TargetResponseTime",
           "LoadBalancer", "app/alb-mercadofresco-tienda/50dc6c495c0c9188",
           { "stat": "p50", "label": "p50" }],
          ["...", { "stat": "p95", "label": "p95" }],
          ["...", { "stat": "p99", "label": "p99" }],
          ["AWS/ApplicationELB", "HTTPCode_Target_5XX_Count",
           "LoadBalancer", "app/alb-mercadofresco-tienda/50dc6c495c0c9188",
           { "stat": "Sum", "yAxis": "right", "label": "5XX aplicacio", "color": "#d62728" }],
          ["AWS/ApplicationELB", "HTTPCode_ELB_5XX_Count",
           "LoadBalancer", "app/alb-mercadofresco-tienda/50dc6c495c0c9188",
           { "stat": "Sum", "yAxis": "right", "label": "5XX balancejador", "color": "#ff7f0e" }]
        ],
        "annotations": {
          "horizontal": [
            { "label": "Objectiu p95", "value": 2, "color": "#d62728", "fill": "above" }
          ]
        }
      }
    },
    {
      "type": "metric",
      "x": 12, "y": 5, "width": 12, "height": 6,
      "properties": {
        "title": "Base de dades - mercadofresco-pedidos",
        "view": "timeSeries",
        "region": "eu-west-1",
        "period": 60,
        "metrics": [
          ["AWS/RDS", "DatabaseConnections",
           "DBInstanceIdentifier", "mercadofresco-pedidos",
           { "stat": "Maximum", "label": "Connexions" }],
          ["AWS/RDS", "CPUUtilization",
           "DBInstanceIdentifier", "mercadofresco-pedidos",
           { "stat": "Average", "label": "CPU %", "yAxis": "right" }],
          ["AWS/RDS", "ReplicaLag",
           "DBInstanceIdentifier", "mercadofresco-pedidos-lectura",
           { "stat": "Maximum", "label": "Retard replica (s)", "yAxis": "right" }]
        ],
        "annotations": {
          "horizontal": [
            { "label": "Limit de connexions", "value": 200, "color": "#d62728" },
            { "label": "Alarma", "value": 160, "color": "#ff7f0e" }
          ]
        }
      }
    },
    {
      "type": "metric",
      "x": 0, "y": 11, "width": 8, "height": 6,
      "properties": {
        "title": "ASG i sistema",
        "view": "timeSeries",
        "region": "eu-west-1",
        "period": 60,
        "metrics": [
          ["AWS/EC2", "CPUUtilization",
           "AutoScalingGroupName", "asg-mercadofresco-tienda",
           { "stat": "Average", "label": "CPU mitjana %" }],
          ["MercadoFresco/Sistema", "MemoriaUsadaPorcentaje",
           "AutoScalingGroupName", "asg-mercadofresco-tienda",
           { "stat": "Average", "label": "Memoria %" }],
          ["AWS/AutoScaling", "GroupInServiceInstances",
           "AutoScalingGroupName", "asg-mercadofresco-tienda",
           { "stat": "Maximum", "label": "Instancies", "yAxis": "right" }]
        ],
        "annotations": {
          "horizontal": [
            { "label": "Maxim de l'ASG", "value": 4, "color": "#d62728", "yAxis": "right" }
          ]
        }
      }
    },
    {
      "type": "metric",
      "x": 8, "y": 11, "width": 8, "height": 6,
      "properties": {
        "title": "Lambda",
        "view": "timeSeries",
        "region": "eu-west-1",
        "period": 60,
        "metrics": [
          ["AWS/Lambda", "Duration", "FunctionName", "mercadofresco-estado-pedido",
           { "stat": "p99", "label": "estado-pedido p99 (ms)" }],
          ["AWS/Lambda", "Errors", "FunctionName", "mercadofresco-estado-pedido",
           { "stat": "Sum", "label": "Errors", "yAxis": "right", "color": "#d62728" }],
          ["AWS/Lambda", "Throttles", "FunctionName", "mercadofresco-estado-pedido",
           { "stat": "Sum", "label": "Estrangulaments", "yAxis": "right", "color": "#ff7f0e" }],
          ["AWS/Lambda", "Errors", "FunctionName", "mercadofresco-generar-miniaturas",
           { "stat": "Sum", "label": "Miniatures: errors", "yAxis": "right" }]
        ]
      }
    },
    {
      "type": "metric",
      "x": 16, "y": 11, "width": 8, "height": 6,
      "properties": {
        "title": "Vora: CloudFront i WAF",
        "view": "timeSeries",
        "region": "us-east-1",
        "period": 300,
        "metrics": [
          ["AWS/CloudFront", "CacheHitRate", "DistributionId", "E2QWERTY123ABC",
           "Region", "Global", { "stat": "Average", "label": "Encerts de memoria cau %" }],
          ["AWS/WAFV2", "BlockedRequests", "WebACL", "waf-mercadofresco-cdn",
           "Rule", "ALL", "Region", "Global",
           { "stat": "Sum", "label": "Bloquejos WAF", "yAxis": "right" }]
        ]
      }
    },
    {
      "type": "log",
      "x": 0, "y": 17, "width": 24, "height": 7,
      "properties": {
        "title": "Ultims errors de la botiga",
        "region": "eu-west-1",
        "view": "table",
        "query": "SOURCE '/mercadofresco/tienda/aplicacion'\n| fields @timestamp, nivel, tipo_error, ruta, pedido_id, duracion_ms\n| filter nivel = \"ERROR\"\n| sort @timestamp desc\n| limit 20"
      }
    }
  ]
}

I la creació:

aws cloudwatch put-dashboard \
  --dashboard-name mercadofresco-produccion \
  --dashboard-body file://panel-produccion.json \
  --profile mercadofresco-dev --region eu-west-1

Detalls del JSON que mereixen explicació:

  • "start": "-PT3H" fixa la finestra per defecte en les últimes 3 hores. Sintaxi ISO 8601 de durada: -PT1H, -P1D, -P7D.
  • ["...", { "stat": "p95" }] repeteix la mètrica anterior canviant només l'estadística. Estalvia repetir l'ARN llarg tres vegades.
  • El giny de la vora porta "region": "us-east-1". Les mètriques de CloudFront i de la Web ACL de CloudFront viuen allà, el mateix parany de 03-04 i 04-05. Un tauler pot barrejar regions giny a giny, i això el converteix en l'únic lloc on MercadoFresco ho veu tot junt.
  • Les annotations horitzontals dibuixen la línia del llindar. Veure el gràfic acostar-se a la línia vermella abans que salti l'alarma és la meitat del valor d'un tauler.
  • El giny log executa una consulta d'Insights cada vegada que es carrega el tauler. Compte: això costa diners cada vegada, i si el tauler està en una pantalla de l'oficina refrescant cada minut, són 1.440 consultes al dia. La Marta li va posar limit 20 i una finestra curta per aquesta raó.

Cost dels taulers: els 3 primers són gratuïts; a partir d'aquí, 3 USD per tauler i mes. MercadoFresco en té dos: mercadofresco-produccion i mercadofresco-negocio (el de la Sara, amb comandes i imports). Cost: 0 USD.

ServiceLens i Synthetics

Dues funcions de CloudWatch que s'esmenten aquí i s'aprofiten a 05-02:

ServiceLens uneix mètriques, registres i traces d'X-Ray en un mapa de serveis únic. Necessita que X-Ray estigui actiu, així que es veu a la lliçó següent.

CloudWatch Synthetics executa «canaris»: petits scripts que es comporten com un client, des de fora, cada N minuts. És monitoratge sintètic: no espera que hi hagi un client real per detectar que la botiga està trencada. Amb 900 comandes/hora això no sembla important, però a les 3 de la matinada un diumenge poden passar dues hores sense una sola compra, i aquest és exactament el forat pel qual s'escola una caiguda.

El canari de MercadoFresco, canario-mercadofresco-compra, s'executa cada 5 minuts i fa el recorregut complet: portada, cercar «tomàquet», obrir la fitxa, afegir a la cistella, arribar al formulari de pagament (sense pagar). Si algun pas falla, dispara alertas-mercadofresco.

aws synthetics create-canary \
  --name canario-mercadofresco-compra \
  --artifact-s3-location s3://mercadofresco-registros-web/canarios/ \
  --execution-role-arn arn:aws:iam::111122223333:role/rol-canario-mercadofresco \
  --runtime-version syn-nodejs-puppeteer-9.0 \
  --schedule Expression="rate(5 minutes)" \
  --code S3Bucket=mercadofresco-registros-web,S3Key=canarios/compra.zip,Handler=compra.handler \
  --run-config MemoryInMB=1024,TimeoutInSeconds=120,ActiveTracing=true \
  --profile mercadofresco-dev --region eu-west-1

Dues advertències pràctiques:

  • Un canari que compra de debò genera comandes de debò. El de MercadoFresco s'atura abans de confirmar, i l'usuari de prova està marcat perquè la Sara l'exclogui dels seus informes.
  • Cost: 0,0012 USD per execució. Cada 5 minuts són 8.640 execucions al mes: 10,37 USD, més les captures de pantalla a S3 i les traces. No és menyspreable; és la partida més cara d'aquesta lliçó després dels registres.

Cost de CloudWatch i com es dispara

Preus de referència a eu-west-1 (aproximats; consulta sempre la calculadora oficial):

Concepte Preu Nota
Mètriques d'AWS (AWS/*) Gratis Resolució de 5 min
Monitoratge detallat d'EC2 ~2,10 USD/instància/mes Baixa a 1 min
Mètrica personalitzada 0,30 USD/mes cadascuna Primeres 10.000
PutMetricData 0,01 USD per 1.000 crides Per això els lots
Ingesta de registres Standard ~0,63 USD/GB La partida gran
Ingesta Infrequent Access ~0,32 USD/GB Sense filtres ni alarmes
Emmagatzematge de registres 0,03 USD/GB/mes Comprimit
Logs Insights 0,0063 USD/GB escanejat Per consulta
Alarma estàndard 0,10 USD/mes
Alarma d'alta resolució 0,30 USD/mes
Alarma composta 0,50 USD/mes
Detecció d'anomalies 0,30 USD/mètrica/mes Més l'alarma
Tauler 3 gratis, després 3 USD/mes
Canari de Synthetics 0,0012 USD/execució
Contributor Insights 0,50 USD/regla + consultes

El càlcul de MercadoFresco:

Concepte Quantitat Cost mensual
Mètriques personalitzades de negoci 8 2,40 USD
Mètriques de l'agent (sistema, agregades) 20 6,00 USD
Mètriques d'EMF de les miniatures 6 1,80 USD
Monitoratge detallat d'EC2 2-4 instàncies ~6,30 USD
Ingesta de registres ~22 GB/mes 13,86 USD
Emmagatzematge de registres ~12 GB mitjans 0,36 USD
Logs Insights ~30 GB escanejats 0,19 USD
Alarmes estàndard (13) 13 1,30 USD
Alarma composta 1 0,50 USD
Detecció d'anomalies 1 mètrica + 1 alarma 0,60 USD
Taulers 2 (gratis els 3 primers) 0,00 USD
Canari cada 5 min 8.640 execucions 10,37 USD
Total ~43,68 USD/mes

Les cinc maneres de disparar aquesta factura, per ordre de freqüència real:

  1. Retenció infinita als grups de registres. Creix sense parar i mai no ho notes de cop.
  2. DEBUG activat en producció. Multiplica la ingesta per deu d'un dia per l'altre. La ingesta és la partida cara: 0,63 USD/GB.
  3. Una dimensió de cardinalitat alta. PedidoId com a dimensió: desenes de milers d'USD. És l'error més car que es pot cometre a CloudWatch, i es comet en una línia.
  4. Consultes d'Insights sobre 30 dies per depurar. Cada refinament de la consulta torna a escanejar-ho tot.
  5. Un tauler amb ginys de registre en una pantalla d'oficina. Executa consultes cada minut per sempre.

I la defensa: un pressupost d'AWS Budgets filtrat pel servei CloudWatch (mòdul 11) amb avís al 80 %. A 05-04 veurem a més com AWS Config detecta automàticament els grups de registres sense retenció.

Neteja

# Alarmes
aws cloudwatch delete-alarms --alarm-names \
  mercadofresco-alb-latencia-alta mercadofresco-alb-destinos-sanos \
  mercadofresco-rds-conexiones-altas mercadofresco-rds-disco-bajo \
  mercadofresco-lambda-estrangulada mercadofresco-tienda-memoria-alta \
  mercadofresco-pedidos-fallidos mercadofresco-sin-pedidos \
  mercadofresco-pedidos-anomalos \
  --profile mercadofresco-dev --region eu-west-1

# La composta no es borra DESPRES de les filles: abans.
aws cloudwatch delete-alarms --alarm-names mercadofresco-tienda-degradada \
  --profile mercadofresco-dev --region eu-west-1

# Detector d'anomalies
aws cloudwatch delete-anomaly-detector \
  --namespace MercadoFresco/Tienda --metric-name PedidosConfirmados \
  --dimensions Name=Entorno,Value=produccion Name=Componente,Value=tienda \
  --stat Sum --profile mercadofresco-dev --region eu-west-1

# Tauler
aws cloudwatch delete-dashboards --dashboard-names mercadofresco-produccion \
  --profile mercadofresco-dev --region eu-west-1

# Canari: primer aturar, despres esborrar
aws synthetics stop-canary --name canario-mercadofresco-compra \
  --profile mercadofresco-dev --region eu-west-1
aws synthetics delete-canary --name canario-mercadofresco-compra \
  --profile mercadofresco-dev --region eu-west-1

# Filtres de metriques
aws logs delete-metric-filter \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --filter-name filtro-pedidos-fallidos \
  --profile mercadofresco-dev --region eu-west-1

# Grups de registres (aixo ESBORRA les dades: irreversible)
aws logs delete-log-group --log-group-name /mercadofresco/tienda/nginx-acceso \
  --profile mercadofresco-dev --region eu-west-1

Dos avisos:

  • Les mètriques personalitzades no es poden esborrar. Deixen de facturar-se quan deixen de rebre dades (després del període corresponent), però romanen visibles un temps. És una raó més per no crear dimensions a la lleugera.
  • Esborrar un grup de registres n'elimina les dades de manera irreversible. Si hi pot haver una obligació de conservació, exporta'l a S3 primer i consulta-ho amb qui porti el compliment.

Errors Habituals i Consells

1. Alarmar sobre la mitjana d'una latència. És l'error número u. L'Average de TargetResponseTime es queda pla mentre el p99 és a 9 segons. Fes servir --extended-statistic p95 o p99 per a tot el que sigui temps.

2. Deixar la retenció de registres en «Never expire». És el valor per defecte. És l'error més car a mitjà termini. Posa retenció el mateix dia que crees el grup, i audita mensualment amb describe-log-groups.

3. Fer servir un identificador com a dimensió. PedidoId, UserId, RequestId en una dimensió creen una mètrica nova cada vegada. Milers d'USD. Aquestes dades van al registre o en una anotació d'X-Ray.

4. --treat-missing-data mal triat. Per a una mètrica l'absència de la qual és el problema —PedidosConfirmados, HealthyHostCount— cal posar breaching. Amb notBreaching l'alarma està tranquil·la mentre la botiga està morta.

5. Confondre HTTPCode_ELB_5XX_Count amb HTTPCode_Target_5XX_Count. El primer és del balancejador (no hi ha destins sans, temps d'espera); el segon és la teva aplicació. Buscar al lloc equivocat costa hores.

6. Llindars en unitats equivocades. FreeStorageSpace està en bytes; Duration de Lambda en mil·lisegons; TargetResponseTime en segons. Un zero de més o de menys i l'alarma no salta mai.

7. Crear una alarma sense comprovar la subscripció d'SNS. Si l'ARN és correcte però ningú no ha confirmat la subscripció, l'alarma dispara al buit. Comprova-ho amb list-subscriptions-by-topic i fes un simulacre amb set-alarm-state.

8. No configurar --ok-actions. Qui rep un avís de matinada necessita saber quan s'ha recuperat. Sense la notificació de tornada a OK, algú s'aixeca per no res o, pitjor, no s'aixeca quan ho hauria de fer.

9. Buscar a CloudWatch qui ha esborrat un recurs. Això és a CloudTrail (05-03). CloudWatch registra el que la teva aplicació diu d'ella mateixa.

10. Mètriques de CloudFront buscades a eu-west-1. Són a us-east-1, sempre. Igual que els certificats d'ACM (03-04) i la Web ACL de CloudFront (04-05).

11. Consultes d'Insights sobre rangs enormes mentre es depura. Comença per 15 minuts, afina la consulta, i només llavors amplia la finestra. Es paga per GB escanejat a cada execució.

12. Registrar text pla en lloc de JSON. Amb JSON, Insights descompon els camps sol i les consultes són trivials. Amb text pla cal escriure expressions regulars amb parse. Canviar el format del registre el primer dia costa una hora; fer-ho tres anys després, setmanes.

13. Un filtre de mètriques sense defaultValue=0. La sèrie té buits, l'alarma es queda en INSUFFICIENT_DATA i la detecció d'anomalies no funciona.

14. Oblidar treure les accions de les alarmes filles en crear-ne una de composta. S'acaba rebent un avís més, no menys.

Consell final: la mètrica més valuosa d'un sistema gairebé mai és tècnica. PedidosConfirmados detecta més incidents reals que CPUUtilization, perquè pot haver-hi mil maneres que la botiga falli amb la CPU al 30 %, però només n'hi ha una que les comandes caiguin a zero.

Exercicis

Exercici 1: dissenyar l'alarma que detecta una fallada parcial

MercadoFresco desplega una versió nova de la botiga un dimarts a les 11:00. La versió té un error que només afecta les comandes pagades amb targeta d'un banc concret: aproximadament el 12 % de les compres. La resta funciona amb normalitat.

Dades de l'incident: CPUUtilization sense canvis (42 %), HealthyHostCount a 2, TargetResponseTime p95 estable a 0,6 s, HTTPCode_Target_5XX_Count amb 3 errors cada 5 minuts (abans: 1). PedidosConfirmados passa de 900/h a 792/h. Als registres apareixen línies {"nivel":"ERROR","tipo_error":"PAGO_RECHAZADO","pasarela":"banco-x"}.

No salta cap alarma actual. L'error es descobreix 6 hores després per una queixa a les xarxes socials.

Dissenya la detecció: quina mètrica o mètriques faries servir, com les obtindries (filtre de mètriques, mètrica personalitzada, expressió), quin tipus d'alarma i amb quins paràmetres exactes de període, avaluació i dades absents. Escriu les ordres. Justifica per què la teva proposta hauria saltat en menys de 30 minuts sense generar falsos positius la resta del mes.

Exercici 2: la consulta de Logs Insights i el filtre de mètriques

La Sara pregunta si el cercador de la botiga funciona bé. L'aplicació escriu una línia JSON per cerca:

{"nivel":"INFO","evento":"busqueda","termino":"tomate rama","resultados":24,"duracion_ms":180,"cliente_hash":"a3f8c1e9"}

Escriu:

  • a) Una consulta d'Insights que doni, per hora, el nombre de cerques, la durada mitjana, el p95 i el percentatge de cerques sense resultats.
  • b) Una consulta que llisti els 20 termes més cercats que retornen zero resultats (són productes que MercadoFresco hauria de tenir al catàleg: valor directe per a la Sara).
  • c) Un filtre de mètriques i una alarma que avisin si el percentatge de cerques sense resultats supera el 20 % durant 15 minuts, que és el senyal que l'índex de cerca s'ha corromput. Compte: un filtre de mètriques compta, no calcula percentatges. Necessitaràs una expressió de mètrica a l'alarma.

Exercici 3: l'auditoria de cost

La Marta rep la factura i CloudWatch ha passat de 44 a 610 USD en un mes. El desglossament:

Partida Import
Ingesta de registres (Standard) 412 USD
Mètriques personalitzades 96 USD
Logs Insights 71 USD
Alarmes i taulers 4 USD
Synthetics 27 USD

Pistes de la investigació:

  • El grup /mercadofresco/tienda/aplicacion ha passat de 8 GB a 620 GB en el mes.
  • L'espai MercadoFresco/Tienda té ara 320 mètriques; el mes passat en tenia 8.
  • Hi ha 11.200 execucions d'Insights, totes amb finestra de 30 dies.
  • Hi ha un canari nou, canario-mercadofresco-admin, executant-se cada minut.
  • El Luis va desplegar fa tres setmanes una funció de «comandes recomanades» que registra l'activitat de cada client.

Diagnostica cada partida, digues exactament què va fer malament el Luis en cada cas, escriu les accions correctives amb les ordres, i proposa tres controls preventius perquè no torni a passar. Estima la factura resultant.

Solucions

Solució 1

Per què no va saltar res. Totes les alarmes actuals miren la infraestructura, i la infraestructura està perfecta. Una fallada del 12 % en una passarel·la concreta és invisible per a la CPU, per al nombre de destins sans i per al p95 de latència (un pagament rebutjat respon ràpid, fins i tot més ràpid que un de correcte). I la caiguda de comandes de 900 a 792 —un 12 %— queda dins de la variació normal d'un dimarts. Cap llindar fix raonable no detecta un 12 %.

Això és el que s'anomena una fallada parcial, i és el tipus d'incident que més triga a detectar-se en sistemes reals.

Detecció proposada: tres capes.

Capa 1 — el senyal directe. Filtre de mètriques sobre l'error concret, desglossat per passarel·la.

aws logs put-metric-filter \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --filter-name filtro-pago-rechazado-banco-x \
  --filter-pattern '{ $.tipo_error = "PAGO_RECHAZADO" && $.pasarela = "banco-x" }' \
  --metric-transformations \
      metricName=PagosRechazadosBancoX,\
metricNamespace=MercadoFresco/Tienda,\
metricValue=1,defaultValue=0 \
  --profile mercadofresco-dev --region eu-west-1

Capa 2 — l'alarma de proporció, que és la bona. Un llindar absolut de «més de N rebuigs» falla en tots dos sentits: de nit 3 rebuigs són moltíssim, al pic del divendres no són res. El correcte és alarmar sobre el percentatge, amb una expressió de mètrica:

aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-tasa-rechazo-pagos \
  --alarm-description "Mes del 5% dels intents de pagament son rebutjats" \
  --evaluation-periods 3 --datapoints-to-alarm 2 \
  --threshold 5 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --metrics '[
    {"Id":"rechazos","MetricStat":{"Metric":{"Namespace":"MercadoFresco/Tienda",
      "MetricName":"PagosRechazadosBancoX"},"Period":300,"Stat":"Sum"},
      "ReturnData":false},
    {"Id":"intentos","MetricStat":{"Metric":{"Namespace":"MercadoFresco/Tienda",
      "MetricName":"IntentosPago"},"Period":300,"Stat":"Sum"},
      "ReturnData":false},
    {"Id":"tasa","Expression":"100 * rechazos / intentos",
      "Label":"% de pagaments rebutjats","ReturnData":true}
  ]' \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

Amb un 12 % de rebuigs enfront d'un llindar del 5 %, i «2 de 3» períodes de 5 minuts, aquesta alarma salta en 10-15 minuts. El requisit és publicar també IntentosPago (un segon filtre de mètriques o una mètrica personalitzada); sense denominador no hi ha proporció.

Capa 3 — la xarxa de seguretat genèrica: detecció d'anomalies sobre PedidosConfirmados. Una caiguda del 12 % sostinguda durant 6 hores que surt de la banda d'anomalia, perquè el model coneix el patró d'un dimarts al matí amb precisió. És l'alarma mercadofresco-pedidos-anomalos que ja vam crear: hauria saltat, probablement a la primera hora. És més lenta que la capa 2, però és l'única que detecta problemes que ningú no ha anticipat.

Per què no genera falsos positius. La capa 2 mira una proporció, i una proporció és independent del volum: funciona igual a les 4 de la matinada que el divendres a les 19:00. Amb «2 de 3» períodes de 5 minuts, un pic aïllat de rebuigs —un banc que reinicia la seva passarel·la un minut— no dispara res.

La lliçó de fons: les alarmes d'infraestructura detecten caigudes totals. Les fallades parcials, que són les més freqüents i les que més diners costen, només les detecten les mètriques de negoci i les proporcions.

Solució 2

a) Panorama per hores.

fields @timestamp, duracion_ms, resultados, termino
| filter evento = "busqueda"
| stats count() as cerques,
        avg(duracion_ms) as mitjana_ms,
        pct(duracion_ms, 95) as p95_ms,
        sum(resultados = 0) as sense_resultats,
        100.0 * sum(resultados = 0) / count() as pct_sense_resultats
  by bin(1h)
| sort @timestamp desc

La clau és a sum(resultados = 0): a Insights, una expressió booleana val 1 o 0, així que sumar-la compta els casos certs. És l'idioma per calcular percentatges en aquest llenguatge.

b) Termes sense resultats: valor de negoci directe.

fields termino
| filter evento = "busqueda" and resultados = 0
| stats count() as vegades by termino
| sort vegades desc
| limit 20

Això no és monitoratge tècnic: és una llista de productes que els clients cerquen i MercadoFresco no ven. La Marta l'envia a la Sara cada dilluns. És el millor exemple que els registres d'una aplicació serveixen per a molt més que depurar.

c) Filtre de mètriques i alarma de proporció.

Dos filtres, perquè calen numerador i denominador:

# Denominador: totes les cerques
aws logs put-metric-filter \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --filter-name filtro-busquedas-total \
  --filter-pattern '{ $.evento = "busqueda" }' \
  --metric-transformations \
      metricName=Busquedas,metricNamespace=MercadoFresco/Tienda,\
metricValue=1,defaultValue=0 \
  --profile mercadofresco-dev --region eu-west-1

# Numerador: les que no retornen res
aws logs put-metric-filter \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --filter-name filtro-busquedas-vacias \
  --filter-pattern '{ $.evento = "busqueda" && $.resultados = 0 }' \
  --metric-transformations \
      metricName=BusquedasSinResultados,metricNamespace=MercadoFresco/Tienda,\
metricValue=1,defaultValue=0 \
  --profile mercadofresco-dev --region eu-west-1

I l'alarma amb expressió:

aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-buscador-degradado \
  --alarm-description "Mes del 20% de les cerques no retornen resultats" \
  --evaluation-periods 3 --datapoints-to-alarm 3 \
  --threshold 20 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --metrics '[
    {"Id":"vacias","MetricStat":{"Metric":{"Namespace":"MercadoFresco/Tienda",
      "MetricName":"BusquedasSinResultados"},"Period":300,"Stat":"Sum"},"ReturnData":false},
    {"Id":"total","MetricStat":{"Metric":{"Namespace":"MercadoFresco/Tienda",
      "MetricName":"Busquedas"},"Period":300,"Stat":"Sum"},"ReturnData":false},
    {"Id":"pct","Expression":"100 * vacias / MAX([total, 1])",
      "Label":"% cerques sense resultats","ReturnData":true}
  ]' \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

Tres decisions justificades:

  • MAX([total, 1]) al denominador evita la divisió per zero quan no hi ha cerques de matinada. Sense això, l'expressió produeix valors no numèrics i l'alarma es comporta de manera erràtica.
  • 3 de 3 períodes de 5 minuts = 15 minuts, tal com demanava l'enunciat. Aquí sí que convé «3 de 3» i no «2 de 3»: un índex corromput no s'arregla sol, així que el senyal serà sostingut, i esperar 15 minuts evita el soroll d'un desplegament de l'índex de cerca.
  • notBreaching perquè de matinada pot no haver-hi cerques, i això no és un incident.

Solució 3

Diagnòstic partida per partida.

1. Ingesta de registres: 412 USD (8 GB → 620 GB). La funció de «comandes recomanades» del Luis registra l'activitat de cada client en cada visita. Probablement escriu una línia per producte vist, i molt probablement amb nivel: DEBUG perquè així ho va deixar després de depurar. 612 GB extra a 0,63 USD/GB són 386 USD. És la causa principal.

Error del Luis: deixar DEBUG en producció i escriure al registre d'aplicació un flux d'esdeveniments d'analítica que no és un registre d'aplicació.

Correcció, en dos temps:

# Immediat: baixar el nivell de registre per variable d'entorn / parametre
aws ssm put-parameter --name /mercadofresco/produccion/nivel-log \
  --value INFO --type String --overwrite \
  --profile mercadofresco-dev --region eu-west-1

# I assegurar la retencio, que molt probablement segueix en "no expira mai"
aws logs put-retention-policy \
  --log-group-name /mercadofresco/tienda/aplicacion \
  --retention-in-days 30 \
  --profile mercadofresco-dev --region eu-west-1

De fons: els esdeveniments de comportament de client no van a CloudWatch Logs. Van a mercadofresco-informes-analitica a S3, via Firehose, on l'emmagatzematge costa 0,023 USD/GB en lloc dels 0,63 USD/GB d'ingesta. És 27 vegades més barat i és on la Sara els vol de totes maneres.

2. Mètriques personalitzades: 96 USD (8 → 320 mètriques). 320 × 0,30 = 96 USD. Gairebé amb seguretat, la funció de recomanacions publica una mètrica amb una dimensió de cardinalitat mitjana —CategoriaProducto amb 300 valors, o ModeloRecomendador—.

Error del Luis: fer servir com a dimensió una cosa que no és una categoria petita i tancada.

Correcció: treure aquesta dimensió i, si cal el desglossament, posar-lo com a propietat EMF, que és consultable amb Insights i no costa 0,30 USD per valor. Nota important per a l'examen mental: la factura de mètriques no baixa fins al mes següent, perquè les mètriques ja creades es facturen mentre rebin dades. No es poden esborrar; cal deixar de publicar-les.

I la prevenció de debò, a la política IAM del rol —el patró de 04-01—:

{
  "Effect": "Allow",
  "Action": "cloudwatch:PutMetricData",
  "Resource": "*",
  "Condition": {
    "StringEquals": { "cloudwatch:namespace": "MercadoFresco/Tienda" }
  }
}

Això no limita la cardinalitat —IAM no pot—, però sí que impedeix que una aplicació creï espais de noms nous sense control, que és l'altra meitat del problema.

3. Logs Insights: 71 USD (11.200 execucions a 30 dies). 11.200 consultes escanejant volums enormes. Dues causes superposades: algú depurant amb la finestra en «30 dies» sense canviar-la, i molt probablement un giny de registre en un tauler obert en una pantalla que reexecuta la consulta cada vegada que es refresca.

Correcció: reduir la finestra per defecte dels ginys de registre, posar un limit baix, i establir la norma de començar sempre per 15 minuts. Com que el grup baixa de 620 GB a ~8 GB en arreglar el punt 1, aquesta partida cau sola en més d'un 95 %.

4. Synthetics: 27 USD. canario-mercadofresco-admin cada minut són 43.200 execucions al mes. Per a un tauler d'administració que fan servir tres persones en horari d'oficina, és absurd.

Correcció:

aws synthetics update-canary \
  --name canario-mercadofresco-admin \
  --schedule Expression="rate(15 minutes)" \
  --profile mercadofresco-dev --region eu-west-1

De 43.200 a 2.880 execucions: de 51,84 a 3,46 USD. I si només importa en horari d'oficina, es pot programar amb una expressió cron en lloc de rate.

5. Alarmes i taulers: 4 USD. Correcte. No es toca.

Factura estimada després de les correccions:

Partida Abans Després Com
Ingesta de registres 412 USD ~14 USD INFO + analítica a S3 + retenció
Mètriques personalitzades 96 USD ~10 USD Treure la dimensió (efecte el mes següent)
Logs Insights 71 USD ~1 USD Menys volum i finestres acotades
Alarmes i taulers 4 USD 4 USD
Synthetics 27 USD ~14 USD Canari d'admin cada 15 min
Total 610 USD ~43 USD

Tres controls preventius:

  1. Pressupost d'AWS Budgets filtrat per servei = CloudWatch, amb llindar de 60 USD i avís al 80 % a [email protected]. Detecta la desviació en dies, no a la factura del mes següent. Es veu al mòdul 11.
  2. Regla d'AWS Config que marqui com a no conforme qualsevol grup de registres sense retentionInDays, amb remediació automàtica que li posi 30 dies. És exactament el cas d'ús de la lliçó 05-04.
  3. Revisió al procés de desplegament: ningú no desplega codi que escriu registres nous sense que una altra persona revisi el volum estimat i el nivell de registre. És una casella a la revisió de codi, no una eina. El mòdul 8 la integra al pipeline.

I un quart, cultural, que és el que de debò funciona: ensenyar la factura. Quan el Luis va veure que els seus registres de depuració costaven 386 USD al mes —més que les instàncies EC2 que els generaven— no va tornar a deixar DEBUG activat.

Conclusió

MercadoFresco ja té una mirada. Saps que CloudWatch són tres coses —mètriques, registres i alarmes— i saps què no és: ni el registre de qui ha cridat l'API (CloudTrail, 05-03), ni el rastrejador de peticions (X-Ray, 05-02), ni l'avaluador de compliment (Config, 05-04), ni el bus d'esdeveniments (EventBridge, 07-03).

Domines el model de mètriques: espai de noms, nom i dimensions, amb la regla d'or que cada combinació de dimensions és una mètrica facturable i que un identificador en una dimensió és l'error més car d'aquest servei. Saps triar l'estadísticaSum per a comptadors, Average per a utilització, p95/p99 sempre per a latència— i per què la mitjana de 0,17 s amagava que deu clients per minut esperaven nou segons per pagar. Coneixes la retenció automàtica que agrega les dades a 5 minuts després de 15 dies, i la taula de les mètriques que de debò decideixen alguna cosa a cada servei que has muntat, amb la diferència crítica entre HTTPCode_ELB_5XX_Count i HTTPCode_Target_5XX_Count.

Has publicat les mètriques de negoci PedidosConfirmados i TiempoConfirmacionPedido a MercadoFresco/Tienda, primer amb PutMetricData directe i després per lots des d'un fil a part per treure la crida de la ruta crítica d'una compra. Saps per què EC2 no publica memòria —l'hipervisor no veu dins del sistema operatiu convidat— i has desplegat l'agent unificat a lt-mercadofresco-tienda amb la seva configuració a Parameter Store, les seves append_dimensions resoltes soles i el seu retention_in_days posat des del primer dia. I coneixes l'Embedded Metric Format, que a Lambda et dona mètriques gratis a la ruta crítica i —això és el més important— et permet desar la clau d'S3 i l'ID de petició al costat de la mètrica sense pagar cardinalitat.

En registres, has centralitzat el que es podia centralitzar i saps el que no: els accessos de l'ALB i de CloudFront viuen a S3 i es consulten amb Athena. Tens la política de retenció per grup, la classe Infrequent Access per als flow logs, i les consultes de Logs Insights que responen preguntes reals —els bloquejos de WAF per regla, el p99 d'una Lambda amb @duration, els rebuigs al port 5432, i parse amb expressió regular per al programari que no registra en JSON—. I, sobretot, has respost la pregunta de la comanda de vuit segons: quatre grups de registres consultats alhora amb un identificador de correlació, una cronologia muntada esdeveniment a esdeveniment, i el culpable identificat —7.402 ms dins de PostgreSQL, un patró N+1 amb 39 consultes per a una comanda de 38 productes—.

Saps convertir un patró de registre en mètrica amb un filtre de mètriques (i per què defaultValue=0 no és opcional), i treure registres amb subscripcions en temps real o exportació a S3.

En alarmes, domines el que separa una alarma útil d'una font de soroll: la combinació «M de N» amb --datapoints-to-alarm 2 --evaluation-periods 3, el tractament de dades absents —amb breaching per a les mètriques l'absència de les quals és l'incident, com PedidosConfirmados—, les alarmes compostes que converteixen tres SMS en un i que exigeixen treure les accions de les filles, i la detecció d'anomalies que sap que 0 comandes a les 4 de la matinada és normal i a les 19:00 és una catàstrofe. Has muntat vuit alarmes noves —que se sumen a les dels mòduls 2 i 4 fins a tretze en total—, la composta mercadofresco-tienda-degradada i el detector d'anomalies sobre PedidosConfirmados.

I has respost la primera pregunta que va deixar oberta el mòdul 4, que era la més incòmoda de totes: l'avís arriba de debò. Has comprovat les subscripcions buscant el temut PendingConfirmation, has subscrit el correu i el telèfon de guàrdia, i has fet el simulacre amb set-alarm-state, que dispara les accions reals sense tocar la mètrica. I saps que la cadena inclou el telèfon: el «no molestar» d'un mòbil ha silenciat més alarmes que qualsevol error de configuració d'AWS.

Tot això es veu junt al tauler mercadofresco-produccion, ordenat de dalt a baix —negoci, components, detall—, amb les mètriques de CloudFront al seu giny d'us-east-1, les línies d'anotació que ensenyen com de prop estàs del llindar abans que salti, i el giny de registre amb els últims errors. I amb el canari canario-mercadofresco-compra recorrent la botiga cada cinc minuts perquè un diumenge de matinada, sense ni un sol client real, algú continuï comprovant que es pot comprar. Tot plegat per uns 44 USD al mes, amb les cinc maneres de disparar aquesta xifra identificades i la retenció posada el mateix dia que es crea cada grup.

Queda, però, una insatisfacció concreta. La investigació de la comanda de vuit segons va funcionar, però va costar quatre consultes, una cronologia muntada a mà en un full de càlcul, i va dependre per complet que algú s'hagués recordat de propagar la capçalera X-Peticion-Id per tots els components. Si demà el coll d'ampolla és dins de la Lambda, o a la crida a la passarel·la de pagament, o al temps de connexió a la base de dades, caldrà repetir la feina sencera i endevinar un altre cop on mirar.

Existeix una eina que fa aquesta feina sola, que dibuixa la cronologia en un gràfic i que diu, per a una petició concreta, quant va trigar cada tram del recorregut: AWS X-Ray. A la lliçó 05-02, «AWS X-Ray i traçabilitat distribuïda», veurem què és una traça, un segment i un subsegment, com es propaga l'identificador amb X-Amzn-Trace-Id, per què no es tracen totes les peticions i com es configura el mostreig, com instrumentar la botiga amb aws_xray_sdk i activar el rastreig a mercadofresco-estado-pedido amb una casella i un permís, com llegir el mapa de serveis i cercar amb filtres com annotation.pedido_id, i tornarem a aquesta mateixa comanda de 8,14 segons per veure-la, aquesta vegada, en un sol gràfic.

Curs d'AWS

Mòdul 1: Introducció a AWS

Mòdul 2: Serveis principals d'AWS

Mòdul 3: Xarxes i lliurament de contingut

Mòdul 4: Seguretat i identitat

Mòdul 5: Monitoratge i gestió

Mòdul 6: Bases de dades

Mòdul 7: Integració d'aplicacions

Mòdul 8: Eines per a desenvolupadors

Mòdul 9: Infraestructura com a codi i govern de comptes

Mòdul 10: Contenidors a AWS

Mòdul 11: Millors pràctiques i gestió de costos

© Copyright 2026. Tots els drets reservats