> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aciona.me/llms.txt
> Use this file to discover all available pages before exploring further.

# AWS CloudWatch

> Conecte alarms do CloudWatch ao aciona.me via SNS e uma Lambda de transformação.

## Visão geral

O CloudWatch publica as transições de alarm em um **topic SNS**. Uma **Lambda** assinada nesse topic converte a notificação para o formato canônico do aciona.me e faz o POST na fonte de alerta.

```mermaid theme={null}
flowchart LR
  A["CloudWatch Alarm"] -->|"AlarmActions / OKActions"| B["SNS topic"]
  B --> C["Lambda de transformação"]
  C -->|"POST + X-Aciona-Token"| D["aciona.me"]
```

|                    |                       |
| ------------------ | --------------------- |
| **Tipo de fonte**  | `CloudWatch`          |
| **Mecanismo**      | SNS + Lambda          |
| **Correlação**     | `alarmArn`            |
| **Auto-resolução** | ✅ (exige `OKActions`) |

## Pré-requisitos

* Permissão na AWS para criar topics SNS, funções Lambda e alterar alarms.
* Um serviço criado no aciona.me e vinculado a um time com escala ativa.

## 1. Crie a fonte de alerta no aciona.me

Em **Fontes de alerta**, crie uma fonte do tipo **CloudWatch**. Copie a URL e o token.

## 2. Crie o topic SNS

```bash theme={null}
aws sns create-topic --name aciona-me-cloudwatch
```

## 3. Implante a Lambda de transformação

Runtime **Node.js 20+**, handler `index.handler`, sem dependências externas.

Variáveis de ambiente:

| Variável             | Valor                                            |
| -------------------- | ------------------------------------------------ |
| `ACIONA_INGRESS_URL` | `https://ingress.aciona.me/webhooks/<idDaFonte>` |
| `ACIONA_TOKEN`       | O token da fonte                                 |
| `REGION`             | Região da Lambda, ex.: `us-east-1`               |

IAM role: `AWSLambdaBasicExecutionRole`.

<Warning>
  Em produção, guarde o token no **SSM Parameter Store (SecureString)** ou no **Secrets Manager**, e leia-o no cold start da função — não deixe o valor em texto puro na configuração da Lambda.
</Warning>

<Accordion title="Código da Lambda" icon="code">
  ```javascript index.mjs theme={null}
  import https from 'node:https';
  import { URL } from 'node:url';

  const SEVERITY_MAP = {
    crit: 'critical', critical: 'critical', sev1: 'critical', p1: 'critical',
    high: 'high', sev2: 'high', p2: 'high',
    warn: 'warning', warning: 'warning', sev3: 'warning', p3: 'warning',
    info: 'info', low: 'info', sev4: 'info', p4: 'info',
  };

  const str = (v) => (v === null || v === undefined ? '' : String(v));

  function parseRegionFromArn(arn) {
    const parts = str(arn).split(':');
    return parts.length >= 4 ? parts[3] : '';
  }

  function resolveSeverity(alarmDescription, alarmName) {
    const m = str(alarmDescription).match(/\[severity:([a-zA-Z0-9]+)\]/);
    if (m) return SEVERITY_MAP[m[1].toLowerCase()] || 'warning';
    const name = str(alarmName).toLowerCase();
    for (const k of ['critical', 'high', 'warning', 'info']) {
      if (name.includes(k)) return k;
    }
    return 'warning';
  }

  function extractServiceHint(trigger, alarmName) {
    if (!trigger || typeof trigger !== 'object') return str(alarmName);
    const dims = trigger.Dimensions || {};
    const byNamespace = {
      'AWS/ECS': ['ServiceName', 'ClusterName'],
      'AWS/RDS': ['DBClusterIdentifier', 'DBInstanceIdentifier'],
      'AWS/DynamoDB': ['TableName'],
      'AWS/Lambda': ['FunctionName'],
      'AWS/EC2': ['InstanceId'],
      'AWS/ApplicationElb': ['LoadBalancer'],
      'AWS/ApiGateway': ['ApiName'],
      'AWS/SQS': ['QueueName'],
      'AWS/S3': ['BucketName'],
    };
    for (const k of byNamespace[trigger.Namespace] || []) {
      if (dims[k]) return str(dims[k]);
    }
    const ns = str(trigger.Namespace).replace(/^(AWS\/|ECS\/|EKS\/)/, '');
    return ns || str(alarmName);
  }

  function toCanonical(msg) {
    const alarmArn = str(msg.AlarmArn);
    const alarmName = str(msg.AlarmName);
    const region = parseRegionFromArn(alarmArn) || process.env.REGION || '';
    const trigger = msg.Trigger || null;
    return {
      schema: 'aciona.cloudwatch.v1',
      source: 'aws_cloudwatch',
      alarmName,
      alarmArn,
      awsAccountId: str(msg.AWSAccountId),
      region,
      state: str(msg.NewStateValue).toUpperCase(),
      oldState: str(msg.OldStateValue).toUpperCase(),
      reason: str(msg.NewStateReason),
      stateChangeTime: str(msg.StateChangeTime),
      severity: resolveSeverity(msg.AlarmDescription, alarmName),
      service: extractServiceHint(trigger, alarmName),
      dimensions: trigger?.Dimensions || null,
      trigger: trigger
        ? {
            metricName: trigger.MetricName,
            namespace: trigger.Namespace,
            statistic: trigger.Statistic,
            period: trigger.Period,
            evaluationPeriods: trigger.EvaluationPeriods,
            comparisonOperator: trigger.ComparisonOperator,
            threshold: trigger.Threshold,
          }
        : null,
      alarmDescription: str(msg.AlarmDescription),
      url: region && alarmName
        ? `https://console.aws.amazon.com/cloudwatch/home?region=${region}#alarmsAlarm:alarmName=${encodeURIComponent(alarmName)}`
        : '',
    };
  }

  function postToIngress(payload) {
    return new Promise((resolve, reject) => {
      const url = new URL(process.env.ACIONA_INGRESS_URL);
      const body = JSON.stringify(payload);
      const req = https.request(
        {
          method: 'POST',
          hostname: url.hostname,
          path: url.pathname + url.search,
          headers: {
            'Content-Type': 'application/json',
            'Content-Length': Buffer.byteLength(body),
            'X-Aciona-Token': process.env.ACIONA_TOKEN,
          },
        },
        (res) => {
          let data = '';
          res.on('data', (c) => (data += c));
          res.on('end', () => {
            if (res.statusCode >= 200 && res.statusCode < 300) resolve(data);
            else reject(new Error(`HTTP ${res.statusCode}: ${data}`));
          });
        },
      );
      req.on('error', reject);
      req.write(body);
      req.end();
    });
  }

  export const handler = async (event) => {
    const results = [];
    for (const record of event.Records || []) {
      try {
        const canonical = toCanonical(JSON.parse(record.Sns.Message));
        await postToIngress(canonical);
        results.push({ ok: true, alarmArn: canonical.alarmArn });
      } catch (err) {
        console.error('[aciona.me] falha no registro SNS', err.message);
        results.push({ ok: false, error: err.message });
      }
    }
    if (results.length > 0 && results.every((r) => !r.ok)) {
      throw new Error('Todos os registros falharam');
    }
    return { batch: results };
  };
  ```
</Accordion>

## 4. Assine a Lambda no topic

```bash theme={null}
aws sns subscribe \
  --topic-arn <arn-do-topic> \
  --protocol lambda \
  --notification-endpoint <arn-da-lambda>
```

Restrinja a policy do topic com `aws:SourceAccount` e, se possível, `aws:SourceArn`, para que apenas o CloudWatch da sua conta possa publicar.

## 5. Configure o alarm com AlarmActions **e** OKActions

```bash theme={null}
aws cloudwatch put-metric-alarm \
  --alarm-name "api-pagamentos-5xx-high" \
  --alarm-description "5xx acima de 5/s por 1 minuto [severity:critical]" \
  --alarm-actions <arn-do-topic> \
  --ok-actions <arn-do-topic> \
  ...
```

<Warning>
  **`OKActions` é obrigatório para a auto-resolução.** Sem ele, o CloudWatch nunca emite a transição `OK` e o acionamento fica aberto indefinidamente. Este é o passo mais esquecido da integração inteira.
</Warning>

## 6. Teste

```bash theme={null}
aws cloudwatch set-alarm-state \
  --alarm-name "api-pagamentos-5xx-high" \
  --state-value ALARM \
  --state-reason "teste manual aciona.me"
```

Confirme o acionamento no painel. Depois force a recuperação:

```bash theme={null}
aws cloudwatch set-alarm-state \
  --alarm-name "api-pagamentos-5xx-high" \
  --state-value OK \
  --state-reason "teste manual aciona.me"
```

O acionamento deve ser resolvido automaticamente.

## Como os campos são traduzidos

| Estado do alarm     | Efeito                     |
| ------------------- | -------------------------- |
| `ALARM`             | Cria ou agrega acionamento |
| `OK`                | Resolve automaticamente    |
| `INSUFFICIENT_DATA` | Ignorado                   |

O CloudWatch não tem severidade nativa. A Lambda resolve o alias em camadas:

1. Marcador no `AlarmDescription`: `[severity:critical]`
2. Convenção no nome do alarm (contém `critical`, `high`, `warning` ou `info`)
3. Padrão: `warning`

## Solução de problemas

<AccordionGroup>
  <Accordion title="O acionamento abre, mas nunca fecha sozinho" icon="triangle-alert">
    O alarm não tem `OKActions` apontando para o topic SNS. Adicione com `put-metric-alarm --ok-actions`.
  </Accordion>

  <Accordion title="Nada chega no aciona.me" icon="unplug">
    Verifique, nesta ordem: a subscription da Lambda no topic está confirmada; os logs da Lambda no CloudWatch Logs; `ACIONA_INGRESS_URL` e `ACIONA_TOKEN` corretos; a Lambda tem saída para a internet (se estiver em VPC, precisa de NAT).
  </Accordion>

  <Accordion title="Serviço errado no acionamento" icon="server">
    Ajuste as dimensões do alarm ou crie no aciona.me um serviço com o nome que a Lambda deriva. Veja a função `extractServiceHint` no código.
  </Accordion>

  <Accordion title="Severidade sempre warning" icon="gauge">
    Adicione `[severity:critical]` ao `AlarmDescription` ou inclua a palavra no nome do alarm.
  </Accordion>
</AccordionGroup>
