From df006427f37724a3275549a6e70a4128feeb0590 Mon Sep 17 00:00:00 2001 From: Matheus Freire | Backend & Infra Date: Fri, 10 Jul 2026 07:18:55 -0300 Subject: [PATCH 1/4] Translate README to English --- README.md | 100 +++++++++++++++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 1f70df3..8c6106c 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,39 @@ # Relay -Relay é uma plataforma full stack para processamento assíncrono de eventos, construída com FastAPI, RabbitMQ, PostgreSQL, Redis, React, Docker e uma stack completa de observabilidade. +Relay is a full-stack platform for asynchronous event processing, built with FastAPI, RabbitMQ, PostgreSQL, Redis, React, Docker, and a complete observability stack. -O projeto demonstra como uma aplicação pode receber eventos, persistir estado de forma confiável, processar mensagens por domínio e oferecer visibilidade operacional sobre falhas, retries, DLQ, métricas, logs e traces. +The project demonstrates how an application can receive events, persist state reliably, process domain-specific messages, and provide operational visibility into failures, retries, the Dead Letter Queue, metrics, logs, and traces. -## Tecnologias +## Technology Stack -| Área | Tecnologias | +| Area | Technologies | | --- | --- | | Backend | Python, FastAPI, SQLAlchemy, Alembic, Pydantic | | Frontend | React, TypeScript, Vite | -| Mensageria | RabbitMQ, topic exchange, DLX, retry queues | -| Dados | PostgreSQL, Redis | -| Observabilidade | Prometheus, Grafana, OpenTelemetry, Tempo, Loki, Alloy, Alertmanager | -| Infra | Docker Compose, Nginx | +| Messaging | RabbitMQ, topic exchange, DLX, retry queues | +| Data | PostgreSQL, Redis | +| Observability | Prometheus, Grafana, OpenTelemetry, Tempo, Loki, Alloy, Alertmanager | +| Infrastructure | Docker Compose, Nginx | -## Problema +## Problem -Sistemas orientados a eventos precisam publicar mensagens com segurança, processar cargas assíncronas sem duplicidade e oferecer uma forma clara de investigar falhas. O Relay simula esse cenário com uma arquitetura próxima de produção, separando API, publicação confiável, workers por domínio e ferramentas de operação. +Event-driven systems must publish messages reliably, process asynchronous workloads without duplicate side effects, and provide a clear path for failure investigation. Relay models this scenario with a production-oriented architecture that separates the API, reliable publishing, domain workers, and operational tooling. -## Funcionalidades +## Features -- Criação e listagem de eventos. -- Publicação confiável com Transactional Outbox. -- Processamento assíncrono por domínio. -- Retry com backoff progressivo. -- Dead Letter Queue com inspeção e reprocessamento manual. -- Consumers idempotentes. -- Correlação por `correlation_id` e `trace_id`. -- Dashboard web com visão operacional, filtros, paginação, detalhes de evento e operação da DLQ. -- Autenticação JWT simples para a interface operacional. -- CI com validação de backend, frontend e Docker Compose. -- Métricas, logs, tracing distribuído e alertas. +- Event creation and listing. +- Reliable publishing through the Transactional Outbox pattern. +- Domain-specific asynchronous processing. +- Progressive retry backoff. +- Dead Letter Queue inspection and manual reprocessing. +- Idempotent consumers. +- Correlation through `correlation_id` and `trace_id`. +- Operational web dashboard with filters, pagination, event details, and DLQ operations. +- JWT authentication for the operational interface. +- CI validation for backend, frontend, and Docker Compose. +- Metrics, centralized logs, distributed tracing, and alerting. -## Arquitetura +## Architecture ```text API -> PostgreSQL + Outbox -> Outbox Publisher -> RabbitMQ -> Workers -> Retry/DLQ -> Observability Stack @@ -41,16 +41,16 @@ API -> PostgreSQL + Outbox -> Outbox Publisher -> RabbitMQ -> Workers -> Retry/D ![Relay Architecture](assets/architecture.png) -## Como Executar +## Running Locally ```bash cp .env.example .env docker compose up --build ``` -URLs principais: +Main URLs: -- Aplicação: http://localhost +- Application: http://localhost - API: http://localhost:8000 - Health check: http://localhost/health - RabbitMQ Management: http://localhost:15672 @@ -58,45 +58,45 @@ URLs principais: - Alertmanager: http://localhost:9093 - Grafana: http://localhost:3000 -Credenciais locais padrão: +Default local credentials: -- Aplicação: `admin` / `relay_admin` +- Application: `admin` / `relay_admin` - RabbitMQ: `relay` / `relay_dev_password` - Grafana: `relay` / `relay_dev_password` ## Endpoints -| Método | Endpoint | Descrição | +| Method | Endpoint | Description | | --- | --- | --- | -| `GET` | `/health` | Health check da API | -| `POST` | `/api/auth/login` | Autentica o operador | -| `GET` | `/api/auth/me` | Retorna usuário autenticado | -| `POST` | `/api/events` | Cria um evento | -| `GET` | `/api/events` | Lista eventos recentes | -| `GET` | `/api/events/summary` | Resume eventos por status e DLQ | -| `GET` | `/api/events/{id}` | Detalha evento, tentativas, logs e DLQ | -| `GET` | `/api/dead-letter-events` | Lista eventos em DLQ | -| `GET` | `/api/dead-letter-events/{id}` | Detalha um evento em DLQ | -| `POST` | `/api/dead-letter-events/{id}/reprocess` | Reprocessa um evento morto | +| `GET` | `/health` | Checks API health | +| `POST` | `/api/auth/login` | Authenticates the operator | +| `GET` | `/api/auth/me` | Returns the authenticated user | +| `POST` | `/api/events` | Creates an event | +| `GET` | `/api/events` | Lists recent events | +| `GET` | `/api/events/summary` | Aggregates events by status and DLQ state | +| `GET` | `/api/events/{id}` | Returns event details, attempts, logs, and DLQ records | +| `GET` | `/api/dead-letter-events` | Lists DLQ events | +| `GET` | `/api/dead-letter-events/{id}` | Returns DLQ event details | +| `POST` | `/api/dead-letter-events/{id}/reprocess` | Reprocesses a dead-letter event | ## Dashboard ![Relay Dashboard](assets/dashboard.png) -## Estrutura +## Project Structure ```text -backend/ API, modelos, serviços, workers e instrumentação -frontend/ Dashboard operacional em React -infra/ Nginx, Prometheus, Grafana, Loki, Tempo, Alloy e Alertmanager -docs/ Documentação técnica complementar -assets/ Imagens usadas na documentação +backend/ API, models, services, workers, and instrumentation +frontend/ React operational dashboard +infra/ Nginx, Prometheus, Grafana, Loki, Tempo, Alloy, and Alertmanager +docs/ Additional technical documentation +assets/ Documentation images ``` -## Documentação +## Documentation -| Documento | Conteúdo | +| Document | Coverage | | --- | --- | -| [Arquitetura](docs/architecture.md) | Fluxo do sistema, RabbitMQ, Outbox, retry, DLQ, idempotência e workers | -| [Observabilidade](docs/observability.md) | Prometheus, Grafana, métricas, logs, tracing e alertas | -| [API](docs/api.md) | Endpoints, payloads, respostas e reprocessamento | +| [Architecture](docs/architecture.md) | System flow, RabbitMQ, Outbox, retry, DLQ, idempotency, and workers | +| [Observability](docs/observability.md) | Prometheus, Grafana, metrics, logs, tracing, and alerts | +| [API](docs/api.md) | Endpoints, payloads, responses, and reprocessing | From 55c648ee56a5dd4f1e86a237fa9f97e336f36b2d Mon Sep 17 00:00:00 2001 From: Matheus Freire | Backend & Infra Date: Fri, 10 Jul 2026 07:19:32 -0300 Subject: [PATCH 2/4] Translate API documentation to English --- docs/api.md | 211 +++++++++++++--------------------------------------- 1 file changed, 50 insertions(+), 161 deletions(-) diff --git a/docs/api.md b/docs/api.md index 383a22f..fe88c5b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,14 +1,12 @@ # API -A API usa o prefixo configurável `API_V1_PREFIX`, com padrão `/api`. +The API uses the configurable `API_V1_PREFIX`, which defaults to `/api`. ## Health ### `GET /health` -Retorna o status básico da API. - -Resposta esperada: +Returns the API's basic status. ```json { @@ -16,9 +14,9 @@ Resposta esperada: } ``` -## Autenticação +## Authentication -As rotas operacionais em `/api/events` e `/api/dead-letter-events` exigem JWT no header: +Operational routes under `/api/events` and `/api/dead-letter-events` require a JWT: ```http Authorization: Bearer ACCESS_TOKEN @@ -26,7 +24,7 @@ Authorization: Bearer ACCESS_TOKEN ### `POST /api/auth/login` -Autentica o usuário administrativo local. +Authenticates the local administrative user. Request: @@ -47,15 +45,11 @@ Response `200`: } ``` -Possível erro: - -- `401`: credenciais inválidas. +Possible error: `401` for invalid credentials. ### `GET /api/auth/me` -Retorna o usuário autenticado. - -Response `200`: +Returns the authenticated user. ```json { @@ -63,15 +57,13 @@ Response `200`: } ``` -Possível erro: +Possible error: `401` for a missing, invalid, or expired token. -- `401`: token ausente, inválido ou expirado. - -## Eventos +## Events ### `POST /api/events` -Cria e persiste um evento, registrando a mensagem correspondente em `outbox_messages`. A publicação na exchange `relay.events` é feita pelo `relay-outbox-publisher`. +Creates and persists an event and writes the corresponding message to `outbox_messages`. The `relay-outbox-publisher` publishes it to the `relay.events` exchange. Request: @@ -87,13 +79,13 @@ Request: } ``` -Campos: +Fields: -- `event_type`: string obrigatória, entre 1 e 120 caracteres. -- `payload`: objeto JSON obrigatório. -- `routing_key`: string opcional, até 120 caracteres. Se ausente, o backend usa `events.created`. -- `correlation_id`: string opcional, até 120 caracteres. -- `trace_id`: string opcional, até 120 caracteres. +- `event_type`: required string from 1 through 120 characters; +- `payload`: required JSON object; +- `routing_key`: optional string up to 120 characters; defaults to `events.created`; +- `correlation_id`: optional string up to 120 characters; +- `trace_id`: optional string up to 120 characters. Response `201`: @@ -113,20 +105,16 @@ Response `201`: } ``` -Possível erro: +Possible errors: -- `401`: token ausente, inválido ou expirado. -- `503`: evento armazenado, mas publicação direta/legada para RabbitMQ falhou. No fluxo atual, a publicação confiável ocorre pela outbox. +- `401`: missing, invalid, or expired token; +- `503`: the event was stored, but the legacy direct RabbitMQ publishing path failed. The current reliable flow publishes through the Outbox. ### `GET /api/events` -Lista eventos recentes para alimentar o dashboard operacional. - -Query params: - -- `limit`: inteiro entre 1 e 100. Padrão: `25`. +Lists recent events for the operational dashboard. -Response: +Query parameter: `limit`, an integer from 1 through 100; default `25`. ```json [ @@ -146,15 +134,11 @@ Response: ] ``` -Possível erro: - -- `401`: token ausente, inválido ou expirado. +Possible error: `401` for a missing, invalid, or expired token. ### `GET /api/events/summary` -Retorna uma visão agregada para o dashboard operacional. - -Response: +Returns aggregated data for the operational dashboard. ```json { @@ -174,87 +158,30 @@ Response: } ``` -Quando não houver eventos em DLQ, `oldest_dead_letter_age_seconds` retorna `null`. - -Possível erro: - -- `401`: token ausente, inválido ou expirado. +When the DLQ is empty, `oldest_dead_letter_age_seconds` is `null`. ### `GET /api/events/{id}` -Mostra detalhes de um evento, incluindo payload, tentativas, logs e registros relacionados de DLQ. +Returns an event with its payload, processing attempts, logs, and related DLQ records. -Response: +The response includes the event fields plus: -```json -{ - "id": "event-id", - "event_type": "customer.created", - "payload": { - "customer_id": "123" - }, - "routing_key": "events.created", - "correlation_id": "correlation-id", - "trace_id": "trace-id", - "status": "dead_letter", - "created_at": "2026-01-01T12:00:00Z", - "updated_at": "2026-01-01T12:10:00Z", - "attempts": [ - { - "id": "attempt-id", - "event_id": "event-id", - "attempt_number": 1, - "status": "failed", - "error_message": "handler error", - "started_at": "2026-01-01T12:00:01Z", - "finished_at": "2026-01-01T12:00:02Z" - } - ], - "logs": [ - { - "id": "log-id", - "event_id": "event-id", - "level": "error", - "message": "Event failed", - "log_metadata": { - "reason": "handler error" - }, - "created_at": "2026-01-01T12:00:02Z" - } - ], - "dead_letter_entries": [ - { - "id": "dead-letter-id", - "event_id": "event-id", - "reason": "max_retries_exceeded", - "payload": { - "customer_id": "123" - }, - "retry_count": 3, - "original_routing_key": "events.created", - "error_message": "handler error", - "created_at": "2026-01-01T12:10:00Z" - } - ] -} -``` +- `attempts`: attempt number, state, error, and start/finish timestamps; +- `logs`: level, message, metadata, and timestamp; +- `dead_letter_entries`: reason, payload, retry count, original routing key, and error. -Possível erro: +Possible errors: -- `401`: token ausente, inválido ou expirado. -- `404`: evento não encontrado. +- `401`: missing, invalid, or expired token; +- `404`: event not found. ## Dead Letter Events ### `GET /api/dead-letter-events` -Lista eventos que foram enviados para DLQ. - -Query params: +Lists events sent to the DLQ. -- `limit`: inteiro entre 1 e 100. Padrão: `50`. - -Response: +Query parameter: `limit`, an integer from 1 through 100; default `50`. ```json [ @@ -273,56 +200,18 @@ Response: ] ``` -Possível erro: - -- `401`: token ausente, inválido ou expirado. - ### `GET /api/dead-letter-events/{id}` -Mostra detalhes operacionais do evento morto, incluindo payload, evento original, tentativas e logs. - -Response: - -```json -{ - "id": "dead-letter-id", - "event_id": "event-id", - "reason": "max_retries_exceeded", - "payload": { - "customer_id": "123" - }, - "retry_count": 3, - "original_routing_key": "events.created", - "error_message": "handler error", - "created_at": "2026-01-01T12:10:00Z", - "event": { - "id": "event-id", - "event_type": "customer.created", - "payload": { - "customer_id": "123" - }, - "routing_key": "events.created", - "correlation_id": "correlation-id", - "trace_id": "trace-id", - "status": "dead_letter", - "created_at": "2026-01-01T12:00:00Z", - "updated_at": "2026-01-01T12:10:00Z" - }, - "attempts": [], - "logs": [] -} -``` +Returns operational details for a dead-letter event, including its payload, original event, attempts, and logs. -Possível erro: +Possible errors: -- `401`: token ausente, inválido ou expirado. -- `404`: evento de DLQ não encontrado. +- `401`: missing, invalid, or expired token; +- `404`: DLQ event not found. ### `POST /api/dead-letter-events/{id}/reprocess` -Republica o evento original na exchange principal usando a routing key original. O Relay preserva `correlation_id` e `trace_id`, atualiza o evento original para `queued` e registra a ação operacional. - -Response: +Republishes the original event to the main exchange with its original routing key. Relay preserves `correlation_id` and `trace_id`, moves the original event back to `queued`, and records the operational action. ```json { @@ -335,17 +224,17 @@ Response: } ``` -Possíveis erros: +Possible errors: -- `401`: token ausente, inválido ou expirado. -- `404`: evento de DLQ não encontrado. -- `409`: reprocessamento bloqueado por regra de segurança operacional. -- `503`: evento não foi republicado. +- `401`: missing, invalid, or expired token; +- `404`: DLQ event not found; +- `409`: reprocessing blocked by an operational safety rule; +- `503`: event could not be republished. -## Semântica de Reprocessamento +## Reprocessing Semantics -- O evento original é reutilizado. -- Nenhum novo `Event` é criado. -- A routing key vem de `original_routing_key`, com fallback para `event.routing_key`. -- `correlation_id` e `trace_id` são preservados. -- A operação não substitui idempotência real no handler. +- The original event is reused. +- No new `Event` is created. +- The routing key comes from `original_routing_key`, falling back to `event.routing_key`. +- `correlation_id` and `trace_id` are preserved. +- Reprocessing does not replace real handler idempotency. From b51d1bd984caa430801e715daa8962fe20fef67e Mon Sep 17 00:00:00 2001 From: Matheus Freire | Backend & Infra Date: Fri, 10 Jul 2026 07:20:57 -0300 Subject: [PATCH 3/4] Translate architecture documentation to English --- docs/architecture.md | 344 +++++++++++++++---------------------------- 1 file changed, 121 insertions(+), 223 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index c7f08ff..13864ca 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,8 +1,8 @@ -# Arquitetura +# Architecture -O Relay é uma plataforma full stack de processamento assíncrono de eventos. A API recebe eventos, persiste o estado no PostgreSQL e registra uma mensagem em `outbox_messages` na mesma transação. Um publisher separado publica mensagens persistentes na exchange topic `relay.events`, e workers independentes consomem filas por domínio. +Relay is a full-stack asynchronous event-processing platform. The API receives events, persists their state in PostgreSQL, and writes a message to `outbox_messages` in the same transaction. A separate publisher sends persistent messages to the `relay.events` topic exchange, and independent domain workers consume their respective queues. -## Fluxo Completo +## End-to-End Flow ```text Client @@ -13,147 +13,98 @@ Client -> relay.events.audit | relay.events.analytics | relay.events.notifications -> domain workers -> retry queues or relay.events.dead_letter - -> PostgreSQL attempts, logs and DLQ records - -> Prometheus, Grafana, Loki, Tempo and Alertmanager + -> PostgreSQL attempts, logs, and DLQ records + -> Prometheus, Grafana, Loki, Tempo, and Alertmanager ``` ![Relay Architecture](../assets/architecture.png) -Cada evento recebe: +Every event carries: -- `correlation_id`: correlaciona eventos de um mesmo fluxo de negócio. Pode vir da API ou ser gerado automaticamente. -- `trace_id`: rastreia o ciclo de processamento. Pode vir da API, do contexto OpenTelemetry ativo ou ser gerado automaticamente. +- `correlation_id`: correlates events in the same business flow; accepted from the API or generated automatically; +- `trace_id`: tracks the processing lifecycle; accepted from the API, derived from the active OpenTelemetry context, or generated automatically. -Ambos são salvos no banco, registrados na outbox e enviados para o RabbitMQ. +Both identifiers are stored in PostgreSQL, included in the Outbox message, and propagated through RabbitMQ. -## Componentes +## Components -| Componente | Responsabilidade | +| Component | Responsibility | | --- | --- | -| FastAPI backend | Recebe eventos, consulta estado operacional e expõe métricas | -| PostgreSQL | Persiste eventos, outbox, tentativas, logs, DLQ e estados de idempotência | -| `relay-outbox-publisher` | Publica mensagens da outbox na exchange principal | -| RabbitMQ | Roteia eventos por domínio com exchange topic, DLX e filas de retry | -| Workers | Processam mensagens por domínio e aplicam retry, DLQ e idempotência | -| Redis | Cache/infra auxiliar da aplicação | -| React frontend | Exibe eventos e operação da DLQ | -| Observability stack | Prometheus, Grafana, Loki, Tempo, OpenTelemetry, Alloy e Alertmanager | +| FastAPI backend | Receives events, exposes operational state, and publishes metrics | +| PostgreSQL | Stores events, Outbox messages, attempts, logs, DLQ records, and idempotency state | +| `relay-outbox-publisher` | Publishes pending Outbox messages to the main exchange | +| RabbitMQ | Routes domain events with a topic exchange, DLX, and retry queues | +| Workers | Process domain messages and enforce retry, DLQ, and idempotency rules | +| Redis | Auxiliary cache and infrastructure | +| React frontend | Provides event and DLQ operations | +| Observability stack | Prometheus, Grafana, Loki, Tempo, OpenTelemetry, Alloy, and Alertmanager | -## Configuração Principal - -Variáveis de ambiente relevantes para a arquitetura local: - -```env -PROJECT_NAME=Relay -ENVIRONMENT=development -API_V1_PREFIX=/api -DATABASE_URL=postgresql+psycopg://relay:relay_dev_password@postgres:5432/relay -RABBITMQ_DEFAULT_USER=relay -RABBITMQ_DEFAULT_PASS=relay_dev_password -RABBITMQ_HOST=rabbitmq -RABBITMQ_PORT=5672 -RABBITMQ_USER=relay -RABBITMQ_PASSWORD=relay_dev_password -RABBITMQ_VHOST=/ -RABBITMQ_EXCHANGE=relay.events -RABBITMQ_DLX=relay.events.dlx -RABBITMQ_AUDIT_QUEUE=relay.events.audit -RABBITMQ_ANALYTICS_QUEUE=relay.events.analytics -RABBITMQ_NOTIFICATIONS_QUEUE=relay.events.notifications -RABBITMQ_DEAD_LETTER_QUEUE=relay.events.dead_letter -RABBITMQ_RETRY_QUEUE_10S=relay.events.retry.10s -RABBITMQ_RETRY_QUEUE_30S=relay.events.retry.30s -RABBITMQ_RETRY_QUEUE_5M=relay.events.retry.5m -RABBITMQ_MAX_RETRIES=3 -REDIS_URL=redis://redis:6379/0 -BACKEND_CORS_ORIGINS=http://localhost,http://localhost:5173,http://127.0.0.1:5173 -``` - -## RabbitMQ +## RabbitMQ Topology Exchanges: -- Principal: `relay.events` -- Dead Letter Exchange: `relay.events.dlx` -- Tipo: `topic` -- Duráveis -- Mensagens persistentes - -Filas de domínio: +- main topic exchange: `relay.events`; +- Dead Letter Exchange: `relay.events.dlx`; +- both durable, with persistent messages. -- `relay.events.audit` -- `relay.events.analytics` -- `relay.events.notifications` +Domain queues: -Fila de dead letter: +- `relay.events.audit`; +- `relay.events.analytics`; +- `relay.events.notifications`. -- `relay.events.dead_letter` +Retry queues: -Filas de retry: +- `relay.events.retry.10s`; +- `relay.events.retry.30s`; +- `relay.events.retry.5m`. -- `relay.events.retry.10s` -- `relay.events.retry.30s` -- `relay.events.retry.5m` +Dead-letter queue: -Bindings principais: +- `relay.events.dead_letter`. -- `audit.*` e `events.*` -> `relay.events.audit` -- `analytics.*` -> `relay.events.analytics` -- `notifications.*` -> `relay.events.notifications` -- `retry.*` -> filas de domínio para liberação de mensagens após TTL +Main bindings: -Bindings na DLX: +- `audit.*` and `events.*` -> `relay.events.audit`; +- `analytics.*` -> `relay.events.analytics`; +- `notifications.*` -> `relay.events.notifications`; +- `retry.*` -> domain queues after the retry TTL expires. -- `retry.10s` -> `relay.events.retry.10s` -- `retry.30s` -> `relay.events.retry.30s` -- `retry.5m` -> `relay.events.retry.5m` -- `dead_letter.#` -> `relay.events.dead_letter` +DLX bindings: -Exemplos de routing keys: +- `retry.10s` -> `relay.events.retry.10s`; +- `retry.30s` -> `relay.events.retry.30s`; +- `retry.5m` -> `relay.events.retry.5m`; +- `dead_letter.#` -> `relay.events.dead_letter`. -- `events.created` -- `audit.created` -- `analytics.page_viewed` -- `notifications.email_requested` +Example routing keys include `events.created`, `audit.created`, `analytics.page_viewed`, and `notifications.email_requested`. -## Outbox Pattern +## Transactional Outbox -O Relay usa Transactional Outbox para garantir que a criação do evento e o registro da mensagem a ser publicada aconteçam na mesma transação de banco. A API não publica diretamente no RabbitMQ. +Relay uses the Transactional Outbox pattern so event creation and publication intent are committed atomically. The API never publishes directly to RabbitMQ in the current flow. ```text -API -> PostgreSQL events + outbox_messages -> relay-outbox-publisher -> RabbitMQ relay.events -> workers +API -> PostgreSQL events + outbox_messages -> relay-outbox-publisher -> RabbitMQ -> workers ``` -Na criação de um evento, o backend persiste: - -- `events` -- `event_processing_states` -- `outbox_messages` - -O `relay-outbox-publisher` lê mensagens pendentes, adquire lock transacional, publica na exchange `relay.events` e atualiza o status da outbox. +A single event-creation transaction persists: -Tabela: +- `events`; +- `event_processing_states`; +- `outbox_messages`. -- `outbox_messages` +Outbox states: -Estados: +- `pending`: stored and waiting for publication; +- `publishing`: locked by a publisher; +- `published`: successfully sent to RabbitMQ; +- `failed`: publication failed and a retry was scheduled. -- `pending`: mensagem registrada no banco e aguardando publicação. -- `publishing`: publisher adquiriu lock e está tentando publicar. -- `published`: mensagem publicada no RabbitMQ. -- `failed`: tentativa de publicação falhou e a mensagem ficou agendada para retry. +The publisher reads batches, acquires transactional locks, publishes to `relay.events`, and updates Outbox state. It uses `SELECT ... FOR UPDATE SKIP LOCKED` where supported, allowing multiple publishers without processing the same row concurrently. -Garantias: +Failure handling records `attempt_count`, `last_error`, `last_attempt_at`, and `next_attempt_at`. Messages left in `publishing` after a publisher crash become eligible for recovery after `OUTBOX_PUBLISHING_TIMEOUT_SECONDS`. -- O `Event`, o estado inicial de idempotência e a `outbox_messages` são criados na mesma transação. -- A API não publica diretamente no RabbitMQ. -- O publisher usa lock transacional com `SELECT ... FOR UPDATE SKIP LOCKED` quando suportado pelo banco. -- Se dois publishers rodarem ao mesmo tempo, apenas um deve adquirir a mensagem. -- Se o RabbitMQ estiver indisponível, a mensagem fica `failed`, com `attempt_count`, `last_error`, `last_attempt_at` e `next_attempt_at`. -- Se o publisher cair durante `publishing`, mensagens antigas voltam para retry após `OUTBOX_PUBLISHING_TIMEOUT_SECONDS`. -- Se a publicação for duplicada por crash entre RabbitMQ e commit no banco, a idempotência dos consumers protege o handler contra efeito colateral duplicado dentro do Relay. - -Configuração: +Relevant configuration: ```env OUTBOX_PUBLISHER_NAME=relay-outbox-publisher @@ -164,159 +115,109 @@ OUTBOX_BACKOFF_SECONDS=5,30,120,300 OUTBOX_METRICS_PORT=9101 ``` -Serviço Docker Compose: - -- `relay-outbox-publisher` - -Riscos e limitações: - -- A outbox fornece publicação confiável, mas não elimina totalmente publicação duplicada em caso de crash depois da publicação e antes do commit do status `published`. -- A proteção contra duplicidade depende dos consumers idempotentes. -- Efeitos colaterais externos ainda precisam receber uma chave idempotente, normalmente `event_id`. -- Múltiplas réplicas do publisher exigem observabilidade cuidadosa dos locks, mensagens presas e retries. +The Outbox guarantees reliable publication intent, but not exactly-once delivery. A crash after RabbitMQ accepts a message and before PostgreSQL commits `published` may cause duplicate publication. Idempotent consumers protect Relay from duplicate handler execution; external side effects must also receive an idempotency key, normally `event_id`. ## Workers -O Docker Compose executa consumers independentes por domínio. Todos usam o mesmo código base de consumo resiliente, mas cada processo consome uma fila específica e aplica seu próprio conjunto de routing keys aceitas. - -| Serviço | Fila | Routing keys | Entry point | +| Service | Source | Accepted routing keys | Entry point | | --- | --- | --- | --- | | `relay-outbox-publisher` | `outbox_messages` | N/A | `python -m app.workers.outbox_publisher` | | `relay-audit-worker` | `relay.events.audit` | `events.*`, `audit.*` | `python -m app.workers.audit_consumer` | | `relay-analytics-worker` | `relay.events.analytics` | `analytics.*` | `python -m app.workers.analytics_consumer` | | `relay-notification-worker` | `relay.events.notifications` | `notifications.*` | `python -m app.workers.notification_consumer` | -Variáveis por worker: - -- `WORKER_NAME`: nome lógico do worker. -- `WORKER_QUEUE`: fila consumida pelo processo. -- `WORKER_ROUTING_KEY`: padrões de routing key aceitos pelo worker. +Per-worker variables: -Os handlers continuam separados por domínio: +- `WORKER_NAME`; +- `WORKER_QUEUE`; +- `WORKER_ROUTING_KEY`. -- `audit_worker` -- `analytics_worker` -- `notification_worker` +Domain handlers remain isolated as `audit_worker`, `analytics_worker`, and `notification_worker`. -Para escalar um domínio específico com Docker Compose: +Workers can scale independently: ```bash docker compose up --scale relay-analytics-worker=3 docker compose up --scale relay-audit-worker=2 --scale relay-notification-worker=2 ``` -As mensagens liberadas pelas filas de retry usam `retry.*` para voltar à exchange principal. Como as filas de retry são compartilhadas, os workers verificam `x-original-routing-key` antes de processar; apenas o worker compatível com a routing key original executa o handler. - -## Retry e Backoff - -O worker não usa `basic_nack(requeue=True)` para retry, evitando loop infinito na fila principal. Quando uma tentativa falha, a mensagem original é publicada novamente na Dead Letter Exchange com o header RabbitMQ `x-retry-count` e a routing key de retry adequada. Depois disso, a mensagem original recebe `basic_ack`. +Shared retry queues return messages through `retry.*`. Workers inspect `x-original-routing-key`, and only the compatible worker invokes the handler. -Fluxo de falha: +## Retry and Backoff -- Falha 1: publica em `relay.events.retry.10s` com `x-retry-count=1`. -- Falha 2: publica em `relay.events.retry.30s` com `x-retry-count=2`. -- Falha 3: publica em `relay.events.retry.5m` com `x-retry-count=3`. -- Falha 4: publica na DLQ `relay.events.dead_letter`, marca o evento como `dead_letter` e registra em `dead_letter_events`. +Workers avoid `basic_nack(requeue=True)`, preventing tight loops in the main queue. On failure, the original message is republished to the DLX with `x-retry-count` and the appropriate retry routing key, then acknowledged. -As filas de retry possuem TTL: +Failure progression: -- `relay.events.retry.10s`: 10 segundos. -- `relay.events.retry.30s`: 30 segundos. -- `relay.events.retry.5m`: 5 minutos. +1. first failure -> `relay.events.retry.10s`, `x-retry-count=1`; +2. second failure -> `relay.events.retry.30s`, `x-retry-count=2`; +3. third failure -> `relay.events.retry.5m`, `x-retry-count=3`; +4. fourth failure -> `relay.events.dead_letter`, event state `dead_letter`, and a `dead_letter_events` record. -Quando o TTL expira, a mensagem volta para a exchange principal `relay.events`. O header `x-original-routing-key` preserva a routing key original do evento para que o worker correto resolva o handler mesmo quando a mensagem retorna a partir de uma fila de retry. - -Retry é uma tentativa controlada de reprocessar uma falha recuperável após um atraso progressivo. DLQ é o destino final de mensagens que excederam o limite de tentativas e precisam de análise ou reprocessamento manual. +After each TTL expires, RabbitMQ returns the message to `relay.events`. The `x-original-routing-key` header preserves domain routing. ## Dead Letter Queue -A DLQ operacional é a área usada para investigar mensagens que chegaram ao fim do ciclo automático de retry. O Relay mantém o histórico no PostgreSQL e expõe endpoints para listar, detalhar e reprocessar eventos mortos sem apagar registros. - -Endpoints: - -- `GET /api/dead-letter-events`: lista eventos em DLQ com motivo, erro, retry count, routing key original, `correlation_id`, `trace_id` e status do evento original. -- `GET /api/dead-letter-events/{id}`: retorna payload, evento original, tentativas e logs relacionados. -- `POST /api/dead-letter-events/{id}/reprocess`: republica o evento existente na exchange principal `relay.events`. - -O reprocessamento manual usa `original_routing_key`, preserva `correlation_id` e `trace_id`, atualiza o evento original para `queued` e registra um `EventLog` com a ação operacional. O Relay não cria outro `Event` para essa operação. +The operational DLQ stores messages that exhausted automated retries. PostgreSQL retains the history, and the API supports: -Fluxo operacional recomendado: +- `GET /api/dead-letter-events`: list DLQ events with failure and correlation data; +- `GET /api/dead-letter-events/{id}`: inspect payload, original event, attempts, and logs; +- `POST /api/dead-letter-events/{id}/reprocess`: republish the existing event. -1. Abrir a área de Dead Letter Queue no dashboard. -2. Verificar erro, payload, tentativas e logs. -3. Corrigir a causa raiz quando necessário. -4. Clicar em `Reprocessar`. -5. Acompanhar o evento voltar para `queued` e ser consumido pelo worker do domínio. +Manual reprocessing preserves `correlation_id`, `trace_id`, and `original_routing_key`, moves the original event to `queued`, and creates an operational `EventLog`. It does not create a second `Event`. -Riscos de reprocessamento: +Recommended operation: -- O handler pode executar efeitos colaterais novamente se ainda não for idempotente. -- O erro original pode continuar acontecendo se a causa raiz não foi corrigida. -- O backend bloqueia reprocessamentos manuais repetidos em uma janela curta para reduzir cliques duplicados, mas isso não substitui idempotência real nos consumers. +1. inspect the DLQ record, payload, attempts, and logs; +2. correct the root cause; +3. request reprocessing; +4. track the event from `queued` through its domain worker. -## Idempotência +Repeated reprocessing is blocked for a short window, but this safeguard does not replace handler idempotency. -Os workers usam controle persistente de idempotência para evitar que o mesmo `event_id` execute o handler mais de uma vez com efeito colateral duplicado dentro do Relay. +## Idempotency -Tabela: +Workers use `event_processing_states` to prevent the same `event_id` from executing a handler more than once inside Relay. -- `event_processing_states` +States: -Estados persistidos: +- `pending`: created but not processed; +- `processing`: locked by a worker; +- `processed`: completed; duplicates are acknowledged and skipped; +- `failed`: failed and eligible for retry; +- `dead_lettered`: retries exhausted. -- `pending`: evento criado e ainda não processado. -- `processing`: worker adquiriu o lock e está executando o handler. -- `processed`: handler concluiu com sucesso; mensagens duplicadas são ignoradas. -- `failed`: handler falhou antes de concluir; retries podem tentar novamente. -- `dead_lettered`: evento excedeu o limite de retry e foi enviado para DLQ. +Processing algorithm: -Estratégia: - -1. A API cria o `Event` e também cria `event_processing_states` com status `pending`. -2. Ao consumir uma mensagem, o worker abre uma transação e bloqueia a linha de idempotência com `SELECT ... FOR UPDATE`. -3. Se o estado já for `processed` ou `dead_lettered`, a mensagem é considerada duplicada ou terminal, um log é registrado e a mensagem original recebe `basic_ack`. -4. Se o estado for `processing` e ainda não estiver expirado, o worker não executa o handler, registra falha de lock/idempotência e faz `basic_ack`. -5. Se o estado for `pending`, `failed` ou `processing` expirado, o worker marca como `processing`, registra a tentativa e executa o handler dentro da transação. -6. Em sucesso, o estado vira `processed`. -7. Em erro, o estado vira `failed` e o fluxo decide retry ou DLQ. -8. Ao exceder retries, o estado vira `dead_lettered`. - -Configuração: +1. begin a transaction and lock the event state with `SELECT ... FOR UPDATE`; +2. skip and acknowledge `processed` or `dead_lettered` events; +3. skip active `processing` states that have not timed out; +4. acquire `pending`, `failed`, or stale `processing` states; +5. record the attempt and run the handler; +6. commit `processed` on success; +7. commit `failed` and choose retry or DLQ on error. ```env IDEMPOTENCY_PROCESSING_TIMEOUT_SECONDS=900 ``` -Esse modelo protege contra: - -- Mensagem duplicada no RabbitMQ. -- Retry de evento já processado. -- Dois workers tentando processar o mesmo `event_id`. -- Reentrega depois de timeout ou conexão perdida. -- Worker crash antes do commit final. -- Publicação duplicada pela outbox após crash entre RabbitMQ e commit no banco. - -Limitação importante: idempotência no consumer impede execução duplicada do handler dentro do Relay. Para efeitos colaterais externos, como envio de email ou chamada a API de terceiros, o destino externo também deve receber uma chave idempotente, normalmente o próprio `event_id`. +This protects against RabbitMQ redelivery, retry of processed events, concurrent workers, lost connections, worker crashes before commit, and duplicate Outbox publication. External systems must still implement idempotency for their own side effects. -## Status +## Status Values -Status de evento centralizados em `backend/app/core/enums.py`: +Event states in `backend/app/core/enums.py`: -- `received` -- `queued` -- `processing` -- `processed` -- `failed` -- `dead_letter` -- `publish_failed` +- `received`; +- `queued`; +- `processing`; +- `processed`; +- `failed`; +- `dead_letter`; +- `publish_failed`. -Níveis de log padronizados: +Standard log levels: `info`, `warning`, and `error`. -- `info` -- `warning` -- `error` - -## Estrutura Interna +## Project Structure ```text backend/ @@ -351,27 +252,24 @@ infra/ tempo/ ``` -## Desenvolvimento Local sem Docker +## Local Development without Docker -Use Python 3.12 para manter o ambiente local alinhado ao `backend/Dockerfile`. +Use Python 3.12 to match `backend/Dockerfile`. Backend: ```bash cd backend python -m venv .venv +# Windows .venv\Scripts\activate +# Linux/macOS +source .venv/bin/activate pip install -r requirements.txt alembic upgrade head uvicorn app.main:app --reload ``` -Em Linux/macOS, a ativação do virtualenv normalmente usa: - -```bash -source .venv/bin/activate -``` - Frontend: ```bash @@ -380,4 +278,4 @@ npm install npm run dev ``` -Para execução sem Docker, ajuste `DATABASE_URL`, `RABBITMQ_*` e `REDIS_URL` conforme sua máquina. +For local execution without Docker, set `DATABASE_URL`, `RABBITMQ_*`, and `REDIS_URL` for the local environment. From 442c6443f8fa09883f30a9ed0c39306854989775 Mon Sep 17 00:00:00 2001 From: Matheus Freire | Backend & Infra Date: Fri, 10 Jul 2026 07:22:00 -0300 Subject: [PATCH 4/4] Translate observability documentation to English --- docs/observability.md | 494 ++++++++++++++---------------------------- 1 file changed, 157 insertions(+), 337 deletions(-) diff --git a/docs/observability.md b/docs/observability.md index b9ad444..62f939b 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -1,78 +1,75 @@ -# Observabilidade +# Observability -O Relay inclui uma stack local para observar API, publisher, workers, RabbitMQ e infraestrutura de apoio. +Relay includes a local stack for observing the API, Outbox publisher, domain workers, RabbitMQ, and supporting infrastructure. ## Stack -- Prometheus: coleta métricas da API, do `relay-outbox-publisher`, dos workers e do RabbitMQ Exporter. -- Grafana: dashboards versionados para aplicação, RabbitMQ, tracing, logs, alertas, outbox e idempotência. -- RabbitMQ Exporter: coleta métricas reais do broker pela API de Management do RabbitMQ. -- Loki: armazena logs localmente. -- Alloy: descobre containers Docker, coleta stdout/stderr e envia logs para Loki. -- OpenTelemetry Collector: recebe traces OTLP. -- Tempo: armazena traces localmente e permite consulta pelo Grafana. -- Alertmanager: recebe alertas do Prometheus e roteia por severidade. +- **Prometheus:** scrapes the API, `relay-outbox-publisher`, workers, and RabbitMQ Exporter. +- **Grafana:** provides version-controlled dashboards for application metrics, RabbitMQ, tracing, logs, alerts, Outbox, and idempotency. +- **RabbitMQ Exporter:** reads broker metrics through the RabbitMQ Management API. +- **Loki:** stores centralized logs locally. +- **Alloy:** discovers Docker containers, collects stdout/stderr, and sends records to Loki. +- **OpenTelemetry Collector:** receives OTLP traces. +- **Tempo:** stores traces and makes them queryable from Grafana. +- **Alertmanager:** receives Prometheus alerts and routes them by severity. -## URLs Locais +## Local Endpoints - Prometheus: http://localhost:9090 - Alertmanager: http://localhost:9093 - Grafana: http://localhost:3000 -- Métricas diretas da API: http://localhost:8000/metrics +- API metrics: http://localhost:8000/metrics - RabbitMQ Exporter: http://localhost:9419/metrics - Tempo: http://localhost:3200 -- OpenTelemetry Collector OTLP gRPC: `localhost:4317` -- OpenTelemetry Collector OTLP HTTP: `localhost:4318` +- OTLP gRPC: `localhost:4317` +- OTLP HTTP: `localhost:4318` - Loki: http://localhost:3100 - Alloy UI: http://localhost:12345 -Credenciais locais do Grafana: - -- Usuário: `relay` -- Senha: `relay_dev_password` - -Essas credenciais são apenas para desenvolvimento local e podem ser alteradas via `GRAFANA_ADMIN_USER` e `GRAFANA_ADMIN_PASSWORD`. +Default local Grafana credentials are `relay` / `relay_dev_password`. They are development-only and can be changed with `GRAFANA_ADMIN_USER` and `GRAFANA_ADMIN_PASSWORD`. ## Prometheus -O Relay expõe métricas reais no endpoint: - -- `GET /metrics` - -O Docker Compose inclui um serviço `prometheus` configurado para coletar métricas do backend em `backend:8000/metrics`, do publisher em `relay-outbox-publisher:9101/metrics` e dos workers em `relay-*-worker:9102/metrics`. - -Métricas principais: - -- `relay_events_created_total`: eventos recebidos pela API. -- `relay_events_published_total`: eventos publicados no RabbitMQ. -- `relay_event_publish_failures_total`: falhas ao publicar no RabbitMQ. -- `relay_event_creation_duration_seconds`: tempo de criação do evento e registro na outbox. -- `relay_worker_events_processed_total`: eventos processados com sucesso por worker. -- `relay_worker_events_failed_total`: falhas de processamento por worker. -- `relay_worker_events_retried_total`: eventos enviados para retry. -- `relay_worker_events_dead_lettered_total`: eventos enviados para DLQ. -- `relay_worker_events_total`: transições de status registradas pelos workers. -- `relay_worker_events_duplicate_skipped_total`: mensagens ignoradas por idempotência. -- `relay_idempotency_lock_failures_total`: falhas ao adquirir lock de processamento. -- `relay_idempotency_stale_processing_events_total`: eventos presos em `processing` além do timeout. -- `relay_outbox_messages_pending_total`: mensagens na outbox aguardando publicação. -- `relay_outbox_messages_failed_total`: mensagens na outbox aguardando retry após falha. -- `relay_outbox_messages_published_total`: mensagens da outbox publicadas. -- `relay_outbox_publish_failures_total`: falhas de publicação da outbox. -- `relay_outbox_messages_stuck_publishing_total`: mensagens presas em `publishing`. -- `relay_outbox_oldest_pending_age_seconds`: idade da mensagem pendente ou falhada mais antiga. -- `relay_outbox_retries_total`: retries agendados pela outbox. -- `relay_event_processing_duration_seconds`: duração do processamento nos workers. -- `relay_dead_letter_events_total`: quantidade atual de eventos na DLQ. -- `relay_dead_letter_oldest_event_age_seconds`: idade aproximada do evento mais antigo na DLQ. -- `relay_dead_letter_reprocess_total`: reprocessamentos manuais solicitados. -- `relay_dead_letter_reprocess_failures_total`: falhas ou bloqueios de reprocessamento manual. -- `relay_events_by_status`: quantidade atual de eventos por status. -- `relay_event_attempts_by_status`: quantidade atual de tentativas por status. - -As métricas de API e reprocessamento manual são incrementadas diretamente no fluxo de código. As métricas atuais de status, tentativas e DLQ são calculadas a partir do PostgreSQL no momento do scrape. As métricas de worker são instrumentadas nos processos consumidores. - -Exemplos de PromQL: +Relay exposes metrics at `GET /metrics`. Docker Compose scrapes: + +- backend at `backend:8000/metrics`; +- publisher at `relay-outbox-publisher:9101/metrics`; +- workers at `relay-*-worker:9102/metrics`; +- RabbitMQ Exporter at `rabbitmq-exporter:9419/metrics`. + +### Main Application Metrics + +| Metric | Meaning | +| --- | --- | +| `relay_events_created_total` | Events accepted by the API | +| `relay_events_published_total` | Events published to RabbitMQ | +| `relay_event_publish_failures_total` | RabbitMQ publishing failures | +| `relay_event_creation_duration_seconds` | Event and Outbox creation duration | +| `relay_worker_events_processed_total` | Successfully processed events by worker | +| `relay_worker_events_failed_total` | Worker processing failures | +| `relay_worker_events_retried_total` | Events routed to retry queues | +| `relay_worker_events_dead_lettered_total` | Events sent to the DLQ | +| `relay_worker_events_duplicate_skipped_total` | Messages skipped by idempotency | +| `relay_idempotency_lock_failures_total` | Processing-lock acquisition failures | +| `relay_idempotency_stale_processing_events_total` | Events exceeding the processing timeout | +| `relay_outbox_messages_pending_total` | Outbox messages waiting for publication | +| `relay_outbox_messages_failed_total` | Failed Outbox messages waiting for retry | +| `relay_outbox_messages_published_total` | Published Outbox messages | +| `relay_outbox_publish_failures_total` | Outbox publication failures | +| `relay_outbox_messages_stuck_publishing_total` | Messages stuck in `publishing` | +| `relay_outbox_oldest_pending_age_seconds` | Age of the oldest pending or failed message | +| `relay_outbox_retries_total` | Outbox retries scheduled | +| `relay_event_processing_duration_seconds` | Worker processing duration | +| `relay_dead_letter_events_total` | Current DLQ size | +| `relay_dead_letter_oldest_event_age_seconds` | Age of the oldest DLQ event | +| `relay_dead_letter_reprocess_total` | Manual reprocessing requests | +| `relay_dead_letter_reprocess_failures_total` | Failed or blocked reprocessing requests | +| `relay_events_by_status` | Current events by status | +| `relay_event_attempts_by_status` | Current attempts by status | + +API and manual-reprocessing counters are incremented in application flows. Current status, attempt, DLQ, Outbox, and idempotency gauges are derived from PostgreSQL at scrape time. Worker metrics are emitted by consumer processes. + +Example PromQL: ```promql sum(rate(relay_events_created_total[5m])) @@ -82,72 +79,33 @@ sum(rate(relay_worker_events_retried_total[5m])) by (retry_queue) relay_dead_letter_events_total relay_dead_letter_oldest_event_age_seconds relay_outbox_messages_pending_total -relay_outbox_messages_failed_total relay_outbox_messages_stuck_publishing_total relay_outbox_oldest_pending_age_seconds -sum(rate(relay_outbox_publish_failures_total[5m])) by (routing_key) -sum(rate(relay_dead_letter_reprocess_total[15m])) by (routing_key) histogram_quantile(0.95, sum(rate(relay_event_processing_duration_seconds_bucket[5m])) by (le, routing_key)) ``` ## Grafana -Datasource Prometheus: - -- Nome: `Prometheus` -- URL interna: `http://prometheus:9090` -- Default: `true` - -Dashboards versionados: - -- `infra/grafana/dashboards/relay-observability.json`: `Relay Observability` -- `infra/grafana/dashboards/relay-rabbitmq.json`: `Relay RabbitMQ` -- `infra/grafana/dashboards/relay-tracing.json`: `Relay Tracing` -- `infra/grafana/dashboards/relay-logs.json`: `Relay Logs` -- `infra/grafana/dashboards/relay-alerts.json`: `Relay Alerts` -- `infra/grafana/dashboards/relay-outbox-idempotency.json`: `Relay Outbox & Idempotency` - -Painéis do dashboard `Relay Observability`: - -- Eventos criados por minuto. -- Eventos publicados por routing key. -- Falhas de publicação. -- Eventos processados. -- Eventos com falha. -- Retries por fila. -- Eventos enviados para DLQ. -- Total atual de eventos em DLQ. -- Idade do evento mais antigo em DLQ. -- Reprocessamentos manuais. -- p95 de tempo de processamento. -- Status dos eventos. - -Painéis do dashboard `Relay RabbitMQ`: - -- Mensagens prontas por fila. -- Mensagens não confirmadas por fila. -- Consumidores por fila. -- Taxa de publicação. -- Taxa de entrega com ack. -- Mensagens em DLQ. -- Mensagens em retry queues. -- Estado das filas principais. -- Conexões. -- Canais. -- Memória usada por node. -- Mensagens totais por fila. - -Para verificar DLQ e retry queues pelo Grafana, abra o dashboard `Relay RabbitMQ` e acompanhe os painéis `Mensagens em DLQ`, `Mensagens em retry queues`, `Mensagens prontas por fila` e `Mensagens não confirmadas por fila`. +Provisioned dashboards: + +- `Relay Observability`: throughput, publishing failures, processing, retries, DLQ, reprocessing, p95 latency, and event states; +- `Relay RabbitMQ`: ready and unacknowledged messages, consumers, publish/delivery rate, DLQ, retry queues, connections, channels, and memory; +- `Relay Tracing`: trace volume, average latency, p95/p99, endpoint errors, operation duration, and recent traces; +- `Relay Logs`: log volume, errors, recent records, and correlation filters; +- `Relay Alerts`: active application and infrastructure alerts; +- `Relay Outbox & Idempotency`: publisher health, Outbox state, duplicate skips, lock failures, and stale processing. + +Dashboard files are stored under `infra/grafana/dashboards/`. ## RabbitMQ Exporter -- Serviço Docker Compose: `rabbitmq-exporter` -- Imagem: `kbudde/rabbitmq-exporter:1.0.0` -- URL interna do RabbitMQ: `http://rabbitmq:15672` -- Endpoint de métricas local: http://localhost:9419/metrics -- Job Prometheus: `rabbitmq-exporter` +- Compose service: `rabbitmq-exporter`; +- image: `kbudde/rabbitmq-exporter:1.0.0`; +- internal RabbitMQ URL: `http://rabbitmq:15672`; +- local metrics endpoint: http://localhost:9419/metrics; +- Prometheus job: `rabbitmq-exporter`. -Exemplos de PromQL para RabbitMQ: +Useful metrics: ```promql rabbitmq_queue_messages_ready @@ -156,36 +114,26 @@ rabbitmq_queue_consumers sum(rate(rabbitmq_queue_messages_published_total[5m])) by (queue) sum(rate(rabbitmq_queue_messages_delivered_total[5m])) by (queue) rabbitmq_queue_messages{queue="relay.events.dead_letter"} -rabbitmq_queue_messages{queue=~"relay\\.events\\.retry\\..*"} -rabbitmq_queue_messages{queue=~"relay\\.events\\.(audit|analytics|notifications)"} -rabbitmq_queue_state{queue=~"relay\\.events\\.(audit|analytics|notifications)"} +rabbitmq_queue_messages{queue=~"relay\.events\.retry\..*"} rabbitmq_connections rabbitmq_channels rabbitmq_node_mem_used ``` -## Tracing - -O Relay possui tracing distribuído com OpenTelemetry, OpenTelemetry Collector e Grafana Tempo. +## Distributed Tracing ```text -FastAPI / Workers -> OTLP gRPC -> OpenTelemetry Collector -> Grafana Tempo -> Grafana +FastAPI / Workers -> OTLP gRPC -> OpenTelemetry Collector -> Tempo -> Grafana ``` -Serviços Docker Compose: - -- `otel-collector`: recebe traces OTLP em `4317` gRPC e `4318` HTTP. -- `tempo`: armazena traces localmente e expõe consulta HTTP em `3200`. -- `grafana`: provisiona o datasource `Tempo` automaticamente. +Instrumentation: -Instrumentações oficiais usadas no backend: +- `opentelemetry-instrumentation-fastapi`; +- `opentelemetry-instrumentation-sqlalchemy`; +- `opentelemetry-instrumentation-pika`; +- `opentelemetry-exporter-otlp-proto-grpc`. -- `opentelemetry-instrumentation-fastapi`: gera spans para requisições HTTP da API. -- `opentelemetry-instrumentation-sqlalchemy`: gera spans para operações no PostgreSQL via SQLAlchemy. -- `opentelemetry-instrumentation-pika`: instrumenta publicação e consumo via RabbitMQ quando suportado pela biblioteca. -- `opentelemetry-exporter-otlp-proto-grpc`: exporta spans para o Collector via OTLP gRPC. - -Variáveis principais: +Main configuration: ```env OTEL_ENABLED=true @@ -196,116 +144,44 @@ OTEL_TRACES_SAMPLER=always_on OTEL_RESOURCE_ATTRIBUTES=service.namespace=relay,deployment.environment=development ``` -Os workers usam `OTEL_SERVICE_NAME` próprio no Docker Compose: - -- `relay-audit-worker` -- `relay-analytics-worker` -- `relay-notification-worker` - -O `trace_id` de negócio existente é preservado. Quando um evento novo chega sem `trace_id`, o backend usa o trace id OpenTelemetry atual, se houver contexto ativo; caso contrário, gera um UUID como fallback. O `correlation_id` continua independente. - -Datasource Tempo: - -- Nome: `Tempo` -- UID: `tempo` -- URL interna: `http://tempo:3200` - -Painéis do dashboard `Relay Tracing`: +Workers use their own service names: `relay-audit-worker`, `relay-analytics-worker`, and `relay-notification-worker`. -- Quantidade de traces. -- Latência média. -- P95 e P99. -- Erros por endpoint. -- Tempo gasto por operação. -- Traces recentes. +The business `trace_id` is preserved. If an incoming event has no `trace_id`, the backend uses the active OpenTelemetry trace ID when available or generates a UUID. `correlation_id` remains independent. -Exemplos de TraceQL Metrics: +Example TraceQL Metrics: ```traceql { resource.service.namespace = "relay" } | rate() { resource.service.namespace = "relay" } | avg_over_time(duration) { resource.service.namespace = "relay" } | quantile_over_time(duration, .95) -{ resource.service.namespace = "relay" } | quantile_over_time(duration, .99) { resource.service.name = "relay-backend" && status = error } | rate() by (span.http.route) -{ resource.service.namespace = "relay" } | avg_over_time(duration) by (span:name) ``` -Limitações atuais: +Tempo uses local storage and has no adaptive or tail sampling. Dashboards require actual ingested traces. -- O ambiente usa armazenamento local do Tempo, adequado para desenvolvimento. -- Não há sampling adaptativo nem tail sampling nesta etapa. -- O dashboard usa TraceQL Metrics; ele depende de traces reais ingeridos no Tempo para exibir dados. - -## Logs - -O Relay possui uma stack local de logs centralizados com Grafana Loki e Grafana Alloy. +## Centralized Logs ```text -Containers Docker -> Grafana Alloy -> Grafana Loki -> Grafana +Docker containers -> Alloy -> Loki -> Grafana ``` -Serviços Docker Compose: - -- `loki`: armazena logs localmente e expõe API em `3100`. -- `alloy`: descobre containers Docker pelo socket `/var/run/docker.sock`, coleta stdout/stderr e envia para Loki. -- `grafana`: provisiona o datasource `Loki` automaticamente. - -Serviços coletados: - -- `backend` -- `relay-audit-worker` -- `relay-analytics-worker` -- `relay-notification-worker` -- `postgres` -- `rabbitmq` -- `redis` -- `frontend` -- `nginx` -- `prometheus` -- `rabbitmq-exporter` -- `tempo` -- `otel-collector` -- `grafana` -- `loki` -- `alloy` - -Logs estruturados do backend: - -O backend escreve logs JSON em stdout. Os campos principais são: - -- `timestamp` -- `level` -- `service` -- `environment` -- `trace_id` -- `span_id` -- `correlation_id` -- `event_id` -- `endpoint` -- `logger` -- `message` - -O `trace_id` e o `span_id` são extraídos do contexto OpenTelemetry ativo. O `correlation_id`, `event_id` e `endpoint` aparecem quando o fluxo possui esses dados. - -Datasource Loki: - -- Nome: `Loki` -- UID: `loki` -- URL interna: `http://loki:3100` -- Derived field: `TraceID` -- Integração: ao encontrar `trace_id` no log, o Grafana cria um link para abrir o trace correspondente no datasource `Tempo`. - -Painéis do dashboard `Relay Logs`: - -- Volume de logs por serviço. -- Erros por serviço. -- Logs recentes. -- Logs filtráveis por `trace_id`. -- Logs filtráveis por `correlation_id`. -- Logs do worker por `event_id`. -- Logs relacionados a DLQ, retry e falhas de publicação. - -Exemplos de LogQL: +Alloy discovers containers through `/var/run/docker.sock` and collects application and infrastructure logs. The backend writes JSON to stdout with: + +- `timestamp`; +- `level`; +- `service`; +- `environment`; +- `trace_id`; +- `span_id`; +- `correlation_id`; +- `event_id`; +- `endpoint`; +- `logger`; +- `message`. + +Grafana's Loki data source defines a `TraceID` derived field so a log containing `trace_id` can open the corresponding Tempo trace. + +Example LogQL: ```logql {job="relay/docker"} @@ -314,144 +190,88 @@ Exemplos de LogQL: {job="relay/docker"} | json | correlation_id="CORRELATION_ID" {job="relay/docker", service=~"relay-.*-worker"} | json | event_id="EVENT_ID" {job="relay/docker"} |~ "(?i)(dlq|dead letter|retry|publish failed|failed to publish)" -sum by (service) (count_over_time({job="relay/docker"}[5m])) -sum by (service) (count_over_time({job="relay/docker", level=~"error|critical"}[5m])) ``` -Limitações atuais: - -- Loki usa armazenamento local, adequado para desenvolvimento. -- `trace_id`, `correlation_id` e `event_id` ficam no JSON do log, não como labels, para evitar alta cardinalidade. -- Logs de serviços de terceiros podem não ser JSON; ainda assim são coletados por container e serviço. - -## Alertas - -O Prometheus carrega regras versionadas a partir de `infra/prometheus/rules/relay-alerts.yml` e envia alertas para o Alertmanager. O Alertmanager possui configuração versionada com rotas por severidade e receivers webhook genéricos. - -Arquivos de configuração: - -- Regras Prometheus: `infra/prometheus/rules/relay-alerts.yml` -- Configuração Prometheus: `infra/prometheus/prometheus.yml` -- Configuração Alertmanager: `infra/alertmanager/alertmanager.yml` - -Datasource Grafana: +High-cardinality identifiers stay inside the JSON payload rather than becoming Loki labels. Third-party logs may not be JSON but remain searchable by container and service. -- Nome: `Alertmanager` -- UID: `alertmanager` -- URL interna: `http://alertmanager:9093` -- Implementação: `prometheus` +## Alerts -Alertas de aplicação: +Prometheus loads version-controlled rules from `infra/prometheus/rules/relay-alerts.yml` and sends them to Alertmanager. -- `RelayDeadLetterQueueHasEvents` (`warning`): existe pelo menos um evento em DLQ por mais de 5 minutos. -- `RelayDeadLetterQueueGrowing` (`critical`): a quantidade de eventos em DLQ aumentou nos últimos 10 minutos. -- `RelayOldDeadLetterEvent` (`warning`): o evento mais antigo na DLQ passou de 30 minutos. -- `RelayHighRetryRate` (`warning`): workers estão enviando muitos eventos para retry. -- `RelayHighWorkerFailureRate` (`warning`): workers estão falhando eventos em taxa elevada. -- `RelayEventPublishFailures` (`critical`): houve falha ao publicar eventos no RabbitMQ. +### Application and DLQ -Alertas de RabbitMQ e scrape: +- `RelayDeadLetterQueueHasEvents` (`warning`); +- `RelayDeadLetterQueueGrowing` (`critical`); +- `RelayOldDeadLetterEvent` (`warning`); +- `RelayHighRetryRate` (`warning`); +- `RelayHighWorkerFailureRate` (`warning`); +- `RelayEventPublishFailures` (`critical`). -- `RelayQueueWithoutConsumers` (`critical`): fila principal sem consumidores. -- `RelayQueueBacklogGrowing` (`warning`): mensagens prontas crescendo em filas principais. -- `RelayHighUnackedMessages` (`warning`): mensagens não confirmadas acima do limite. -- `RelayRetryQueueBacklog` (`warning`): filas de retry acumulando mensagens. -- `RelayRabbitMQExporterDown` (`critical`): Prometheus não consegue coletar o RabbitMQ Exporter. -- `RelayBackendMetricsDown` (`critical`): Prometheus não consegue coletar `/metrics` do backend. +### RabbitMQ and Scraping -Alertas de Outbox: +- `RelayQueueWithoutConsumers` (`critical`); +- `RelayQueueBacklogGrowing` (`warning`); +- `RelayHighUnackedMessages` (`warning`); +- `RelayRetryQueueBacklog` (`warning`); +- `RelayRabbitMQExporterDown` (`critical`); +- `RelayBackendMetricsDown` (`critical`). -- `RelayOutboxPendingTooLong` (`warning`): a mensagem pendente ou falhada mais antiga ficou tempo demais sem publicação. -- `RelayOutboxPublishFailures` (`critical`): houve falha real de publicação da outbox no RabbitMQ. -- `RelayOutboxHighRetryRate` (`warning`): o publisher está reagendando retries em taxa elevada. -- `RelayOutboxStuckPublishing` (`critical`): existe mensagem presa em `publishing` além do timeout configurado. -- `RelayOutboxPublisherDown` (`critical`): Prometheus não consegue coletar o endpoint `/metrics` do `relay-outbox-publisher`. +### Outbox -Alertas de idempotência: +- `RelayOutboxPendingTooLong` (`warning`); +- `RelayOutboxPublishFailures` (`critical`); +- `RelayOutboxHighRetryRate` (`warning`); +- `RelayOutboxStuckPublishing` (`critical`); +- `RelayOutboxPublisherDown` (`critical`). -- `RelayDuplicateEventsSkipped` (`info`): workers ignoraram mensagens duplicadas ou já terminais. -- `RelayIdempotencyLockFailures` (`warning`): workers não conseguiram adquirir o lock de processamento. -- `RelayStaleProcessingEvents` (`critical`): existe evento preso em `processing` além de `IDEMPOTENCY_PROCESSING_TIMEOUT_SECONDS`. +### Idempotency -Métricas usadas: +- `RelayDuplicateEventsSkipped` (`info`); +- `RelayIdempotencyLockFailures` (`warning`); +- `RelayStaleProcessingEvents` (`critical`). -- Gauges do Relay: `relay_dead_letter_events_total`, `relay_dead_letter_oldest_event_age_seconds`. -- Gauges de Outbox e Idempotência: `relay_outbox_messages_pending_total`, `relay_outbox_messages_failed_total`, `relay_outbox_messages_stuck_publishing_total`, `relay_outbox_oldest_pending_age_seconds`, `relay_idempotency_stale_processing_events_total`. -- Counters do Relay: `relay_worker_events_retried_total`, `relay_worker_events_failed_total`, `relay_event_publish_failures_total`, `relay_outbox_publish_failures_total`, `relay_outbox_retries_total`, `relay_worker_events_duplicate_skipped_total`, `relay_idempotency_lock_failures_total`. -- Gauges do RabbitMQ Exporter: `rabbitmq_queue_consumers`, `rabbitmq_queue_messages_ready`, `rabbitmq_queue_messages_unacknowledged`, `rabbitmq_queue_messages`. -- Métrica nativa do Prometheus: `up`. +Rules use `rate` and `increase` for counters and `max_over_time` or `min_over_time` for gauges. -As regras usam `rate` e `increase` apenas para counters. Para gauges, como contadores atuais de filas, idade e estados presos, as regras usam `max_over_time` e `min_over_time`. +Severity routing: -Investigação de incidentes de Outbox: +- `critical` -> `webhook-critical`; +- `warning` -> `webhook-warning`; +- `info` -> `webhook-info`. -1. Abrir o dashboard `Relay Outbox & Idempotency` e verificar `Outbox publisher health`, `Outbox pending`, `Outbox failed`, `Stuck publishing` e `Oldest pending outbox age`. -2. Consultar os logs no dashboard `Relay Logs` filtrando `service="relay-outbox-publisher"` e, se disponível, `outbox_message_id`, `event_id`, `correlation_id` ou `trace_id`. -3. Conferir a tabela `outbox_messages` para `status`, `attempt_count`, `last_error`, `last_attempt_at`, `next_attempt_at`, `locked_by` e `locked_at`. -4. Se o alerta for de publicação, verificar RabbitMQ, exchange `relay.events`, conectividade e credenciais. -5. Se o alerta for de `publishing` travado, verificar se o publisher caiu durante publicação e se a recuperação automática marcou a mensagem como `failed` para retry. +The configured endpoints under `host.docker.internal:8080/alerts/*` are generic local-development receivers; no real secrets are stored in the repository. -Investigação de incidentes de Idempotência: +## Incident Investigation -1. Abrir o dashboard `Relay Outbox & Idempotency` e verificar `Duplicate skipped rate`, `Idempotency lock failures rate` e `Stale processing events`. -2. Consultar logs dos workers filtrando `event_id`, `correlation_id` ou `trace_id`. -3. Conferir `event_processing_states` para `status`, `processing_started_at`, `worker_name`, `attempt_count` e timestamps. -4. Para duplicidades, confirmar se houve redelivery do RabbitMQ, retry manual ou publicação duplicada pela outbox após crash. -5. Para eventos presos em `processing`, validar timeout, estado do worker responsável e possíveis falhas parciais no handler. +For Outbox incidents: -Rotas de notificação: +1. inspect publisher health, pending, failed, stuck, and oldest-message panels; +2. query publisher logs by `outbox_message_id`, `event_id`, `correlation_id`, or `trace_id`; +3. inspect `outbox_messages` state, attempts, errors, schedule, and lock data; +4. validate RabbitMQ connectivity, credentials, and the `relay.events` exchange; +5. for stuck publication, confirm publisher recovery moved stale messages to `failed`. -- `severity="critical"` -> receiver `webhook-critical` -- `severity="warning"` -> receiver `webhook-warning` -- `severity="info"` -> receiver `webhook-info` +For idempotency incidents: -Receivers webhook: +1. inspect duplicate-skip, lock-failure, and stale-processing panels; +2. query worker logs by event, correlation, or trace ID; +3. inspect `event_processing_states`, worker ownership, attempts, and timestamps; +4. identify RabbitMQ redelivery, manual retry, or duplicate Outbox publication; +5. validate the responsible worker and any partial handler failure. -- `webhook-critical`: `http://host.docker.internal:8080/alerts/critical` -- `webhook-warning`: `http://host.docker.internal:8080/alerts/warning` -- `webhook-info`: `http://host.docker.internal:8080/alerts/info` - -Esses endpoints são genéricos e próprios para desenvolvimento local. Não há secrets reais no repositório. - -Também é possível acompanhar os alertas em: - -- Prometheus: http://localhost:9090/alerts -- Alertmanager: http://localhost:9093 -- Grafana: dashboard `Relay Alerts` - -Limitação atual: o Prometheus coleta o endpoint `/metrics` do backend, do `relay-outbox-publisher` e dos workers. Como cada worker expõe métricas no próprio processo, escalar vários containers do mesmo serviço exige atenção aos targets efetivos do Prometheus. As métricas derivadas do PostgreSQL, como status, DLQ, outbox e idempotência, aparecem pelo scrape do backend. - -Próximos passos de observabilidade: - -- Avaliar service discovery mais robusto para múltiplas réplicas de workers. -- Conectar receivers reais como Slack, email, Discord, PagerDuty ou webhook corporativo. -- Versionar dashboards adicionais para RabbitMQ e infraestrutura. -- Criar alertas baseados em logs do Loki para erros críticos e falhas operacionais. - -## Validações - -Validar regras Prometheus: +## Validation ```bash docker compose run --rm --entrypoint promtool prometheus check rules /etc/prometheus/rules/relay-alerts.yml -``` - -Validar Alertmanager: - -```bash docker compose run --rm --entrypoint amtool alertmanager check-config /etc/alertmanager/alertmanager.yml -``` - -Validar OpenTelemetry Collector e Tempo: - -```bash docker compose run --rm --entrypoint /otelcol-contrib otel-collector validate --config=/etc/otelcol-contrib/config.yml docker compose run --rm --entrypoint /tempo tempo --config.file=/etc/tempo/tempo.yml --config.verify=true -``` - -Validar Loki e Alloy: - -```bash docker compose run --rm --entrypoint /usr/bin/loki loki --config.file=/etc/loki/loki.yml --verify-config=true docker compose run --rm --entrypoint /bin/alloy alloy validate /etc/alloy/config.alloy ``` + +## Current Limitations and Next Steps + +- Tempo and Loki use local development storage. +- Scaling several containers of the same worker service requires Prometheus target discovery to include every replica. +- PostgreSQL-derived gauges are exposed through the backend scrape. +- Production evolution should add robust service discovery, real notification receivers, additional infrastructure dashboards, and Loki-based alert rules.