diff --git a/platform/00-application-platform.yaml b/platform/00-application-platform.yaml new file mode 100644 index 0000000..b25da9d --- /dev/null +++ b/platform/00-application-platform.yaml @@ -0,0 +1,55 @@ +# ============================================================================= +# A pasta `platform/` passa a ser GITOPS (2026-09-21). +# +# ── O QUE ESTAVA ACONTECENDO ──────────────────────────────────────────────── +# +# As cinco Applications cobrem `tenants/`; a `monitoring` e o chart do +# Helm. **Ninguem cobria `platform/`** — regras de alerta, ServiceMonitor, +# dashboard e o Ingress do ArgoCD eram aplicados com `kubectl apply` a mao. +# Consequencia: o repositorio podia dizer uma coisa e o cluster outra, e nada +# avisava. Foi o que aconteceu: o clone tinha 2 regras de alerta e o cluster, 8. +# +# ── POR QUE ESTE ARQUIVO VIVE AQUI DENTRO ─────────────────────────────────── +# +# A Application que sincroniza `platform/` esta DENTRO de `platform/`: depois do +# primeiro `kubectl apply`, ela passa a se manter sozinha. Mudanca nela vira +# commit, como todo o resto. +# +# ── prune: false ──────────────────────────────────────────────────────────── +# +# Mesma escolha das outras Applications do projeto: arquivo removido do git NAO +# apaga o recurso no cluster. Para remover de verdade, `kubectl delete` explicito +# — apagar por esquecimento aqui derrubaria monitoramento ou o proprio Ingress do +# ArgoCD. +# +# ── O `.tmpl` que fica de fora ────────────────────────────────────────────── +# +# `monitoring-netpol-template.yaml.tmpl` e MOLDE, com `__NS__` no lugar do +# namespace: nao e manifesto valido. A extensao `.tmpl` ja o deixa fora do que o +# ArgoCD le, e o `exclude` abaixo torna isso explicito para quem renomear. +# ============================================================================= +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: atm-platform + namespace: argocd +spec: + project: default + source: + repoURL: http://gitea-http.gitea.svc:3000/atmadmin/athletic-map-deploy.git + targetRevision: main + path: platform + directory: + recurse: true + exclude: "*.tmpl" + destination: + server: https://kubernetes.default.svc + # Os recursos trazem o proprio `namespace` (monitoring, argocd). Este e so o + # destino de quem vier sem. + namespace: monitoring + syncPolicy: + automated: + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=false diff --git a/platform/monitoring-netpol-template.yaml.tmpl b/platform/monitoring-netpol-template.yaml.tmpl new file mode 100644 index 0000000..5fb7011 --- /dev/null +++ b/platform/monitoring-netpol-template.yaml.tmpl @@ -0,0 +1,40 @@ +# ============================================================================= +# Deixa o Prometheus raspar as metricas deste tenant. +# +# Aditivo a `deny-cross-tenant` (em `00-namespace-quota-netpol.yaml`), que fecha +# ingresso de fora do namespace. Sem esta politica o tenant sobe, funciona e fica +# INVISIVEL no Grafana — e um tenant sem metrica e um tenant em que ninguem +# percebe consumo subindo nem pod reiniciando. +# +# ── POR QUE `podSelector: {}` (2026-09-20) ────────────────────────────────── +# +# Antes a politica citava pods por NOME (`app: backend`, ou uma lista). Resultado: +# quando o `ServiceMonitor` passou a raspar os runtimes e os 13 servicos, TODOS os +# alvos novos ficaram `up=0` e o cluster acendeu 48 alertas de `TargetDown` — a +# coleta chegava ao Service e morria na rede. +# +# Lista de nomes aqui e a mesma lista do `ServiceMonitor` escrita duas vezes, em +# lugares diferentes, com o erro aparecendo so em producao. Agora vale para +# qualquer pod DESTE namespace, e a fronteira e a mesma de antes: +# +# - SO a porta 8083, que e a das metricas (`/actuator/prometheus`); +# - SO do namespace `monitoring`. +# +# O `frontend-spa` escuta na 80 e continua fora, sem precisar ser citado. +# ============================================================================= +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-monitoring + namespace: __NS__ +spec: + podSelector: {} + policyTypes: [Ingress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - protocol: TCP + port: 8083 diff --git a/platform/monitoring/apps-servicemonitor.yaml b/platform/monitoring/apps-servicemonitor.yaml new file mode 100644 index 0000000..e8e3941 --- /dev/null +++ b/platform/monitoring/apps-servicemonitor.yaml @@ -0,0 +1,113 @@ +# ============================================================================= +# P5.3/P6.x — o Prometheus passa a raspar as APLICAÇÕES do Athletic Map. +# +# ── O QUE ESTAVA FALTANDO (achado em 2026-09-20) ──────────────────────────── +# +# O `athleticmap-backends` só seleciona Services com `app: backend` — o backend +# monolítico antigo. Resultado: 4 alvos raspados no cluster inteiro. Nenhum dos +# 13 serviços, nenhum runtime, nem o BFF, nem o `servico-captacao` entregavam +# métrica ao Prometheus. +# +# Consequência prática: os contadores que o código já publica +# (`atm_provisionamento_teto_atingido_total`, `atm_webhook_assinatura_recusado_total`) +# não existiam em lugar nenhum, e alerta sobre eles não teria como funcionar. +# +# ── POR QUE UMA LISTA, E NÃO "TUDO" ───────────────────────────────────────── +# +# Selecionar todo Service com porta `http` pegaria o `frontend-spa` (nginx, que +# não tem `/actuator/prometheus`) e encheria o log do Prometheus de 404. +# A lista é explícita: quem entra, entra por nome. +# +# ── POR QUE DOIS ServiceMonitor, E NÃO UM (2026-09-21) ────────────────────── +# +# Num tenant consolidado os 13 Services `servico-` APONTAM PARA O +# RUNTIME: raspar os 13 e mais os 3 runtimes significa raspar o MESMO pod 14 +# vezes, com 14 nomes de `service` diferentes. Medido: 85 alvos e 119 mil séries, +# e no Grafana a mesma JVM aparecia como catorze coisas. +# +# Então a seleção é por NAMESPACE: onde há runtime, raspa-se o runtime; onde os +# 13 serviços ainda rodam de verdade (o `escolinha`), raspa-se os 13. +# **Tenant que migrar muda de lista aqui** — é o mesmo passo do PgBouncer. +# +# ── CUIDADO COM A MEMÓRIA DO PROMETHEUS ───────────────────────────────────── +# +# O nó é único e o Prometheus tem limite de 1536Mi (usava 758Mi em 2026-09-20). +# Cada app Spring publica ~300 séries. Por isso: intervalo de 60s (e não 30s) e +# uma LISTA DO QUE GUARDAR — o resto é descartado na ingestão. Se um dia faltar +# uma métrica no Grafana, é aqui que ela precisa entrar. +# ============================================================================= +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: athleticmap-runtimes + namespace: monitoring + labels: + release: monitoring +spec: + # Tenants JA consolidados: o que roda sao os 3 runtimes (+ BFF). + namespaceSelector: + matchNames: [demo-prod, piloto-prod, acme-prod] + selector: + matchExpressions: + - key: app + operator: In + values: + - runtime-core + - runtime-operacao + - runtime-saude + - servico-bff + endpoints: + - port: http + path: /actuator/prometheus + interval: 60s + scrapeTimeout: 20s + metricRelabelings: + # Guarda o que se usa: as métricas do domínio (`atm_*`), o essencial da + # JVM e do HTTP, o pool do banco, e o `up` (que o Prometheus gera). + - sourceLabels: [__name__] + regex: "atm_.*|up|jvm_memory_used_bytes|jvm_memory_max_bytes|jvm_threads_live_threads|process_cpu_usage|system_cpu_usage|http_server_requests_seconds_count|http_server_requests_seconds_sum|http_server_requests_seconds_max|hikaricp_connections_active|hikaricp_connections_pending|hikaricp_connections_max|flyway_migrations_.*|application_ready_time_seconds" + action: keep +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: athleticmap-servicos + namespace: monitoring + labels: + release: monitoring +spec: + # Tenants que ainda rodam os servicos separados. O `escolinha` e o mostruario + # (decisao do dono, 2026-09-19); o `plataforma` tem os proprios servicos. + namespaceSelector: + matchNames: [escolinha-prod, plataforma-prod] + selector: + matchExpressions: + - key: app + operator: In + values: + - servico-administrativo + - servico-agenda + - servico-assistencia-social + - servico-bff + - servico-cadastro + - servico-campeonato + - servico-captacao + - servico-configuracao + - servico-documentos + - servico-financeiro + - servico-nutricao + - servico-organizacao + - servico-pedagogico + - servico-planejamento + - servico-saude + endpoints: + - port: http + path: /actuator/prometheus + interval: 60s + scrapeTimeout: 20s + metricRelabelings: + # Guarda o que se usa: as métricas do domínio (`atm_*`), o essencial da + # JVM e do HTTP, o pool do banco, e o `up` (que o Prometheus gera). + - sourceLabels: [__name__] + regex: "atm_.*|up|jvm_memory_used_bytes|jvm_memory_max_bytes|jvm_threads_live_threads|process_cpu_usage|system_cpu_usage|http_server_requests_seconds_count|http_server_requests_seconds_sum|http_server_requests_seconds_max|hikaricp_connections_active|hikaricp_connections_pending|hikaricp_connections_max|flyway_migrations_.*|application_ready_time_seconds" + action: keep diff --git a/platform/monitoring/athleticmap-rules.yaml b/platform/monitoring/athleticmap-rules.yaml index 159bf81..fa94cdf 100644 --- a/platform/monitoring/athleticmap-rules.yaml +++ b/platform/monitoring/athleticmap-rules.yaml @@ -1,27 +1,132 @@ +# ============================================================================= +# Regras de alerta do Athletic Map. +# +# ── LIMITE QUE VALE SABER (2026-09-20) ────────────────────────────────────── +# O cluster NAO tem Alertmanager: estas regras DISPARAM no Prometheus (aparecem +# em Alerts, e no Grafana), mas ninguem e avisado por e-mail ou mensagem. Enquanto +# o Alertmanager nao existir, alerta so e visto por quem abre a tela. Esta +# registrado no diario como pendencia propria. +# ============================================================================= apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: athleticmap-rules namespace: monitoring labels: - release: monitoring + release: monitoring # o Prometheus so carrega regras com este label spec: groups: - name: athleticmap.backup rules: + # 26h: o backup roda 1x por dia (02:00 nos tenants, 02:30 no compartilhado). + # Duas horas de folga evitam alarme por atraso de agendamento. + # + # O `pg-backup.*` cobre os DOIS: o `pg-backup` de cada tenant (que ainda + # salva o Postgres antigo dele) e o `pg-backup-compartilhado`, que salva + # os bancos `athleticmap_*` da instancia compartilhada. Antes a regra + # citava `cronjob="pg-backup"` e o compartilhado — o backup dos tres + # tenants migrados — ficava SEM vigilancia nenhuma. - alert: AthleticMapBackupStale - expr: time() - kube_cronjob_status_last_successful_time{cronjob="pg-backup"} > 93600 + expr: time() - kube_cronjob_status_last_successful_time{cronjob=~"pg-backup.*"} > 93600 for: 15m labels: severity: warning annotations: - summary: "Backup do tenant {{ $labels.namespace }} sem sucesso ha mais de 26h" + summary: "Backup {{ $labels.cronjob }} ({{ $labels.namespace }}) sem sucesso ha mais de 26h" + description: "Ver: kubectl -n {{ $labels.namespace }} get job -l job-name --sort-by=.metadata.creationTimestamp" + + # CronJob que existe e NUNCA teve uma execucao bem-sucedida. A regra + # acima nao pega este caso: sem sucesso nenhum, a metrica nem existe. + # Foi o que aconteceu com o compartilhado em 2026-09-19 (a 1a copia + # falhou por senha com quebra de linha, e o script disse "PUBLICADO"). + - alert: AthleticMapBackupNuncaRodou + expr: | + kube_cronjob_spec_suspend{cronjob=~"pg-backup.*"} == 0 + unless on (namespace, cronjob) kube_cronjob_status_last_successful_time + for: 6h + labels: + severity: critical + annotations: + summary: "Backup {{ $labels.cronjob }} ({{ $labels.namespace }}) NUNCA rodou com sucesso" + description: "Backup que nunca rodou nao e backup. Rode a copia na hora e veja o log do Job." + + # Job de backup que terminou em falha (inclusive a `primeira-rodada` que + # os scripts criam). `kube_job_status_failed` conta tentativas falhas. + - alert: AthleticMapBackupJobFalhou + expr: kube_job_status_failed{job_name=~"pg-backup.*|primeira-rodada.*"} > 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Job de backup {{ $labels.job_name }} ({{ $labels.namespace }}) falhou" + description: "kubectl -n {{ $labels.namespace }} logs job/{{ $labels.job_name }}" + - name: athleticmap.health rules: + # O `unless` tira os pods de Job que TERMINARAM: pod concluido nao e + # "Ready", e sem isso todo backup bem-sucedido acendia um alerta + # (conferido em 2026-09-20 contra o Prometheus: a versao antiga casava + # com os pods do `pg-backup` do dia). - alert: AthleticMapTenantPodNotReady - expr: kube_pod_status_ready{namespace=~".*-prod", condition="true"} == 0 + expr: | + kube_pod_status_ready{namespace=~".*-prod", condition="true"} == 0 + unless on (namespace, pod) kube_pod_status_phase{phase=~"Succeeded|Failed"} == 1 for: 10m labels: severity: warning annotations: summary: "Pod {{ $labels.pod }} em {{ $labels.namespace }} nao-Ready ha mais de 10m" + + # O no e UNICO e tem limite de 110 pods. Em 2026-09-19 a consolidacao + # parou porque nao havia vaga ("Too many pods"), e o sintoma so aparecia + # no evento do pod Pending. 95% = 104 pods. + - alert: AthleticMapNoQuaseSemVagaDePod + expr: | + sum(kube_pod_status_phase{phase=~"Running|Pending"}) + / sum(kube_node_status_allocatable{resource="pods"}) > 0.95 + for: 15m + labels: + severity: warning + annotations: + summary: "O no esta com mais de 95% das vagas de pod ocupadas" + description: "Subir algo 'ao lado' pode ficar Pending. Consolidar tenants libera 10 vagas cada." + + - name: athleticmap.provisionamento + rules: + # O teto de execucoes por hora existe porque o provisionamento IMPLANTA: + # um pedido que volta para a fila viraria um laco que publica tenant atras + # de tenant. Quando ele segura pedidos, o log registrava e mais nada — "um + # laco as tres da manha enche o log e ninguem ve". + - alert: AthleticMapProvisionamentoTetoAtingido + expr: increase(atm_provisionamento_teto_atingido_total[15m]) > 0 + for: 0m + labels: + severity: warning + annotations: + summary: "O teto de provisionamentos por hora segurou pedidos" + description: "Ou entraram muitos clientes de uma vez, ou ha um laco. Veja provisionamento/execucao no banco do plataforma." + + # Webhook recusado e evento que o Asaas mandou e nos rejeitamos: token + # errado (configuracao) ou alguem batendo na rota (tentativa). Um ou outro + # acontece; uma rajada, nao. + - alert: AthleticMapWebhookAssinaturaRecusado + expr: increase(atm_webhook_assinatura_recusado_total[15m]) > 5 + for: 0m + labels: + severity: warning + annotations: + summary: "Mais de 5 eventos de assinatura recusados em 15 min" + description: "Token do webhook trocado sem atualizar o SealedSecret, ou alguem batendo na rota publica." + + - name: athleticmap.banco + rules: + # O Postgres compartilhado e o PgBouncer atendem TRES tenants: parada + # neles nao e incidente de um cliente, e de todos. + - alert: AthleticMapCompartilhadoFora + expr: kube_deployment_status_replicas_available{namespace="plataforma-prod", + deployment=~"pg-compartilhado|pgbouncer"} < 1 + for: 5m + labels: + severity: critical + annotations: + summary: "{{ $labels.deployment }} sem replica disponivel — demo, piloto e acme dependem dele"