diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 7e3e2f0..bb4dcaa 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -121,12 +121,42 @@ jobs: - name: Verificar formatação run: dotnet format PlataformaEducacao.sln --verify-no-changes --no-restore + observability-validation: + name: Validar observabilidade + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validar Docker Compose + run: docker compose config --quiet + + - name: Validar configurações Prometheus e Grafana + run: | + test -s observability/prometheus/prometheus.yml + grep -q 'bff-api:8080' observability/prometheus/prometheus.yml + grep -q 'gestao-identidade-api:8080' observability/prometheus/prometheus.yml + grep -q 'gestao-conteudo-api:8080' observability/prometheus/prometheus.yml + grep -q 'gestao-aluno-api:8080' observability/prometheus/prometheus.yml + grep -q 'gestao-financeira-api:8080' observability/prometheus/prometheus.yml + jq empty observability/grafana/dashboards/plataforma-overview.json + + - name: Validar endpoints declarados + run: | + grep -R -q '/health/live' k8s + grep -R -q '/health/ready' k8s + grep -R -q 'UseMetricsConfiguration' src + docker-push: name: Build e Push das Imagens needs: - build-test - lint + - observability-validation if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') @@ -174,4 +204,4 @@ jobs: push: true tags: | ${{ secrets.DOCKERHUB_USERNAME }}/plataforma-educacao-${{ matrix.service }}:latest - ${{ secrets.DOCKERHUB_USERNAME }}/plataforma-educacao-${{ matrix.service }}:${{ github.sha }} \ No newline at end of file + ${{ secrets.DOCKERHUB_USERNAME }}/plataforma-educacao-${{ matrix.service }}:${{ github.sha }} diff --git a/README.md b/README.md index 6d1ac77..f58237f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ O projeto consiste em: - **Orquestração:** Kubernetes (Kind local) - **CI/CD:** GitHub Actions - **Documentação:** Swagger / OpenAPI +- **Observabilidade:** prometheus-net.AspNetCore, Prometheus, Grafana e Serilog ## **4. Estrutura do Projeto** @@ -70,6 +71,7 @@ plataforma-educacao-devops/ | +-- bff-api.yaml | +-- ingress.yaml # Ingress (nginx) roteando host plataforma.local -> bff-api | +-- hpa.yaml # HorizontalPodAutoscalers das 5 APIs +| +-- observability/ # Prometheus, Grafana, ConfigMaps e PVCs | +-- deploy.sh # Script único que aplica todos os manifests na ordem correta | +-- src/ @@ -92,6 +94,15 @@ plataforma-educacao-devops/ | +-- ... (demais projetos de teste) | +-- docker-compose.yml ++-- observability/ # Configurações de Prometheus e Grafana +| +-- prometheus/ +| | +-- prometheus.yml +| +-- grafana/ +| +-- provisioning/ +| | +-- datasources/ +| | +-- dashboards/ +| +-- dashboards/ +| +-- plataforma-overview.json +-- PlataformaEducacao.sln +-- build.ps1 / build.sh ``` @@ -110,6 +121,7 @@ plataforma-educacao-devops/ - **API Gateway (BFF):** Ponto único de entrada que agrega chamadas dos serviços de Identidade, Conteúdo e Alunos. - **Documentação da API:** Documentação automática dos endpoints de cada API utilizando Swagger. - **Health checks nativos (/health)** +- **Observabilidade:** métricas HTTP e de runtime, endpoint `/metrics`, Prometheus, Grafana e logs estruturados em JSON. ## 6. Arquitetura DevOps @@ -209,9 +221,46 @@ Todos os serviços + RabbitMQ sobem automaticamente. Swagger acessível em: | Gestão Conteúdo API | http://localhost:5440/swagger/ | | Gestão Aluno API | http://localhost:5460/swagger/ | | Gestão Financeira API | http://localhost:7083/swagger/ | -| BFF API Gateway | http://localhost:5450/swagger/ | +| BFF API Gateway | http://localhost:5000/swagger/ | | RabbitMQ Management | http://localhost:15672 | +### 7.3.1. Observabilidade + +O ambiente Docker Compose inclui Prometheus e Grafana. Para iniciar os serviços, incluindo os componentes de observabilidade: + +```bash +docker compose up -d --build +``` + +| Componente | URL | +|------------|-----| +| Prometheus | http://localhost:9090 | +| Grafana | http://localhost:3000 | +| Métricas do BFF | http://localhost:5000/metrics | +| Métricas de Identidade | http://localhost:5430/metrics | +| Métricas de Conteúdo | http://localhost:5440/metrics | +| Métricas de Aluno | http://localhost:5460/metrics | +| Métricas Financeiras | http://localhost:7083/metrics | + +O Grafana utiliza o Prometheus como datasource, carrega automaticamente o dashboard **Plataforma Educacao - Overview** e o define como dashboard inicial após o login. + +Credenciais padrão para o ambiente local: + +- Usuário: `admin` +- Senha: `admin` + +Altere essas credenciais antes de utilizar o Grafana em qualquer ambiente compartilhado ou produtivo. + +As métricas e os health checks podem ser verificados diretamente com: + +```bash +curl http://localhost:5000/metrics +curl http://localhost:5000/health/live +curl http://localhost:5000/health/ready +``` + +No Prometheus, os targets podem ser consultados em http://localhost:9090/targets. As cinco aplicações devem aparecer com o estado `UP`. + Para derrubar: ```bash docker compose down -v @@ -257,7 +306,7 @@ docker pull rabbitmq:3-management && kind load docker-image rabb #### Executar o deploy (script único) -O script `k8s/deploy.sh` orquestra toda a aplicação dos manifests na ordem correta (namespace → secret → configmap → infra → wait → APIs → ingress → HPA): +O script `k8s/deploy.sh` orquestra toda a aplicação dos manifests na ordem correta (namespace → secret → configmap → infra → wait → APIs → observabilidade → ingress → HPA): ```bash ./k8s/deploy.sh @@ -272,7 +321,7 @@ O script `k8s/deploy.sh` orquestra toda a aplicação dos manifests na ordem cor ```bash kubectl get pods -n plataforma-educacao ``` -São esperados 7 pods em `Running` (sqlserver, rabbitmq, 4 APIs e o BFF) e os pods do `ingress-nginx` no namespace `ingress-nginx`. +São esperados 9 pods em `Running` (sqlserver, rabbitmq, Prometheus, Grafana, 4 APIs e o BFF) e os pods do `ingress-nginx` no namespace `ingress-nginx`. ```bash kubectl get hpa -n plataforma-educacao # ver os autoscalers @@ -310,6 +359,38 @@ kubectl port-forward -n plataforma-educacao service/gestao-identidade-api 5430:8 ``` Swagger do BFF em `http://localhost:5450/swagger/`. +#### Acessar a observabilidade no Kubernetes + +O deploy também instala Prometheus e Grafana no namespace `plataforma-educacao`: + +```bash +kubectl port-forward -n plataforma-educacao service/prometheus 9090:9090 +kubectl port-forward -n plataforma-educacao service/grafana 3000:3000 +``` + +Os comandos devem ser executados em terminais separados. Depois, acesse: + +| Componente | URL | +|------------|-----| +| Prometheus | http://localhost:9090 | +| Prometheus targets | http://localhost:9090/targets | +| Grafana | http://localhost:3000 | +| Dashboard Grafana | http://localhost:3000/d/plataforma-overview/plataforma-educacao-overview | + +Credenciais locais do Grafana: + +- Usuário: `admin` +- Senha: `admin` + +O Prometheus coleta internamente os endpoints `/metrics` dos Services das cinco aplicações. Após gerar tráfego no BFF, valide no Prometheus com: + +```promql +up +sum(rate(http_requests_received_total[5m])) +``` + +Todos os cinco targets devem aparecer como `UP` em `http://localhost:9090/targets`. + #### Verificando o log estruturado (JSON) No k8s, por padrão, apenas o sink Console está ativo (stdout do pod — o sink File é neutralizado via env var no `k8s/configmap.yaml`). Para inspecionar: @@ -340,7 +421,7 @@ kind delete cluster --name plataforma-educacao Disparada em **pull requests** e **push** para `main`: -A etapa de CI é composta por 2 jobs paralelos: +A etapa de CI é composta por 3 jobs paralelos: 1. **build-test (Build e testes)** - `actions/checkout@v4` → `actions/setup-dotnet@v4` (utilizando .NET 8.0.x) @@ -353,9 +434,15 @@ A etapa de CI é composta por 2 jobs paralelos: - `dotnet restore PlataformaEducacao.sln` — restaura as dependências da solução - `dotnet format PlataformaEducacao.sln --verify-no-changes --no-restore` — valida se o código segue os padrões de formatação definidos, falhando o job caso existam divergências. +3. **observability-validation (Validação de observabilidade)** + - Valida a configuração com `docker compose config`. + - Confirma os cinco targets configurados no Prometheus. + - Valida o JSON do dashboard Grafana. + - Verifica a existência dos endpoints de health check e métricas. + ### CD (Deploy Contínuo) -Disparada automaticamente após o sucesso dos jobs de CI (build-test e lint) apenas quando o evento for um push na branch `main`: +Disparada automaticamente após o sucesso dos jobs de CI (`build-test`, `lint` e `observability-validation`) apenas quando o evento for um push na branch `main`: 1. **docker-push (Build e push das imagens) (matrix strategy)** - `docker/login-action@v3` — realiza a autenticação no Docker Hub @@ -432,7 +519,7 @@ Todas as APIs possuem Swagger configurado com autenticação Bearer JWT. | Conteúdo | https://localhost:5441/swagger/ | http://localhost:5440/swagger/ | http://localhost:5440/swagger/ | | Aluno | https://localhost:5461/swagger/ | http://localhost:5460/swagger/ | http://localhost:5460/swagger/ | | Financeira | https://localhost:7083/swagger/ | http://localhost:7083/swagger/ | http://localhost:7083/swagger/ | -| BFF | https://localhost:5451/swagger/ | http://localhost:5450/swagger/ | http://localhost:5450/swagger/ | +| BFF | https://localhost:5451/swagger/ | http://localhost:5000/swagger/ | http://localhost:5000/swagger/ | ### Autenticação no Swagger @@ -452,7 +539,17 @@ Todas as APIs possuem Swagger configurado com autenticação Bearer JWT. - Campos estruturados por evento: `@t` (timestamp UTC ISO 8601), `@mt` (message template), `@l` (level), `@tr`/`@sp` (TraceId/SpanId), `CorrelationId`, `SourceContext`, `MachineName`, `ProcessId`, `ThreadId`, `Application`, `Service`, e campos de request HTTP (`RequestMethod`, `RequestPath`, `StatusCode`, `Elapsed`) via `UseSerilogRequestLogging`. - **Correlação entre serviços:** middleware `CorrelationId` lê/gera o header `X-Correlation-ID` (presente na resposta HTTP). No BFF, `AddCorrelationIdForwarding` propaga o header em chamadas `HttpClient` outbound para os serviços backend. O mesmo `CorrelationId` atravessa BFF → serviço backend → log, permitindo rastrear uma requisição ponta-a-ponta. - **Logs no Kubernetes:** o sink File é neutralizado via env vars no `k8s/configmap.yaml` (`Serilog__WriteTo__1__Args__path=/dev/null`), de modo que **apenas o sink Console** grava em **stdout** do pod — consumido via `kubectl logs -n plataforma-educacao `. No docker-compose o File sink permanece ativo (`logs/log-YYYYMMDD.json`, 7 dias). -- **Health Checks:** Endpoints `/health` de todas as APIs + - **Métricas:** todas as cinco aplicações expõem o endpoint `/metrics`, com métricas HTTP, runtime e de exceções não tratadas. + - Métricas HTTP incluem quantidade de requisições, duração, status HTTP e requisições em andamento. + - Métricas de runtime incluem uso de memória e CPU do processo .NET. + - Exceções não tratadas são contabilizadas em `application_exceptions_total`. + - A configuração centralizada está em `src/building-blocks/PlataformaEducacao.WebApi.Core/Extensions/MetricsConfig.cs`. + - **Prometheus:** coleta as métricas das aplicações a cada 15 segundos, conforme `observability/prometheus/prometheus.yml`. + - **Grafana:** utiliza o Prometheus como datasource e provisiona o dashboard `observability/grafana/dashboards/plataforma-overview.json`. + - **Health Checks:** + - `/health/live` verifica somente se a aplicação está em execução. + - `/health/ready` verifica SQL Server, RabbitMQ e as APIs dependentes do BFF. + - As dependências possuem timeouts para evitar respostas bloqueadas indefinidamente. ## 12. Avaliação diff --git a/docker-compose.yml b/docker-compose.yml index fa3c1f1..29feab7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -120,9 +120,52 @@ services: depends_on: - gestao-identidade-api + prometheus: + image: prom/prometheus:v3.5.0 + container_name: plataforma-prometheus + restart: unless-stopped + ports: + - "9090:9090" + volumes: + - prometheus-data:/prometheus + - ./observability/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + command: + - --config.file=/etc/prometheus/prometheus.yml + - --storage.tsdb.path=/prometheus + networks: + - plataforma-network + depends_on: + - bff-api + - gestao-identidade-api + - gestao-conteudo-api + - gestao-aluno-api + - gestao-financeira-api + + grafana: + image: grafana/grafana:12.1.1 + container_name: plataforma-grafana + restart: unless-stopped + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/plataforma-overview.json + volumes: + - grafana-data:/var/lib/grafana + - ./observability/grafana/provisioning:/etc/grafana/provisioning:ro + - ./observability/grafana/dashboards:/var/lib/grafana/dashboards:ro + networks: + - plataforma-network + depends_on: + - prometheus + networks: plataforma-network: driver: bridge volumes: sqlserver-data: + prometheus-data: + grafana-data: diff --git a/k8s/bff-api.yaml b/k8s/bff-api.yaml index 1737d05..9d76052 100644 --- a/k8s/bff-api.yaml +++ b/k8s/bff-api.yaml @@ -29,7 +29,7 @@ spec: containers: - name: bff-api image: rinaldoserra/plataforma-educacao-bff-api:latest - imagePullPolicy: Always + imagePullPolicy: IfNotPresent ports: - containerPort: 8080 envFrom: diff --git a/k8s/deploy.sh b/k8s/deploy.sh index af05c0f..584c61e 100644 --- a/k8s/deploy.sh +++ b/k8s/deploy.sh @@ -23,29 +23,29 @@ if ! kubectl cluster-info &> /dev/null; then fi echo "" -echo "[1/7] Aplicando Namespace..." +echo "[1/8] Aplicando Namespace..." kubectl apply -f k8s/namespace.yaml echo "" -echo "[2/7] Aplicando Secrets..." +echo "[2/8] Aplicando Secrets..." kubectl apply -f k8s/secret.yaml echo "" -echo "[3/7] Aplicando ConfigMap..." +echo "[3/8] Aplicando ConfigMap..." kubectl apply -f k8s/configmap.yaml echo "" -echo "[4/7] Aplicando infraestrutura (SQL Server + RabbitMQ)..." +echo "[4/8] Aplicando infraestrutura (SQL Server + RabbitMQ)..." kubectl apply -f k8s/sqlserver.yaml kubectl apply -f k8s/rabbitmq.yaml echo "" -echo "[5/7] Aguardando infraestrutura ficar pronta..." +echo "[5/8] Aguardando infraestrutura ficar pronta..." kubectl wait --for=condition=ready pod -l app=sqlserver -n $NAMESPACE --timeout=300s || true kubectl wait --for=condition=ready pod -l app=rabbitmq -n $NAMESPACE --timeout=120s || true echo "" -echo "[6/7] Aplicando deployments de aplicacao..." +echo "[6/8] Aplicando deployments de aplicacao..." kubectl apply -f k8s/gestao-identidade.yaml kubectl apply -f k8s/gestao-conteudo.yaml kubectl apply -f k8s/gestao-aluno.yaml @@ -53,7 +53,11 @@ kubectl apply -f k8s/gestao-financeira.yaml kubectl apply -f k8s/bff-api.yaml echo "" -echo "[7/7] Aguardando Ingress Controller e aplicando Ingress + HPA..." +echo "[7/8] Aplicando observabilidade (Prometheus + Grafana)..." +kubectl apply -f k8s/observability/ + +echo "" +echo "[8/8] Aguardando Ingress Controller e aplicando Ingress + HPA..." kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=120s 2>/dev/null || echo "AVISO: Ingress controller nao estava pronto, tentando mesmo assim..." kubectl apply -f k8s/ingress.yaml kubectl apply -f k8s/hpa.yaml diff --git a/k8s/observability/01-prometheus-config.yaml b/k8s/observability/01-prometheus-config.yaml new file mode 100644 index 0000000..4c059c6 --- /dev/null +++ b/k8s/observability/01-prometheus-config.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config + namespace: plataforma-educacao +data: + prometheus.yml: | + global: + scrape_interval: 15s + evaluation_interval: 15s + + scrape_configs: + - job_name: bff-api + metrics_path: /metrics + static_configs: + - targets: ["bff-api:8080"] + + - job_name: gestao-identidade-api + metrics_path: /metrics + static_configs: + - targets: ["gestao-identidade-api:8080"] + + - job_name: gestao-conteudo-api + metrics_path: /metrics + static_configs: + - targets: ["gestao-conteudo-api:8080"] + + - job_name: gestao-aluno-api + metrics_path: /metrics + static_configs: + - targets: ["gestao-aluno-api:8080"] + + - job_name: gestao-financeira-api + metrics_path: /metrics + static_configs: + - targets: ["gestao-financeira-api:8080"] diff --git a/k8s/observability/02-prometheus-storage.yaml b/k8s/observability/02-prometheus-storage.yaml new file mode 100644 index 0000000..999f243 --- /dev/null +++ b/k8s/observability/02-prometheus-storage.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: prometheus-data + namespace: plataforma-educacao +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi diff --git a/k8s/observability/03-prometheus.yaml b/k8s/observability/03-prometheus.yaml new file mode 100644 index 0000000..3362381 --- /dev/null +++ b/k8s/observability/03-prometheus.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus + namespace: plataforma-educacao +spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + containers: + - name: prometheus + image: prom/prometheus:v3.5.0 + imagePullPolicy: IfNotPresent + args: + - --config.file=/etc/prometheus/prometheus.yml + - --storage.tsdb.path=/prometheus + ports: + - containerPort: 9090 + readinessProbe: + httpGet: + path: /-/ready + port: 9090 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /-/healthy + port: 9090 + initialDelaySeconds: 30 + periodSeconds: 30 + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "500m" + volumeMounts: + - name: config + mountPath: /etc/prometheus + readOnly: true + - name: data + mountPath: /prometheus + volumes: + - name: config + configMap: + name: prometheus-config + - name: data + persistentVolumeClaim: + claimName: prometheus-data diff --git a/k8s/observability/04-prometheus-service.yaml b/k8s/observability/04-prometheus-service.yaml new file mode 100644 index 0000000..b64618a --- /dev/null +++ b/k8s/observability/04-prometheus-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: prometheus + namespace: plataforma-educacao +spec: + type: ClusterIP + selector: + app: prometheus + ports: + - name: http + port: 9090 + targetPort: 9090 diff --git a/k8s/observability/05-grafana-config.yaml b/k8s/observability/05-grafana-config.yaml new file mode 100644 index 0000000..80f0c79 --- /dev/null +++ b/k8s/observability/05-grafana-config.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-provisioning + namespace: plataforma-educacao +data: + datasource.yml: | + apiVersion: 1 + + datasources: + - name: Prometheus + type: prometheus + uid: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false + dashboards.yml: | + apiVersion: 1 + + providers: + - name: Plataforma Educacao + orgId: 1 + folder: Plataforma Educacao + type: file + disableDeletion: true + updateIntervalSeconds: 30 + options: + path: /var/lib/grafana/dashboards diff --git a/k8s/observability/06-grafana-dashboard.yaml b/k8s/observability/06-grafana-dashboard.yaml new file mode 100644 index 0000000..477220c --- /dev/null +++ b/k8s/observability/06-grafana-dashboard.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard + namespace: plataforma-educacao +data: + plataforma-overview.json: | + { + "annotations": {"list": []}, + "editable": false, + "graphTooltip": 0, + "panels": [ + {"title": "Status dos servicos", "type": "stat", "gridPos": {"h": 4, "w": 6, "x": 0, "y": 0}, "targets": [{"expr": "sum(up)", "legendFormat": "UP"}], "options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "orientation": "auto"}}, + {"title": "Requisicoes por segundo", "type": "timeseries", "gridPos": {"h": 8, "w": 9, "x": 6, "y": 0}, "targets": [{"expr": "sum by (job) (rate(http_requests_received_total[5m]))", "legendFormat": "{{job}}"}]}, + {"title": "Taxa de erro HTTP", "type": "timeseries", "gridPos": {"h": 8, "w": 9, "x": 15, "y": 0}, "targets": [{"expr": "sum(rate(http_requests_received_total{code=~\"5..\"}[5m])) / clamp_min(sum(rate(http_requests_received_total[5m])), 1)", "legendFormat": "5xx"}]}, + {"title": "Latencia media", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, "targets": [{"expr": "sum(rate(http_request_duration_seconds_sum[5m])) / clamp_min(sum(rate(http_request_duration_seconds_count[5m])), 1)", "legendFormat": "media"}]}, + {"title": "Latencia P95", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, "targets": [{"expr": "histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))", "legendFormat": "p95"}]}, + {"title": "Memoria utilizada", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}, "targets": [{"expr": "process_working_set_bytes", "legendFormat": "{{job}}"}]}, + {"title": "CPU utilizada", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}, "targets": [{"expr": "rate(process_cpu_seconds_total[5m])", "legendFormat": "{{job}}"}]}, + {"title": "Health checks / disponibilidade", "type": "timeseries", "gridPos": {"h": 6, "w": 24, "x": 0, "y": 24}, "targets": [{"expr": "up", "legendFormat": "{{job}}"}]} + ], + "schemaVersion": 39, + "tags": ["plataforma-educacao", "observabilidade"], + "templating": {"list": []}, + "time": {"from": "now-6h", "to": "now"}, + "title": "Plataforma Educacao - Overview", + "uid": "plataforma-overview", + "version": 1 + } diff --git a/k8s/observability/07-grafana-secret.yaml b/k8s/observability/07-grafana-secret.yaml new file mode 100644 index 0000000..c0cad00 --- /dev/null +++ b/k8s/observability/07-grafana-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: grafana-secret + namespace: plataforma-educacao +type: Opaque +stringData: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin diff --git a/k8s/observability/08-grafana-storage.yaml b/k8s/observability/08-grafana-storage.yaml new file mode 100644 index 0000000..b9edaca --- /dev/null +++ b/k8s/observability/08-grafana-storage.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: grafana-data + namespace: plataforma-educacao +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/k8s/observability/09-grafana.yaml b/k8s/observability/09-grafana.yaml new file mode 100644 index 0000000..1cf7ecd --- /dev/null +++ b/k8s/observability/09-grafana.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grafana + namespace: plataforma-educacao +spec: + replicas: 1 + selector: + matchLabels: + app: grafana + template: + metadata: + labels: + app: grafana + spec: + containers: + - name: grafana + image: grafana/grafana:12.1.1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3000 + envFrom: + - secretRef: + name: grafana-secret + env: + - name: GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH + value: /var/lib/grafana/dashboards/plataforma-overview.json + readinessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 30 + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "300m" + volumeMounts: + - name: provisioning + mountPath: /etc/grafana/provisioning/datasources/datasource.yml + subPath: datasource.yml + readOnly: true + - name: provisioning + mountPath: /etc/grafana/provisioning/dashboards/dashboards.yml + subPath: dashboards.yml + readOnly: true + - name: dashboard + mountPath: /var/lib/grafana/dashboards/plataforma-overview.json + subPath: plataforma-overview.json + readOnly: true + - name: data + mountPath: /var/lib/grafana + volumes: + - name: provisioning + configMap: + name: grafana-provisioning + - name: dashboard + configMap: + name: grafana-dashboard + - name: data + persistentVolumeClaim: + claimName: grafana-data diff --git a/k8s/observability/10-grafana-service.yaml b/k8s/observability/10-grafana-service.yaml new file mode 100644 index 0000000..6cf9bdf --- /dev/null +++ b/k8s/observability/10-grafana-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: grafana + namespace: plataforma-educacao +spec: + type: ClusterIP + selector: + app: grafana + ports: + - name: http + port: 3000 + targetPort: 3000 diff --git a/observability/grafana/dashboards/plataforma-overview.json b/observability/grafana/dashboards/plataforma-overview.json new file mode 100644 index 0000000..514bfc1 --- /dev/null +++ b/observability/grafana/dashboards/plataforma-overview.json @@ -0,0 +1,22 @@ +{ + "annotations": {"list": []}, + "editable": false, + "graphTooltip": 0, + "panels": [ + {"title": "Status dos servicos", "type": "stat", "gridPos": {"h": 4, "w": 6, "x": 0, "y": 0}, "targets": [{"expr": "sum(up)", "legendFormat": "UP"}], "options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "orientation": "auto"}}, + {"title": "Requisicoes por segundo", "type": "timeseries", "gridPos": {"h": 8, "w": 9, "x": 6, "y": 0}, "targets": [{"expr": "sum by (job) (rate(http_requests_received_total[5m]))", "legendFormat": "{{job}}"}]}, + {"title": "Taxa de erro HTTP", "type": "timeseries", "gridPos": {"h": 8, "w": 9, "x": 15, "y": 0}, "targets": [{"expr": "sum(rate(http_requests_received_total{code=~\"5..\"}[5m])) / clamp_min(sum(rate(http_requests_received_total[5m])), 1)", "legendFormat": "5xx"}]}, + {"title": "Latencia media", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, "targets": [{"expr": "sum(rate(http_request_duration_seconds_sum[5m])) / clamp_min(sum(rate(http_request_duration_seconds_count[5m])), 1)", "legendFormat": "media"}]}, + {"title": "Latencia P95", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, "targets": [{"expr": "histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))", "legendFormat": "p95"}]}, + {"title": "Memoria utilizada", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}, "targets": [{"expr": "process_working_set_bytes", "legendFormat": "{{job}}"}]}, + {"title": "CPU utilizada", "type": "timeseries", "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}, "targets": [{"expr": "rate(process_cpu_seconds_total[5m])", "legendFormat": "{{job}}"}]}, + {"title": "Health checks / disponibilidade", "type": "timeseries", "gridPos": {"h": 6, "w": 24, "x": 0, "y": 24}, "targets": [{"expr": "up", "legendFormat": "{{job}}"}]} + ], + "schemaVersion": 39, + "tags": ["plataforma-educacao", "observabilidade"], + "templating": {"list": []}, + "time": {"from": "now-6h", "to": "now"}, + "title": "Plataforma Educacao - Overview", + "uid": "plataforma-overview", + "version": 1 +} diff --git a/observability/grafana/provisioning/dashboards/dashboards.yml b/observability/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..483d4a6 --- /dev/null +++ b/observability/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: Plataforma Educacao + orgId: 1 + folder: Plataforma Educacao + type: file + disableDeletion: true + updateIntervalSeconds: 30 + options: + path: /var/lib/grafana/dashboards diff --git a/observability/grafana/provisioning/datasources/prometheus.yml b/observability/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 0000000..8704d9e --- /dev/null +++ b/observability/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + uid: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false diff --git a/observability/prometheus/prometheus.yml b/observability/prometheus/prometheus.yml new file mode 100644 index 0000000..6843a3a --- /dev/null +++ b/observability/prometheus/prometheus.yml @@ -0,0 +1,29 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: bff-api + metrics_path: /metrics + static_configs: + - targets: ["bff-api:8080"] + + - job_name: gestao-identidade-api + metrics_path: /metrics + static_configs: + - targets: ["gestao-identidade-api:8080"] + + - job_name: gestao-conteudo-api + metrics_path: /metrics + static_configs: + - targets: ["gestao-conteudo-api:8080"] + + - job_name: gestao-aluno-api + metrics_path: /metrics + static_configs: + - targets: ["gestao-aluno-api:8080"] + + - job_name: gestao-financeira-api + metrics_path: /metrics + static_configs: + - targets: ["gestao-financeira-api:8080"] diff --git a/src/api-gateways/PlataformaEducacao.Bff.Api/Configurations/ApiConfig.cs b/src/api-gateways/PlataformaEducacao.Bff.Api/Configurations/ApiConfig.cs index 956e849..774dce4 100644 --- a/src/api-gateways/PlataformaEducacao.Bff.Api/Configurations/ApiConfig.cs +++ b/src/api-gateways/PlataformaEducacao.Bff.Api/Configurations/ApiConfig.cs @@ -33,7 +33,7 @@ public static IServiceCollection AddApiConfig(this IServiceCollection services, services.AddCorsConfiguration(configuration); services.AddHealthChecks() .AddCheck("live", () => HealthCheckResult.Healthy(), tags: ["live"]) - .AddCheck("dependencies", tags: ["ready"]); + .AddCheck("dependencies", tags: ["ready"], timeout: TimeSpan.FromSeconds(10)); return services; } diff --git a/src/api-gateways/PlataformaEducacao.Bff.Api/Configurations/DependencyInjectionConfig.cs b/src/api-gateways/PlataformaEducacao.Bff.Api/Configurations/DependencyInjectionConfig.cs index 3ae0bd4..a95aae3 100644 --- a/src/api-gateways/PlataformaEducacao.Bff.Api/Configurations/DependencyInjectionConfig.cs +++ b/src/api-gateways/PlataformaEducacao.Bff.Api/Configurations/DependencyInjectionConfig.cs @@ -45,6 +45,7 @@ public static IServiceCollection RegisterServices(this IServiceCollection servic p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))); services.AddHttpClient() + .ConfigureHttpClient(client => client.Timeout = TimeSpan.FromSeconds(5)) .AddCorrelationIdForwarding() .AddPolicyHandler(PollyExtensions.EsperarTentar()) .AddTransientHttpErrorPolicy( diff --git a/src/api-gateways/PlataformaEducacao.Bff.Api/Program.cs b/src/api-gateways/PlataformaEducacao.Bff.Api/Program.cs index 968b4c0..954ae5a 100644 --- a/src/api-gateways/PlataformaEducacao.Bff.Api/Program.cs +++ b/src/api-gateways/PlataformaEducacao.Bff.Api/Program.cs @@ -7,6 +7,7 @@ builder.Host.ConfigureAppSettings(); builder.Host.AddLoggingConfiguration(builder.Configuration, "BFF"); builder.Services.AddCorrelationIdConfiguration(builder.Configuration); +builder.Services.AddMetricsConfiguration(); builder.Services .AddApiConfig(builder.Configuration) @@ -17,7 +18,8 @@ var app = builder.Build(); -app.UseLoggingConfiguration() +app.UseMetricsConfiguration() + .UseLoggingConfiguration() .UseSwaggerConfiguration() .UseApiConfiguration(app.Environment); diff --git a/src/building-blocks/PlataformaEducacao.WebApi.Core/Extensions/MetricsConfig.cs b/src/building-blocks/PlataformaEducacao.WebApi.Core/Extensions/MetricsConfig.cs new file mode 100644 index 0000000..495e70f --- /dev/null +++ b/src/building-blocks/PlataformaEducacao.WebApi.Core/Extensions/MetricsConfig.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Prometheus; + +namespace PlataformaEducacao.WebApi.Core.Extensions +{ + public static class MetricsConfig + { + private static readonly Counter ApplicationExceptions = Metrics.CreateCounter( + "application_exceptions_total", + "Total de excecoes nao tratadas observadas pela aplicacao."); + + public static IServiceCollection AddMetricsConfiguration(this IServiceCollection services) + { + return services; + } + + public static IApplicationBuilder UseMetricsConfiguration(this IApplicationBuilder app) + { + app.UseHttpMetrics(); + app.Use(async (context, next) => + { + try + { + await next(context); + } + catch + { + ApplicationExceptions.Inc(); + throw; + } + }); + app.UseMetricServer(); + return app; + } + } +} diff --git a/src/building-blocks/PlataformaEducacao.WebApi.Core/PlataformaEducacao.WebApi.Core.csproj b/src/building-blocks/PlataformaEducacao.WebApi.Core/PlataformaEducacao.WebApi.Core.csproj index 692c8f0..a689720 100644 --- a/src/building-blocks/PlataformaEducacao.WebApi.Core/PlataformaEducacao.WebApi.Core.csproj +++ b/src/building-blocks/PlataformaEducacao.WebApi.Core/PlataformaEducacao.WebApi.Core.csproj @@ -17,7 +17,8 @@ - + + diff --git a/src/services/GestaoAluno/PlataformaEducacao.GestaoAluno.Api/Configurations/ApiConfig.cs b/src/services/GestaoAluno/PlataformaEducacao.GestaoAluno.Api/Configurations/ApiConfig.cs index b2b1441..4417c1f 100644 --- a/src/services/GestaoAluno/PlataformaEducacao.GestaoAluno.Api/Configurations/ApiConfig.cs +++ b/src/services/GestaoAluno/PlataformaEducacao.GestaoAluno.Api/Configurations/ApiConfig.cs @@ -46,8 +46,8 @@ public static IServiceCollection AddApiConfiguration(this IServiceCollection ser services.AddCorsConfiguration(configuration); services.AddHealthChecks() .AddCheck("live", () => HealthCheckResult.Healthy(), tags: ["live"]) - .AddCheck>("sqlserver", tags: ["ready"]) - .AddCheck("rabbitmq", tags: ["ready"]); + .AddCheck>("sqlserver", tags: ["ready"], timeout: TimeSpan.FromSeconds(5)) + .AddCheck("rabbitmq", tags: ["ready"], timeout: TimeSpan.FromSeconds(5)); return services; } diff --git a/src/services/GestaoAluno/PlataformaEducacao.GestaoAluno.Api/Program.cs b/src/services/GestaoAluno/PlataformaEducacao.GestaoAluno.Api/Program.cs index ab84c94..7d1abf4 100644 --- a/src/services/GestaoAluno/PlataformaEducacao.GestaoAluno.Api/Program.cs +++ b/src/services/GestaoAluno/PlataformaEducacao.GestaoAluno.Api/Program.cs @@ -11,6 +11,7 @@ builder.Host.ConfigureAppSettings(); builder.Host.AddLoggingConfiguration(builder.Configuration, "GestaoAluno"); builder.Services.AddCorrelationIdConfiguration(builder.Configuration); +builder.Services.AddMetricsConfiguration(); builder.Services .AddApiConfiguration(builder.Configuration) @@ -22,7 +23,8 @@ var app = builder.Build(); -app.UseLoggingConfiguration() +app.UseMetricsConfiguration() + .UseLoggingConfiguration() .UseSwaggerConfiguration() .UseApiConfiguration(app.Environment); diff --git a/src/services/GestaoConteudo/PlataformaEducacao.GestaoConteudo.Api/Configurations/ApiConfig.cs b/src/services/GestaoConteudo/PlataformaEducacao.GestaoConteudo.Api/Configurations/ApiConfig.cs index 868db82..2f55119 100644 --- a/src/services/GestaoConteudo/PlataformaEducacao.GestaoConteudo.Api/Configurations/ApiConfig.cs +++ b/src/services/GestaoConteudo/PlataformaEducacao.GestaoConteudo.Api/Configurations/ApiConfig.cs @@ -46,8 +46,8 @@ public static IServiceCollection AddApiConfiguration(this IServiceCollection ser services.AddCorsConfiguration(configuration); services.AddHealthChecks() .AddCheck("live", () => HealthCheckResult.Healthy(), tags: ["live"]) - .AddCheck>("sqlserver", tags: ["ready"]) - .AddCheck("rabbitmq", tags: ["ready"]); + .AddCheck>("sqlserver", tags: ["ready"], timeout: TimeSpan.FromSeconds(5)) + .AddCheck("rabbitmq", tags: ["ready"], timeout: TimeSpan.FromSeconds(5)); return services; } diff --git a/src/services/GestaoConteudo/PlataformaEducacao.GestaoConteudo.Api/Program.cs b/src/services/GestaoConteudo/PlataformaEducacao.GestaoConteudo.Api/Program.cs index 9ba1450..840d52a 100644 --- a/src/services/GestaoConteudo/PlataformaEducacao.GestaoConteudo.Api/Program.cs +++ b/src/services/GestaoConteudo/PlataformaEducacao.GestaoConteudo.Api/Program.cs @@ -7,6 +7,7 @@ builder.Host.ConfigureAppSettings(); builder.Host.AddLoggingConfiguration(builder.Configuration, "GestaoConteudo"); builder.Services.AddCorrelationIdConfiguration(builder.Configuration); +builder.Services.AddMetricsConfiguration(); builder.Services .AddApiConfiguration(builder.Configuration) @@ -18,7 +19,8 @@ var app = builder.Build(); -app.UseLoggingConfiguration() +app.UseMetricsConfiguration() + .UseLoggingConfiguration() .UseSwaggerConfiguration() .UseApiConfiguration(app.Environment); diff --git a/src/services/GestaoFinanceira/PlataformaEducacao.GestaoFinanceira.Api/Configuration/ApiConfig.cs b/src/services/GestaoFinanceira/PlataformaEducacao.GestaoFinanceira.Api/Configuration/ApiConfig.cs index c3ef726..d2ac024 100644 --- a/src/services/GestaoFinanceira/PlataformaEducacao.GestaoFinanceira.Api/Configuration/ApiConfig.cs +++ b/src/services/GestaoFinanceira/PlataformaEducacao.GestaoFinanceira.Api/Configuration/ApiConfig.cs @@ -46,8 +46,8 @@ public static IServiceCollection AddApiConfiguration(this IServiceCollection ser services.AddCorsConfiguration(configuration); services.AddHealthChecks() .AddCheck("live", () => HealthCheckResult.Healthy(), tags: ["live"]) - .AddCheck>("sqlserver", tags: ["ready"]) - .AddCheck("rabbitmq", tags: ["ready"]); + .AddCheck>("sqlserver", tags: ["ready"], timeout: TimeSpan.FromSeconds(5)) + .AddCheck("rabbitmq", tags: ["ready"], timeout: TimeSpan.FromSeconds(5)); return services; } diff --git a/src/services/GestaoFinanceira/PlataformaEducacao.GestaoFinanceira.Api/Program.cs b/src/services/GestaoFinanceira/PlataformaEducacao.GestaoFinanceira.Api/Program.cs index 423f1fd..e4f9781 100644 --- a/src/services/GestaoFinanceira/PlataformaEducacao.GestaoFinanceira.Api/Program.cs +++ b/src/services/GestaoFinanceira/PlataformaEducacao.GestaoFinanceira.Api/Program.cs @@ -8,6 +8,7 @@ builder.Host.ConfigureAppSettings(); builder.Host.AddLoggingConfiguration(builder.Configuration, "GestaoFinanceira"); builder.Services.AddCorrelationIdConfiguration(builder.Configuration); +builder.Services.AddMetricsConfiguration(); builder.Services .AddApiConfiguration(builder.Configuration) @@ -22,7 +23,8 @@ var app = builder.Build(); -app.UseLoggingConfiguration() +app.UseMetricsConfiguration() + .UseLoggingConfiguration() .UseSwaggerConfiguration() .UseApiConfiguration(app.Environment); diff --git a/src/services/GestaoIdentidade/PlataformaEducacao.GestaoIdentidade.Api/Configurations/ApiConfig.cs b/src/services/GestaoIdentidade/PlataformaEducacao.GestaoIdentidade.Api/Configurations/ApiConfig.cs index 30439e9..d257ca7 100644 --- a/src/services/GestaoIdentidade/PlataformaEducacao.GestaoIdentidade.Api/Configurations/ApiConfig.cs +++ b/src/services/GestaoIdentidade/PlataformaEducacao.GestaoIdentidade.Api/Configurations/ApiConfig.cs @@ -43,8 +43,8 @@ public static IServiceCollection AddApiConfig(this IServiceCollection services, services.AddCorsConfiguration(configuration); services.AddHealthChecks() .AddCheck("live", () => HealthCheckResult.Healthy(), tags: ["live"]) - .AddCheck>("sqlserver", tags: ["ready"]) - .AddCheck("rabbitmq", tags: ["ready"]); + .AddCheck>("sqlserver", tags: ["ready"], timeout: TimeSpan.FromSeconds(5)) + .AddCheck("rabbitmq", tags: ["ready"], timeout: TimeSpan.FromSeconds(5)); return services; } diff --git a/src/services/GestaoIdentidade/PlataformaEducacao.GestaoIdentidade.Api/Program.cs b/src/services/GestaoIdentidade/PlataformaEducacao.GestaoIdentidade.Api/Program.cs index 882a6f5..c6ecf8b 100644 --- a/src/services/GestaoIdentidade/PlataformaEducacao.GestaoIdentidade.Api/Program.cs +++ b/src/services/GestaoIdentidade/PlataformaEducacao.GestaoIdentidade.Api/Program.cs @@ -6,6 +6,7 @@ builder.Host.ConfigureAppSettings(); builder.Host.AddLoggingConfiguration(builder.Configuration, "GestaoIdentidade"); builder.Services.AddCorrelationIdConfiguration(builder.Configuration); +builder.Services.AddMetricsConfiguration(); builder.Services .AddApiConfig(builder.Configuration) @@ -17,7 +18,8 @@ var app = builder.Build(); -app.UseLoggingConfiguration() +app.UseMetricsConfiguration() + .UseLoggingConfiguration() .UseSwaggerConfiguration() .UseApiConfiguration(app.Environment);