From cb72816c2fcf6476fb98de31cd17d609b1108198 Mon Sep 17 00:00:00 2001 From: Alexandro Hervis Date: Sun, 6 Apr 2025 11:58:35 -0300 Subject: [PATCH 1/2] Inserindo Instrumentacao com GoLang --- learning-otel-go/.env.example | 14 ++ learning-otel-go/LICENSE | 28 +++ learning-otel-go/README.md | 165 ++++++++++++++++++ learning-otel-go/cmd/migrate/main.go | 29 +++ .../000001_create_tasks_table.down.sql | 1 + .../000001_create_tasks_table.up.sql | 6 + learning-otel-go/docker-compose.yml | 35 ++++ learning-otel-go/go.mod | 33 ++++ learning-otel-go/go.sum | 59 +++++++ learning-otel-go/internal/config/env.go | 55 ++++++ .../internal/core/task/mock_repository.go | 43 +++++ learning-otel-go/internal/core/task/model.go | 8 + .../internal/core/task/pg_repository.go | 113 ++++++++++++ .../internal/core/task/repository.go | 8 + .../internal/core/task/service.go | 83 +++++++++ .../internal/core/task/service_test.go | 57 ++++++ .../internal/handler/task_handler.go | 78 +++++++++ .../internal/handler/task_handler_test.go | 126 +++++++++++++ learning-otel-go/internal/router/router.go | 30 ++++ .../internal/telemetry/middleware.go | 75 ++++++++ .../internal/telemetry/tracing.go | 74 ++++++++ learning-otel-go/main.go | 102 +++++++++++ 22 files changed, 1222 insertions(+) create mode 100644 learning-otel-go/.env.example create mode 100644 learning-otel-go/LICENSE create mode 100644 learning-otel-go/README.md create mode 100644 learning-otel-go/cmd/migrate/main.go create mode 100644 learning-otel-go/db/migrations/000001_create_tasks_table.down.sql create mode 100644 learning-otel-go/db/migrations/000001_create_tasks_table.up.sql create mode 100644 learning-otel-go/docker-compose.yml create mode 100644 learning-otel-go/go.mod create mode 100644 learning-otel-go/go.sum create mode 100644 learning-otel-go/internal/config/env.go create mode 100644 learning-otel-go/internal/core/task/mock_repository.go create mode 100644 learning-otel-go/internal/core/task/model.go create mode 100644 learning-otel-go/internal/core/task/pg_repository.go create mode 100644 learning-otel-go/internal/core/task/repository.go create mode 100644 learning-otel-go/internal/core/task/service.go create mode 100644 learning-otel-go/internal/core/task/service_test.go create mode 100644 learning-otel-go/internal/handler/task_handler.go create mode 100644 learning-otel-go/internal/handler/task_handler_test.go create mode 100644 learning-otel-go/internal/router/router.go create mode 100644 learning-otel-go/internal/telemetry/middleware.go create mode 100644 learning-otel-go/internal/telemetry/tracing.go create mode 100644 learning-otel-go/main.go diff --git a/learning-otel-go/.env.example b/learning-otel-go/.env.example new file mode 100644 index 0000000..23dd34e --- /dev/null +++ b/learning-otel-go/.env.example @@ -0,0 +1,14 @@ +# Configurações do servidor +PORT=8080 +ENV=development + +# Configurações do banco de dados +DB_HOST=localhost +DB_PORT=5432 +DB_USER=admin +DB_PASS=admin123 +DB_NAME=golearn_db + +# Outras configurações +LOG_LEVEL=info +API_KEY=sua_chave_secreta_aqui \ No newline at end of file diff --git a/learning-otel-go/LICENSE b/learning-otel-go/LICENSE new file mode 100644 index 0000000..d03742c --- /dev/null +++ b/learning-otel-go/LICENSE @@ -0,0 +1,28 @@ +The MIT License (MIT) + +Original Work +Copyright (c) 2016 Matthias Kadenbach +https://github.com/mattes/migrate + +Modified Work +Copyright (c) 2018 Dale Hui +https://github.com/golang-migrate/migrate + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/learning-otel-go/README.md b/learning-otel-go/README.md new file mode 100644 index 0000000..e9fd233 --- /dev/null +++ b/learning-otel-go/README.md @@ -0,0 +1,165 @@ +# API de Tarefas com OpenTelemetry em Go + +Este projeto é um exemplo prático de como implementar telemetria usando OpenTelemetry em uma aplicação Go. A aplicação consiste em uma API de gerenciamento de tarefas (todo list) com uma arquitetura em camadas e persistência em PostgreSQL. + +## Estrutura do Projeto + +O projeto segue uma arquitetura em camadas: + +``` +todo-api/ +├── cmd/ +│ └── migrate/ # Ferramenta para migrações do banco de dados +├── db/ +│ └── migrations/ # Migrações SQL +├── internal/ +│ ├── config/ # Configurações da aplicação +│ ├── core/ +│ │ └── task/ # Domínio e regras de negócio +│ ├── handler/ # Handlers HTTP +│ ├── router/ # Configuração de rotas +│ └── telemetry/ # Configuração OpenTelemetry +└── main.go # Ponto de entrada da aplicação +``` + +## Tecnologias Utilizadas + +### Bibliotecas Principais +- **gorilla/mux**: Router HTTP +- **lib/pq**: Driver PostgreSQL para Go +- **golang-migrate/migrate**: Migrações de banco de dados +- **google/uuid**: Geração de identificadores únicos +- **joho/godotenv**: Carregamento de variáveis de ambiente + +### OpenTelemetry +- **go.opentelemetry.io/otel**: API principal do OpenTelemetry +- **go.opentelemetry.io/otel/trace**: API de traces +- **go.opentelemetry.io/otel/sdk**: Implementação do SDK +- **go.opentelemetry.io/otel/exporters/otlp**: Exportadores para OTLP +- **go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc**: Exportador gRPC + +## Funcionalidades + +A API implementa operações CRUD para tarefas: + +- **GET /tasks**: Lista todas as tarefas +- **POST /task**: Cria uma nova tarefa +- **PUT /task/{id}**: Atualiza uma tarefa existente +- **DELETE /task/{id}**: Remove uma tarefa +- **GET /health**: Endpoint de verificação de saúde + +## Implementação do OpenTelemetry + +A telemetria foi implementada em várias camadas da aplicação: + +### 1. Inicialização (internal/telemetry/tracing.go) + +Configuração do provedor de traces, exportador OTLP e propagadores: + +```go +func InitTracer(serviceName string, collectorURL string) (func(context.Context) error, error) { + // Configuração do tracer, exportador e propagadores + // ... +} +``` + +### 2. Middleware HTTP (internal/telemetry/middleware.go) + +Instrumenta todas as requisições HTTP: + +```go +func TracingMiddleware(next http.Handler) http.Handler { + // Criação de spans para cada requisição HTTP + // Propagação de contexto + // Captura de status code e headers + // ... +} +``` + +### 3. Camada de Serviço (internal/core/task/service.go) + +Cada método de serviço cria spans próprios: + +```go +func (s *TaskService) CreateTask(title, description string, concluded bool) error { + // Criação de span específico para esta operação + // Adição de atributos e metadados + // ... +} +``` + +### 4. Repositório (internal/core/task/pg_repository.go) + +Operações de banco de dados também são rastreadas: + +```go +func (r *PgTaskRepository) GetAll() ([]Task, error) { + // Span para operação de banco + // Atributos como tipo de operação SQL + // Métricas como número de linhas retornadas + // ... +} +``` + +## Configuração do Ambiente + +O projeto usa Docker Compose para configurar: + +1. **PostgreSQL**: Banco de dados para armazenar as tarefas +2. **Jaeger**: Backend para visualização de traces + +```yaml +version: '3.8' +services: + postgres: + # Configuração do PostgreSQL + + jaeger: + # Configuração do Jaeger All-in-One + # Expõe UI na porta 16686 +``` + +## Como Executar + +1. Clone o repositório + ``` + git clone + cd todo-api + ``` + +2. Inicie os serviços + ``` + docker-compose up -d + ``` + +3. Execute as migrações + ``` + go run cmd/migrate/main.go + ``` + +4. Inicie a aplicação + ``` + go run main.go + ``` + +5. Visualize os traces em http://localhost:16686 + +## Observabilidade + +A implementação de OpenTelemetry permite: + +- **Rastreamento distribuído**: Acompanhe operações em todos os componentes +- **Métricas detalhadas**: Número de requisições, duração, erros +- **Diagnóstico de problemas**: Identifique gargalos de performance +- **Correlação de eventos**: Associe cada operação a um identificador único + +## Benefícios do OpenTelemetry + +- **Padrão aberto**: Não há lock-in de fornecedor +- **Instrumentação consistente**: Mesmo padrão em diferentes linguagens +- **Extensibilidade**: Suporte a múltiplos backends (Jaeger, Zipkin, etc.) +- **Baixo overhead**: Impacto mínimo na performance + +## Contribuindo + +Contribuições são bem-vindas! Este projeto serve como referência para implementação de telemetria em aplicações Go. diff --git a/learning-otel-go/cmd/migrate/main.go b/learning-otel-go/cmd/migrate/main.go new file mode 100644 index 0000000..fd58935 --- /dev/null +++ b/learning-otel-go/cmd/migrate/main.go @@ -0,0 +1,29 @@ +// cmd/migrate/main.go +package main + +import ( + "fmt" + "log" + "todo-api/internal/config" + + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" +) + +func main() { + cfg := config.LoadConfig() + + dbURL := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", cfg.Database.User, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name) + + m, err := migrate.New("file://db/migrations", dbURL) + if err != nil { + log.Fatal(err) + } + + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + log.Fatal(err) + } + + log.Println("Migrations aplicadas com sucesso!") +} diff --git a/learning-otel-go/db/migrations/000001_create_tasks_table.down.sql b/learning-otel-go/db/migrations/000001_create_tasks_table.down.sql new file mode 100644 index 0000000..2ff1380 --- /dev/null +++ b/learning-otel-go/db/migrations/000001_create_tasks_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS tasks; diff --git a/learning-otel-go/db/migrations/000001_create_tasks_table.up.sql b/learning-otel-go/db/migrations/000001_create_tasks_table.up.sql new file mode 100644 index 0000000..c3e4000 --- /dev/null +++ b/learning-otel-go/db/migrations/000001_create_tasks_table.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE tasks ( + id UUID PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + concluded BOOLEAN NOT NULL DEFAULT FALSE +); \ No newline at end of file diff --git a/learning-otel-go/docker-compose.yml b/learning-otel-go/docker-compose.yml new file mode 100644 index 0000000..cd8b17e --- /dev/null +++ b/learning-otel-go/docker-compose.yml @@ -0,0 +1,35 @@ +version: "3.8" + +services: + postgres: + image: postgres:latest + container_name: golearn_postgres + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: golearn_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + restart: unless-stopped + + jaeger: + image: jaegertracing/all-in-one:latest + container_name: golearn_jaeger + ports: + - "6831:6831/udp" + - "6832:6832/udp" + - "5778:5778" + - "16686:16686" + - "4317:4317" + - "4318:4318" + - "14250:14250" + - "14268:14268" + - "14269:14269" + environment: + - COLLECTOR_OTLP_ENABLED=true + restart: unless-stopped + +volumes: + postgres_data: diff --git a/learning-otel-go/go.mod b/learning-otel-go/go.mod new file mode 100644 index 0000000..02192d0 --- /dev/null +++ b/learning-otel-go/go.mod @@ -0,0 +1,33 @@ +module todo-api + +go 1.24.2 + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-migrate/migrate/v4 v4.18.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/lib/pq v1.10.9 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/grpc v1.71.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect +) diff --git a/learning-otel-go/go.sum b/learning-otel-go/go.sum new file mode 100644 index 0000000..52c72d3 --- /dev/null +++ b/learning-otel-go/go.sum @@ -0,0 +1,59 @@ +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= +github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/learning-otel-go/internal/config/env.go b/learning-otel-go/internal/config/env.go new file mode 100644 index 0000000..6b02922 --- /dev/null +++ b/learning-otel-go/internal/config/env.go @@ -0,0 +1,55 @@ +package config + +import ( + "os" + "strconv" +) + +type Config struct { + Server ServerConfig + Database DatabaseConfig + LogLevel string + APIKey string +} + +type ServerConfig struct { + Port string + Env string +} + +type DatabaseConfig struct { + Host string + Port int + User string + Password string + Name string +} + +func LoadConfig() *Config { + port := getEnv("PORT", "8080") + dbPort, _ := strconv.Atoi(getEnv("DB_PORT", "5432")) + + return &Config{ + Server: ServerConfig{ + Port: port, + Env: getEnv("ENV", "development"), + }, + Database: DatabaseConfig{ + Host: getEnv("DB_HOST", "localhost"), + Port: dbPort, + User: getEnv("DB_USER", "admin"), + Password: getEnv("DB_PASS", "admin123"), + Name: getEnv("DB_NAME", "golearn_db"), + }, + LogLevel: getEnv("LOG_LEVEL", "info"), + APIKey: getEnv("API_KEY", ""), + } +} + +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} diff --git a/learning-otel-go/internal/core/task/mock_repository.go b/learning-otel-go/internal/core/task/mock_repository.go new file mode 100644 index 0000000..ca7cd14 --- /dev/null +++ b/learning-otel-go/internal/core/task/mock_repository.go @@ -0,0 +1,43 @@ +package task + +type MockTaskRepository struct { + tasks []Task + created []Task +} + +func NewMockTaskRepository() *MockTaskRepository { + return &MockTaskRepository{ + tasks: []Task{}, + created: []Task{}, + } +} + +func (m *MockTaskRepository) Create(t Task) error { + m.tasks = append(m.tasks, t) + m.created = append(m.created, t) + return nil +} + +func (m *MockTaskRepository) GetAll() ([]Task, error) { + return m.tasks, nil +} + +func (m *MockTaskRepository) Update(id string, newTask Task) error { + for i, t := range m.tasks { + if t.Id == id { + m.tasks[i] = newTask + return nil + } + } + return nil +} + +func (m *MockTaskRepository) Delete(id string) error { + for i, t := range m.tasks { + if t.Id == id { + m.tasks = append(m.tasks[:i], m.tasks[i+1:]...) + return nil + } + } + return nil +} diff --git a/learning-otel-go/internal/core/task/model.go b/learning-otel-go/internal/core/task/model.go new file mode 100644 index 0000000..006807b --- /dev/null +++ b/learning-otel-go/internal/core/task/model.go @@ -0,0 +1,8 @@ +package task + +type Task struct { + Id string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Concluded bool `json:"concluded"` +} diff --git a/learning-otel-go/internal/core/task/pg_repository.go b/learning-otel-go/internal/core/task/pg_repository.go new file mode 100644 index 0000000..9c7daa5 --- /dev/null +++ b/learning-otel-go/internal/core/task/pg_repository.go @@ -0,0 +1,113 @@ +package task + +import ( + "context" + "database/sql" + "errors" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +type PgTaskRepository struct { + db *sql.DB +} + +func NewPgTaskRepository(db *sql.DB) *PgTaskRepository { + return &PgTaskRepository{db: db} +} + +func (r *PgTaskRepository) Create(task Task) error { + ctx := context.Background() + tracer := otel.Tracer("postgres-repository") + + ctx, span := tracer.Start(ctx, "CreateTask", trace.WithAttributes( + attribute.String("db.operation", "INSERT"), + attribute.String("task.id", task.Id), + )) + defer span.End() + + query := `INSERT INTO tasks (id, title, description, concluded) VALUES ($1, $2, $3, $4)` + _, err := r.db.ExecContext(ctx, query, task.Id, task.Title, task.Description, task.Concluded) + return err +} + +func (r *PgTaskRepository) GetAll() ([]Task, error) { + ctx := context.Background() + tracer := otel.Tracer("postgres-repository") + + ctx, span := tracer.Start(ctx, "GetAllTasks", trace.WithAttributes( + attribute.String("db.operation", "SELECT"), + )) + defer span.End() + + query := `SELECT id, title, description, concluded FROM tasks` + rows, err := r.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var tasks []Task + for rows.Next() { + var t Task + if err := rows.Scan(&t.Id, &t.Title, &t.Description, &t.Concluded); err != nil { + return nil, err + } + tasks = append(tasks, t) + } + + span.SetAttributes(attribute.Int("db.rows_returned", len(tasks))) + return tasks, nil +} + +func (r *PgTaskRepository) Update(id string, task Task) error { + ctx := context.Background() + tracer := otel.Tracer("postgres-repository") + + ctx, span := tracer.Start(ctx, "UpdateTask", trace.WithAttributes( + attribute.String("db.operation", "UPDATE"), + attribute.String("task.id", id), + )) + defer span.End() + + query := `UPDATE tasks SET title = $1, description = $2, concluded = $3 WHERE id = $4` + result, err := r.db.ExecContext(ctx, query, task.Title, task.Description, task.Concluded, id) + if err != nil { + return err + } + + rowsAffected, _ := result.RowsAffected() + span.SetAttributes(attribute.Int("db.rows_affected", int(rowsAffected))) + + if rowsAffected == 0 { + return errors.New("task não encontrada") + } + return nil +} + +func (r *PgTaskRepository) Delete(id string) error { + ctx := context.Background() + tracer := otel.Tracer("postgres-repository") + + ctx, span := tracer.Start(ctx, "DeleteTask", trace.WithAttributes( + attribute.String("db.operation", "DELETE"), + attribute.String("task.id", id), + )) + defer span.End() + + query := `DELETE FROM tasks WHERE id = $1` + result, err := r.db.ExecContext(ctx, query, id) + if err != nil { + return err + } + + rowsAffected, _ := result.RowsAffected() + span.SetAttributes(attribute.Int("db.rows_affected", int(rowsAffected))) + + if rowsAffected == 0 { + return errors.New("task não encontrada") + } + return nil +} diff --git a/learning-otel-go/internal/core/task/repository.go b/learning-otel-go/internal/core/task/repository.go new file mode 100644 index 0000000..2c8ef70 --- /dev/null +++ b/learning-otel-go/internal/core/task/repository.go @@ -0,0 +1,8 @@ +package task + +type TaskRepository interface { + Create(task Task) error + GetAll() ([]Task, error) + Update(id string, task Task) error + Delete(id string) error +} diff --git a/learning-otel-go/internal/core/task/service.go b/learning-otel-go/internal/core/task/service.go new file mode 100644 index 0000000..3043b5f --- /dev/null +++ b/learning-otel-go/internal/core/task/service.go @@ -0,0 +1,83 @@ +package task + +import ( + "context" + + "github.com/google/uuid" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +type TaskService struct { + repo TaskRepository +} + +func NewTaskService(repo TaskRepository) *TaskService { + return &TaskService{repo: repo} +} + +func (s *TaskService) CreateTask(title, description string, concluded bool) error { + // Obter o tracer para este service + ctx := context.Background() + tracer := otel.Tracer("task-service") + + // Criar um span para a operação + ctx, span := tracer.Start(ctx, "CreateTask", trace.WithAttributes( + attribute.String("task.title", title), + )) + defer span.End() + + id := uuid.New().String() + task := Task{ + Id: id, + Title: title, + Description: description, + Concluded: concluded, + } + + // Adicionar ID ao span para correlacionar + span.SetAttributes(attribute.String("task.id", id)) + + return s.repo.Create(task) +} + +func (s *TaskService) GetTasks() ([]Task, error) { + ctx := context.Background() + tracer := otel.Tracer("task-service") + + ctx, span := tracer.Start(ctx, "GetTasks") + defer span.End() + + tasks, err := s.repo.GetAll() + if err != nil { + return nil, err + } + + span.SetAttributes(attribute.Int("task.count", len(tasks))) + return tasks, nil +} + +func (s *TaskService) EditTask(id string, newTask Task) error { + ctx := context.Background() + tracer := otel.Tracer("task-service") + + ctx, span := tracer.Start(ctx, "EditTask", trace.WithAttributes( + attribute.String("task.id", id), + )) + defer span.End() + + return s.repo.Update(id, newTask) +} + +func (s *TaskService) DeleteTask(id string) error { + ctx := context.Background() + tracer := otel.Tracer("task-service") + + ctx, span := tracer.Start(ctx, "DeleteTask", trace.WithAttributes( + attribute.String("task.id", id), + )) + defer span.End() + + return s.repo.Delete(id) +} diff --git a/learning-otel-go/internal/core/task/service_test.go b/learning-otel-go/internal/core/task/service_test.go new file mode 100644 index 0000000..1a65700 --- /dev/null +++ b/learning-otel-go/internal/core/task/service_test.go @@ -0,0 +1,57 @@ +package task + +import ( + "testing" +) + +func TestCreateTask(t *testing.T) { + mockRepo := NewMockTaskRepository() + service := NewTaskService(mockRepo) + + err := service.CreateTask("Título", "Descrição", false) + if err != nil { + t.Errorf("Erro inesperado ao criar tarefa: %v", err) + } + + tasks, _ := mockRepo.GetAll() + if len(tasks) != 1 { + t.Errorf("Esperava 1 tarefa, mas tenho %d", len(tasks)) + } +} + +func TestEditTask(t *testing.T) { + + mockRepo := NewMockTaskRepository() + service := NewTaskService(mockRepo) + + service.CreateTask("Antigo", "Desc", false) + + tasks, _ := mockRepo.GetAll() + if len(tasks) != 1 { + t.Fatalf("Falha ao criar tarefa inicial") + } + taskId := tasks[0].Id + + update := Task{Title: "Novo", Description: "Nova Desc", Concluded: true} + service.EditTask(taskId, update) + + tasksAtualizadas, _ := mockRepo.GetAll() + if tasksAtualizadas[0].Title != "Novo" { + t.Errorf("Esperava título 'Novo', obtive '%s'", tasksAtualizadas[0].Title) + } +} + +func TestDeleteTask(t *testing.T) { + mockRepo := NewMockTaskRepository() + service := NewTaskService(mockRepo) + + task := Task{Id: "123", Title: "Excluir", Description: "Desc", Concluded: false} + mockRepo.tasks = append(mockRepo.tasks, task) + + service.DeleteTask("123") + + tasks, _ := mockRepo.GetAll() + if len(tasks) != 0 { + t.Errorf("Esperava lista vazia, mas tenho %d item(ns)", len(tasks)) + } +} diff --git a/learning-otel-go/internal/handler/task_handler.go b/learning-otel-go/internal/handler/task_handler.go new file mode 100644 index 0000000..3df1b7d --- /dev/null +++ b/learning-otel-go/internal/handler/task_handler.go @@ -0,0 +1,78 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "todo-api/internal/core/task" + + "github.com/gorilla/mux" +) + +type TaskHandler struct { + service *task.TaskService +} + +func NewTaskHandler(service *task.TaskService) *TaskHandler { + return &TaskHandler{service: service} +} + +func (h *TaskHandler) CreateTaskHandler(w http.ResponseWriter, r *http.Request) { + var taskBody task.Task + err := json.NewDecoder(r.Body).Decode(&taskBody) + if err != nil { + http.Error(w, "Erro ao decodificar JSON", http.StatusBadRequest) + return + } + + err = h.service.CreateTask(taskBody.Title, taskBody.Description, taskBody.Concluded) + if err != nil { + http.Error(w, "Erro ao criar tarefa", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} + +func (h *TaskHandler) GetTasksHandler(w http.ResponseWriter, r *http.Request) { + tasks, err := h.service.GetTasks() + if err != nil { + http.Error(w, "Erro ao buscar tarefas", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(tasks) +} + +func (h *TaskHandler) EditTaskHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + var taskBody task.Task + err := json.NewDecoder(r.Body).Decode(&taskBody) + if err != nil { + http.Error(w, "Erro ao decodificar JSON", http.StatusBadRequest) + return + } + + err = h.service.EditTask(id, taskBody) + if err != nil { + http.Error(w, "Erro ao editar tarefa", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (h *TaskHandler) DeleteTaskHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + err := h.service.DeleteTask(id) + if err != nil { + http.Error(w, "Erro ao deletar tarefa", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/learning-otel-go/internal/handler/task_handler_test.go b/learning-otel-go/internal/handler/task_handler_test.go new file mode 100644 index 0000000..5a3435c --- /dev/null +++ b/learning-otel-go/internal/handler/task_handler_test.go @@ -0,0 +1,126 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "todo-api/internal/core/task" + + "github.com/gorilla/mux" +) + +// Função para simular variáveis de rota do Mux +func muxSetVars(r *http.Request, vars map[string]string) *http.Request { + return mux.SetURLVars(r, vars) +} + +func setupMockHandler() *TaskHandler { + mockRepo := task.NewMockTaskRepository() + service := task.NewTaskService(mockRepo) + + service.CreateTask("Teste", "Descrição", false) + + return NewTaskHandler(service) +} + +func TestCreateTaskHandler(t *testing.T) { + handler := setupMockHandler() + + body := map[string]interface{}{ + "title": "Nova Task", + "description": "Descrição da nova task", + "concluded": false, + } + jsonBody, _ := json.Marshal(body) + + req := httptest.NewRequest("POST", "/task", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + + handler.CreateTaskHandler(rr, req) + + if status := rr.Code; status != http.StatusCreated { + t.Errorf("Esperava status 201, obtive %d", status) + } +} + +func TestGetTasksHandler(t *testing.T) { + handler := setupMockHandler() + + req := httptest.NewRequest("GET", "/tasks", nil) + rr := httptest.NewRecorder() + + handler.GetTasksHandler(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("Esperava 200, obtive %d", rr.Code) + } + + var tasks []task.Task + err := json.NewDecoder(rr.Body).Decode(&tasks) + if err != nil { + t.Errorf("Erro ao decodificar JSON: %v", err) + } + + if len(tasks) == 0 { + t.Errorf("Esperava ao menos 1 tarefa") + } +} + +func TestEditTaskHandler(t *testing.T) { + mockRepo := task.NewMockTaskRepository() + svc := task.NewTaskService(mockRepo) + handler := NewTaskHandler(svc) + + taskData := task.Task{ + Id: "123", + Title: "Antigo", + Description: "Desc", + Concluded: false, + } + mockRepo.Create(taskData) + + updated := task.Task{ + Title: "Atualizado", + Description: "Nova Desc", + Concluded: true, + } + body, _ := json.Marshal(updated) + + req := httptest.NewRequest("PUT", "/task/123", bytes.NewBuffer(body)) + req = muxSetVars(req, map[string]string{"id": "123"}) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + + handler.EditTaskHandler(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("Esperava 200, obtive %d", rr.Code) + } +} + +func TestDeleteTaskHandler(t *testing.T) { + mockRepo := task.NewMockTaskRepository() + svc := task.NewTaskService(mockRepo) + handler := NewTaskHandler(svc) + + mockRepo.Create(task.Task{ + Id: "abc", + Title: "Apagar", + Description: "Desc", + Concluded: false, + }) + + req := httptest.NewRequest("DELETE", "/task/abc", nil) + req = muxSetVars(req, map[string]string{"id": "abc"}) + rr := httptest.NewRecorder() + + handler.DeleteTaskHandler(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("Esperava 200, obtive %d", rr.Code) + } +} diff --git a/learning-otel-go/internal/router/router.go b/learning-otel-go/internal/router/router.go new file mode 100644 index 0000000..f83287e --- /dev/null +++ b/learning-otel-go/internal/router/router.go @@ -0,0 +1,30 @@ +package router + +import ( + "net/http" + + "todo-api/internal/core/task" + "todo-api/internal/handler" + + "github.com/gorilla/mux" +) + +func NewRouter(service *task.TaskService) http.Handler { + r := mux.NewRouter() + + taskHandler := handler.NewTaskHandler(service) + + // Configurar rotas + r.HandleFunc("/task", taskHandler.CreateTaskHandler).Methods("POST") + r.HandleFunc("/tasks", taskHandler.GetTasksHandler).Methods("GET") + r.HandleFunc("/task/{id}", taskHandler.EditTaskHandler).Methods("PUT") + r.HandleFunc("/task/{id}", taskHandler.DeleteTaskHandler).Methods("DELETE") + + // Adicionar health check + r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }).Methods("GET") + + return r +} diff --git a/learning-otel-go/internal/telemetry/middleware.go b/learning-otel-go/internal/telemetry/middleware.go new file mode 100644 index 0000000..dae75e2 --- /dev/null +++ b/learning-otel-go/internal/telemetry/middleware.go @@ -0,0 +1,75 @@ +package telemetry + +import ( + "net/http" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +// TracingMiddleware instrumenta requisições HTTP com OpenTelemetry +func TracingMiddleware(next http.Handler) http.Handler { + tracer := otel.Tracer("http-middleware") + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Extrai contexto de trace da requisição + propagator := otel.GetTextMapPropagator() + ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + + // Inicia um novo span para esta requisição + path := r.URL.Path + method := r.Method + ctx, span := tracer.Start( + ctx, + method+" "+path, + trace.WithSpanKind(trace.SpanKindServer), + trace.WithAttributes( + attribute.String("http.method", method), + attribute.String("http.url", path), + attribute.String("http.user_agent", r.UserAgent()), + attribute.String("http.remote_addr", r.RemoteAddr), + ), + ) + defer span.End() + + // Wrapper do ResponseWriter para capturar o código de status + wrapper := &responseWriter{w: w, status: http.StatusOK} + + // Passa o contexto de trace para o próximo handler + next.ServeHTTP(wrapper, r.WithContext(ctx)) + + // Adiciona informações de resposta ao span + span.SetAttributes( + attribute.Int("http.status_code", wrapper.status), + ) + + // Se o status é de erro, marca o span como erro + if wrapper.status >= 400 { + span.SetStatus(codes.Error, http.StatusText(wrapper.status)) + } else { + span.SetStatus(codes.Ok, "") + } + }) +} + +// responseWriter é um wrapper de http.ResponseWriter que captura o código de status +type responseWriter struct { + w http.ResponseWriter + status int +} + +func (rw *responseWriter) Header() http.Header { + return rw.w.Header() +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + return rw.w.Write(b) +} + +func (rw *responseWriter) WriteHeader(statusCode int) { + rw.status = statusCode + rw.w.WriteHeader(statusCode) +} diff --git a/learning-otel-go/internal/telemetry/tracing.go b/learning-otel-go/internal/telemetry/tracing.go new file mode 100644 index 0000000..a516f87 --- /dev/null +++ b/learning-otel-go/internal/telemetry/tracing.go @@ -0,0 +1,74 @@ +package telemetry + +import ( + "context" + "fmt" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// InitTracer inicializa o rastreamento do OpenTelemetry +func InitTracer(serviceName string, collectorURL string) (func(context.Context) error, error) { + ctx := context.Background() + + // Cria uma conexão com o coletor OTLP + conn, err := grpc.DialContext(ctx, collectorURL, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithBlock(), + ) + if err != nil { + return nil, fmt.Errorf("falha ao conectar com o coletor OTLP: %w", err) + } + + // Cria o exportador de traces + traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn)) + if err != nil { + return nil, fmt.Errorf("falha ao criar exportador OTLP: %w", err) + } + + // Cria um recurso que identifica a aplicação + res, err := resource.New(ctx, + resource.WithAttributes( + semconv.ServiceName(serviceName), + ), + ) + if err != nil { + return nil, fmt.Errorf("falha ao criar recurso: %w", err) + } + + // Cria um provedor de tracer + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(traceExporter), + sdktrace.WithResource(res), + sdktrace.WithSampler(sdktrace.AlwaysSample()), + ) + + // Define o provedor global de tracer + otel.SetTracerProvider(tracerProvider) + + // Define o propagador global + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + // Função para limpeza ao encerrar a aplicação + cleanup := func(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + if err := tracerProvider.Shutdown(ctx); err != nil { + return fmt.Errorf("falha ao desligar provedor de tracer: %w", err) + } + return nil + } + + return cleanup, nil +} diff --git a/learning-otel-go/main.go b/learning-otel-go/main.go new file mode 100644 index 0000000..76ceba7 --- /dev/null +++ b/learning-otel-go/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + _ "github.com/lib/pq" + + "todo-api/internal/config" + "todo-api/internal/core/task" + "todo-api/internal/router" + "todo-api/internal/telemetry" +) + +func main() { + // Carregar configurações + var cfg = config.LoadConfig() + + // Inicializar OpenTelemetry + collectorURL := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + if collectorURL == "" { + collectorURL = "localhost:4317" // Valor padrão para o coletor OTLP + } + + cleanup, err := telemetry.InitTracer("todo-api", collectorURL) + if err != nil { + log.Printf("Erro ao inicializar OpenTelemetry: %v. Continuando sem telemetria.", err) + } else { + // Garantir que o tracer seja encerrado corretamente na saída + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := cleanup(ctx); err != nil { + log.Printf("Erro ao limpar recursos de telemetria: %v", err) + } + }() + } + + // Conectar ao banco de dados + dbURL := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", + cfg.Database.User, + cfg.Database.Password, + cfg.Database.Host, + cfg.Database.Port, + cfg.Database.Name) + + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatal("Erro ao conectar no banco:", err) + } + defer db.Close() + + if err := db.Ping(); err != nil { + log.Fatal("Banco indisponível:", err) + } + + // Inicializar componentes + repo := task.NewPgTaskRepository(db) + service := task.NewTaskService(repo) + r := router.NewRouter(service) + + // Adicionar middleware de telemetria + handler := telemetry.TracingMiddleware(r) + + // Configurar e iniciar o servidor + port := cfg.Server.Port + srv := &http.Server{ + Addr: ":" + port, + Handler: handler, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + } + + // Iniciar o servidor em uma goroutine + go func() { + log.Printf("Servidor rodando em http://localhost:%s", port) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Erro ao iniciar servidor: %v", err) + } + }() + + // Graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Desligando servidor...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Erro durante shutdown do servidor: %v", err) + } + + log.Println("Servidor desligado com sucesso") +} From 6d1576f564874cd11873e0a256da380a5a5332c6 Mon Sep 17 00:00:00 2001 From: Alexandro Hervis Date: Mon, 7 Apr 2025 08:43:19 -0300 Subject: [PATCH 2/2] instrumenting metrics and logs --- .gitignore | 1 + learning-otel-go/.env.example | 9 +- learning-otel-go/docker-compose.yml | 29 ++- learning-otel-go/go.mod | 3 + learning-otel-go/go.sum | 6 + learning-otel-go/internal/app/app.go | 189 ++++++++++++++++++ learning-otel-go/internal/config/env.go | 14 ++ .../internal/telemetry/logging.go | 98 +++++++++ .../internal/telemetry/metrics.go | 169 ++++++++++++++++ .../internal/telemetry/middleware.go | 36 +++- .../internal/telemetry/tracing.go | 16 +- learning-otel-go/main.go | 96 ++------- learning-otel-go/otel-collector-config.yaml | 34 ++++ 13 files changed, 597 insertions(+), 103 deletions(-) create mode 100644 learning-otel-go/internal/app/app.go create mode 100644 learning-otel-go/internal/telemetry/logging.go create mode 100644 learning-otel-go/internal/telemetry/metrics.go create mode 100644 learning-otel-go/otel-collector-config.yaml diff --git a/.gitignore b/.gitignore index e43b0f9..bde4e77 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +**/.env \ No newline at end of file diff --git a/learning-otel-go/.env.example b/learning-otel-go/.env.example index 23dd34e..4d19454 100644 --- a/learning-otel-go/.env.example +++ b/learning-otel-go/.env.example @@ -9,6 +9,13 @@ DB_USER=admin DB_PASS=admin123 DB_NAME=golearn_db -# Outras configurações +# Configurações de logs LOG_LEVEL=info + +# OpenTelemetry +# Para usar o OpenTelemetry Collector (recomendado): +OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 +# O OpenTelemetry Collector encaminhará traces para Jaeger e métricas para seus destinos configurados + +# Segurança API_KEY=sua_chave_secreta_aqui \ No newline at end of file diff --git a/learning-otel-go/docker-compose.yml b/learning-otel-go/docker-compose.yml index cd8b17e..436c2ea 100644 --- a/learning-otel-go/docker-compose.yml +++ b/learning-otel-go/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: postgres: image: postgres:latest @@ -13,23 +11,44 @@ services: volumes: - postgres_data:/var/lib/postgresql/data restart: unless-stopped + networks: + - otel-network + + otel-collector: + image: otel/opentelemetry-collector:latest + container_name: otel-collector + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "4319:4317" # OTLP gRPC - mapeando para 4319 externamente + - "4320:4318" # OTLP HTTP - mapeando para 4320 externamente + depends_on: + - jaeger + networks: + - otel-network jaeger: image: jaegertracing/all-in-one:latest - container_name: golearn_jaeger + container_name: jaeger ports: - "6831:6831/udp" - "6832:6832/udp" - "5778:5778" - "16686:16686" - - "4317:4317" - - "4318:4318" - "14250:14250" - "14268:14268" - "14269:14269" + - "4317:4317" # OTLP gRPC nativo do Jaeger environment: - COLLECTOR_OTLP_ENABLED=true restart: unless-stopped + networks: + - otel-network volumes: postgres_data: + +networks: + otel-network: + driver: bridge diff --git a/learning-otel-go/go.mod b/learning-otel-go/go.mod index 02192d0..e7c5098 100644 --- a/learning-otel-go/go.mod +++ b/learning-otel-go/go.mod @@ -16,10 +16,13 @@ require ( github.com/lib/pq v1.10.9 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect diff --git a/learning-otel-go/go.sum b/learning-otel-go/go.sum index 52c72d3..43db076 100644 --- a/learning-otel-go/go.sum +++ b/learning-otel-go/go.sum @@ -28,14 +28,20 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= diff --git a/learning-otel-go/internal/app/app.go b/learning-otel-go/internal/app/app.go new file mode 100644 index 0000000..8069438 --- /dev/null +++ b/learning-otel-go/internal/app/app.go @@ -0,0 +1,189 @@ +package app + +import ( + "context" + "database/sql" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "todo-api/internal/config" + "todo-api/internal/core/task" + "todo-api/internal/router" + "todo-api/internal/telemetry" +) + +// Application encapsula toda a lógica da aplicação +type Application struct { + config *config.Config + logger *telemetry.Logger + server *http.Server + db *sql.DB +} + +// New cria uma nova instância da aplicação +func New() *Application { + return &Application{ + config: config.LoadConfig(), + logger: telemetry.NewLogger(), + } +} + +// Initialize inicializa todos os componentes da aplicação +func (app *Application) Initialize(ctx context.Context) error { + if err := app.setupTelemetry(ctx); err != nil { + return fmt.Errorf("falha ao configurar telemetria: %w", err) + } + + if err := app.setupDatabase(ctx); err != nil { + return fmt.Errorf("falha ao configurar banco de dados: %w", err) + } + + app.setupServer(ctx) + return nil +} + +// setupTelemetry inicializa os componentes de telemetria +func (app *Application) setupTelemetry(ctx context.Context) error { + collectorURL := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + fmt.Println("collectorURL", collectorURL) + if collectorURL == "" { + collectorURL = "localhost:4317" + } + + // Inicializar tracer + traceCleanup, err := telemetry.InitTracer("todo-api", collectorURL) + if err != nil { + app.logger.Error(ctx, "Erro ao inicializar OpenTelemetry Tracer", "error", err) + return err + } + + // Registrar função de limpeza + go func() { + <-ctx.Done() + cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := traceCleanup(cleanupCtx); err != nil { + app.logger.Error(cleanupCtx, "Erro ao limpar recursos de tracer", "error", err) + } + }() + + // Inicializar métricas + meterCleanup, err := telemetry.InitMeter("todo-api", collectorURL) + if err != nil { + app.logger.Error(ctx, "Erro ao inicializar OpenTelemetry Metrics", "error", err) + return err + } + + // Registrar função de limpeza + go func() { + <-ctx.Done() + cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := meterCleanup(cleanupCtx); err != nil { + app.logger.Error(cleanupCtx, "Erro ao limpar recursos de métricas", "error", err) + } + }() + + return nil +} + +// setupDatabase configura a conexão com o banco de dados +func (app *Application) setupDatabase(ctx context.Context) error { + dbURL := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", + app.config.Database.User, + app.config.Database.Password, + app.config.Database.Host, + app.config.Database.Port, + app.config.Database.Name) + + db, err := sql.Open("postgres", dbURL) + if err != nil { + app.logger.Error(ctx, "Erro ao conectar no banco", "error", err) + return err + } + + if err := db.Ping(); err != nil { + app.logger.Error(ctx, "Banco indisponível", "error", err) + return err + } + + app.db = db + app.logger.Info(ctx, "Conexão com o banco de dados estabelecida com sucesso") + return nil +} + +// setupServer configura o servidor HTTP +func (app *Application) setupServer(ctx context.Context) { + repo := task.NewPgTaskRepository(app.db) + service := task.NewTaskService(repo) + r := router.NewRouter(service) + + // Adicionar middleware de telemetria + handler := telemetry.TracingMiddleware(r) + + port := app.config.Server.Port + app.server = &http.Server{ + Addr: ":" + port, + Handler: handler, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + } +} + +// Start inicia o servidor e configura o graceful shutdown +func (app *Application) Start(ctx context.Context) error { + // Contexto para gerenciar o ciclo de vida da aplicação + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Canal para receber sinais de término + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + // Iniciar o servidor em uma goroutine + errChan := make(chan error, 1) + go func() { + port := app.config.Server.Port + app.logger.Info(ctx, "Servidor iniciado", "port", port) + if err := app.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + app.logger.Error(ctx, "Erro ao iniciar servidor", "error", err) + errChan <- err + } + }() + + // Aguardar sinal de término ou erro + select { + case <-quit: + app.logger.Info(ctx, "Desligando servidor...") + case err := <-errChan: + app.logger.Error(ctx, "Erro fatal no servidor", "error", err) + return err + } + + return app.Shutdown(ctx) +} + +// Shutdown desliga o servidor de forma graciosa +func (app *Application) Shutdown(ctx context.Context) error { + shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + if err := app.server.Shutdown(shutdownCtx); err != nil { + app.logger.Error(shutdownCtx, "Erro durante shutdown do servidor", "error", err) + return err + } + + if app.db != nil { + if err := app.db.Close(); err != nil { + app.logger.Error(shutdownCtx, "Erro ao fechar conexão com banco de dados", "error", err) + return err + } + } + + app.logger.Info(shutdownCtx, "Servidor desligado com sucesso") + return nil +} diff --git a/learning-otel-go/internal/config/env.go b/learning-otel-go/internal/config/env.go index 6b02922..d0bfa34 100644 --- a/learning-otel-go/internal/config/env.go +++ b/learning-otel-go/internal/config/env.go @@ -1,10 +1,21 @@ package config import ( + "log" "os" "strconv" + + "github.com/joho/godotenv" ) +// LoadEnv carrega as variáveis de ambiente do arquivo .env, se existir +func LoadEnv() { + // Tenta carregar .env, mas não falha se o arquivo não existir + if err := godotenv.Load(); err != nil { + log.Println("Arquivo .env não encontrado, usando variáveis de ambiente do sistema") + } +} + type Config struct { Server ServerConfig Database DatabaseConfig @@ -26,6 +37,9 @@ type DatabaseConfig struct { } func LoadConfig() *Config { + // Tenta carregar as variáveis de ambiente do arquivo .env + LoadEnv() + port := getEnv("PORT", "8080") dbPort, _ := strconv.Atoi(getEnv("DB_PORT", "5432")) diff --git a/learning-otel-go/internal/telemetry/logging.go b/learning-otel-go/internal/telemetry/logging.go new file mode 100644 index 0000000..4f0877a --- /dev/null +++ b/learning-otel-go/internal/telemetry/logging.go @@ -0,0 +1,98 @@ +package telemetry + +import ( + "context" + "fmt" + "log" + "os" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// Logger é uma estrutura personalizada para logs que integra com OpenTelemetry +type Logger struct { + infoLogger *log.Logger + errorLogger *log.Logger + debugLogger *log.Logger +} + +// NewLogger cria uma nova instância de Logger +func NewLogger() *Logger { + return &Logger{ + infoLogger: log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile), + errorLogger: log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile), + debugLogger: log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile), + } +} + +// Info registra uma mensagem de informação +func (l *Logger) Info(ctx context.Context, msg string, keyValues ...interface{}) { + // Adiciona informações de trace se disponíveis + addTraceInfoToLog(ctx, l.infoLogger, msg, keyValues...) +} + +// Error registra uma mensagem de erro +func (l *Logger) Error(ctx context.Context, msg string, keyValues ...interface{}) { + // Marca o span atual com o erro (se existir) + span := trace.SpanFromContext(ctx) + if span.IsRecording() { + span.SetStatus(codes.Error, msg) + + // Adiciona atributos do erro ao span + for i := 0; i < len(keyValues); i += 2 { + if i+1 < len(keyValues) { + key, ok := keyValues[i].(string) + if ok { + span.SetAttributes(attribute.String(key, toString(keyValues[i+1]))) + } + } + } + } + + // Registra no log + addTraceInfoToLog(ctx, l.errorLogger, msg, keyValues...) +} + +// Debug registra uma mensagem de depuração +func (l *Logger) Debug(ctx context.Context, msg string, keyValues ...interface{}) { + // Adiciona informações de trace se disponíveis + addTraceInfoToLog(ctx, l.debugLogger, msg, keyValues...) +} + +// addTraceInfoToLog adiciona informações de trace ao log +func addTraceInfoToLog(ctx context.Context, logger *log.Logger, msg string, keyValues ...interface{}) { + // Extrai o TraceID e SpanID do contexto, se estiver disponível + span := trace.SpanFromContext(ctx) + + if span.IsRecording() { + spanContext := span.SpanContext() + if spanContext.IsValid() { + // Adiciona os IDs de trace e span à mensagem + msg = msg + " [trace_id=" + spanContext.TraceID().String() + + " span_id=" + spanContext.SpanID().String() + "]" + } + } + + // Formata os pares chave-valor adicionais + if len(keyValues) > 0 { + pairs := make([]interface{}, 0, len(keyValues)+1) + pairs = append(pairs, msg) + pairs = append(pairs, keyValues...) + logger.Println(pairs...) + } else { + logger.Println(msg) + } +} + +// toString converte um valor para string +func toString(value interface{}) string { + if value == nil { + return "" + } + if s, ok := value.(string); ok { + return s + } + return fmt.Sprintf("%v", value) +} diff --git a/learning-otel-go/internal/telemetry/metrics.go b/learning-otel-go/internal/telemetry/metrics.go new file mode 100644 index 0000000..aa2f26d --- /dev/null +++ b/learning-otel-go/internal/telemetry/metrics.go @@ -0,0 +1,169 @@ +package telemetry + +import ( + "context" + "fmt" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/resource" + + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +// Variáveis globais de métricas para uso em todo o aplicativo +var ( + // Contador para requisições HTTP + httpRequestCounter metric.Int64Counter + // Histograma para duração de requisições + httpRequestDuration metric.Float64Histogram + // Contador para operações de banco de dados + dbOperationCounter metric.Int64Counter + // Histograma para duração de operações de banco de dados + dbOperationDuration metric.Float64Histogram +) + +// InitMeter inicializa o provedor de métricas do OpenTelemetry +func InitMeter(serviceName string, collectorURL string) (func(context.Context) error, error) { + ctx := context.Background() + + // Cria um recurso que identifica a aplicação + res, err := resource.New(ctx, + resource.WithAttributes( + semconv.ServiceName(serviceName), + ), + ) + if err != nil { + return nil, fmt.Errorf("falha ao criar recurso: %w", err) + } + + // Cria o exportador de métricas usando gRPC com o mesmo coletor dos traces + exporter, err := otlpmetricgrpc.New(ctx, + otlpmetricgrpc.WithEndpoint(collectorURL), + otlpmetricgrpc.WithInsecure(), + ) + if err != nil { + return nil, fmt.Errorf("falha ao criar exportador de métricas: %w", err) + } + + // Cria o leitor periódico de métricas que envia dados a cada 15 segundos + reader := sdkmetric.NewPeriodicReader(exporter, + sdkmetric.WithInterval(15*time.Second), + ) + + // Cria um provedor de métricas + meterProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(reader), + ) + + // Define o provedor global de métricas + otel.SetMeterProvider(meterProvider) + + // Cria um medidor de métricas específico para nossa aplicação + meter := meterProvider.Meter( + "todo-api", + metric.WithInstrumentationVersion("0.1.0"), + ) + + // Inicializa as métricas que serão usadas pelo aplicativo + initMetrics(meter) + + // Função para limpeza ao encerrar a aplicação + cleanup := func(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + if err := meterProvider.Shutdown(ctx); err != nil { + return fmt.Errorf("falha ao desligar provedor de métricas: %w", err) + } + return nil + } + + return cleanup, nil +} + +// Inicializa as métricas que serão usadas pelo aplicativo +func initMetrics(meter metric.Meter) { + var err error + + // Contador de requisições HTTP + httpRequestCounter, err = meter.Int64Counter( + "http.requests.total", + metric.WithDescription("Número total de requisições HTTP"), + metric.WithUnit("1"), + ) + if err != nil { + otel.Handle(err) + } + + // Histograma de duração de requisições HTTP + httpRequestDuration, err = meter.Float64Histogram( + "http.request.duration", + metric.WithDescription("Duração das requisições HTTP"), + metric.WithUnit("ms"), + ) + if err != nil { + otel.Handle(err) + } + + // Contador de operações de banco de dados + dbOperationCounter, err = meter.Int64Counter( + "db.operations.total", + metric.WithDescription("Número total de operações de banco de dados"), + metric.WithUnit("1"), + ) + if err != nil { + otel.Handle(err) + } + + // Histograma de duração de operações de banco de dados + dbOperationDuration, err = meter.Float64Histogram( + "db.operation.duration", + metric.WithDescription("Duração das operações de banco de dados"), + metric.WithUnit("ms"), + ) + if err != nil { + otel.Handle(err) + } +} + +// RecordHTTPRequest registra uma requisição HTTP +func RecordHTTPRequest(ctx context.Context, method, route string, statusCode int, duration float64) { + httpRequestCounter.Add(ctx, 1, + metric.WithAttributes( + semconv.HTTPMethodKey.String(method), + semconv.HTTPRouteKey.String(route), + semconv.HTTPStatusCodeKey.Int(statusCode), + ), + ) + + httpRequestDuration.Record(ctx, duration, + metric.WithAttributes( + semconv.HTTPMethodKey.String(method), + semconv.HTTPRouteKey.String(route), + semconv.HTTPStatusCodeKey.Int(statusCode), + ), + ) +} + +// RecordDBOperation registra uma operação de banco de dados +func RecordDBOperation(ctx context.Context, operation string, success bool, duration float64) { + dbOperationCounter.Add(ctx, 1, + metric.WithAttributes( + semconv.DBOperationKey.String(operation), + semconv.DBSystemKey.String("postgresql"), + semconv.DBStatementKey.String(operation), + ), + ) + + dbOperationDuration.Record(ctx, duration, + metric.WithAttributes( + semconv.DBOperationKey.String(operation), + semconv.DBSystemKey.String("postgresql"), + semconv.DBStatementKey.String(operation), + ), + ) +} diff --git a/learning-otel-go/internal/telemetry/middleware.go b/learning-otel-go/internal/telemetry/middleware.go index dae75e2..e858d97 100644 --- a/learning-otel-go/internal/telemetry/middleware.go +++ b/learning-otel-go/internal/telemetry/middleware.go @@ -2,6 +2,7 @@ package telemetry import ( "net/http" + "time" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -13,8 +14,12 @@ import ( // TracingMiddleware instrumenta requisições HTTP com OpenTelemetry func TracingMiddleware(next http.Handler) http.Handler { tracer := otel.Tracer("http-middleware") + logger := NewLogger() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Marca o início para cálculo de duração + startTime := time.Now() + // Extrai contexto de trace da requisição propagator := otel.GetTextMapPropagator() ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) @@ -35,21 +40,50 @@ func TracingMiddleware(next http.Handler) http.Handler { ) defer span.End() + // Log da requisição recebida + logger.Info(ctx, "Requisição recebida", + "method", method, + "path", path, + "remote_addr", r.RemoteAddr, + "user_agent", r.UserAgent(), + ) + // Wrapper do ResponseWriter para capturar o código de status wrapper := &responseWriter{w: w, status: http.StatusOK} // Passa o contexto de trace para o próximo handler next.ServeHTTP(wrapper, r.WithContext(ctx)) + // Calcula a duração + duration := float64(time.Since(startTime).Milliseconds()) + // Adiciona informações de resposta ao span span.SetAttributes( attribute.Int("http.status_code", wrapper.status), + attribute.Float64("http.duration_ms", duration), ) - // Se o status é de erro, marca o span como erro + // Registra métricas (comentado até os pacotes de métricas serem adicionados) + /* + RecordHTTPRequest(ctx, method, path, wrapper.status, duration) + */ + + // Log da resposta if wrapper.status >= 400 { + logger.Error(ctx, "Erro na resposta HTTP", + "method", method, + "path", path, + "status", wrapper.status, + "duration_ms", duration, + ) span.SetStatus(codes.Error, http.StatusText(wrapper.status)) } else { + logger.Info(ctx, "Resposta enviada com sucesso", + "method", method, + "path", path, + "status", wrapper.status, + "duration_ms", duration, + ) span.SetStatus(codes.Ok, "") } }) diff --git a/learning-otel-go/internal/telemetry/tracing.go b/learning-otel-go/internal/telemetry/tracing.go index a516f87..be6fc78 100644 --- a/learning-otel-go/internal/telemetry/tracing.go +++ b/learning-otel-go/internal/telemetry/tracing.go @@ -11,25 +11,17 @@ import ( "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" ) // InitTracer inicializa o rastreamento do OpenTelemetry func InitTracer(serviceName string, collectorURL string) (func(context.Context) error, error) { ctx := context.Background() - // Cria uma conexão com o coletor OTLP - conn, err := grpc.DialContext(ctx, collectorURL, - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithBlock(), + // Cria o exportador de traces usando a API mais recente + traceExporter, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint(collectorURL), + otlptracegrpc.WithInsecure(), ) - if err != nil { - return nil, fmt.Errorf("falha ao conectar com o coletor OTLP: %w", err) - } - - // Cria o exportador de traces - traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn)) if err != nil { return nil, fmt.Errorf("falha ao criar exportador OTLP: %w", err) } diff --git a/learning-otel-go/main.go b/learning-otel-go/main.go index 76ceba7..19b7644 100644 --- a/learning-otel-go/main.go +++ b/learning-otel-go/main.go @@ -2,101 +2,29 @@ package main import ( "context" - "database/sql" - "fmt" "log" - "net/http" "os" - "os/signal" - "syscall" - "time" _ "github.com/lib/pq" - "todo-api/internal/config" - "todo-api/internal/core/task" - "todo-api/internal/router" - "todo-api/internal/telemetry" + "todo-api/internal/app" ) func main() { - // Carregar configurações - var cfg = config.LoadConfig() + // Criar contexto raiz da aplicação + ctx := context.Background() - // Inicializar OpenTelemetry - collectorURL := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") - if collectorURL == "" { - collectorURL = "localhost:4317" // Valor padrão para o coletor OTLP - } - - cleanup, err := telemetry.InitTracer("todo-api", collectorURL) - if err != nil { - log.Printf("Erro ao inicializar OpenTelemetry: %v. Continuando sem telemetria.", err) - } else { - // Garantir que o tracer seja encerrado corretamente na saída - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := cleanup(ctx); err != nil { - log.Printf("Erro ao limpar recursos de telemetria: %v", err) - } - }() - } - - // Conectar ao banco de dados - dbURL := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", - cfg.Database.User, - cfg.Database.Password, - cfg.Database.Host, - cfg.Database.Port, - cfg.Database.Name) + // Criar e inicializar a aplicação + application := app.New() - db, err := sql.Open("postgres", dbURL) - if err != nil { - log.Fatal("Erro ao conectar no banco:", err) + if err := application.Initialize(ctx); err != nil { + log.Fatalf("Falha ao inicializar aplicação: %v", err) + os.Exit(1) } - defer db.Close() - if err := db.Ping(); err != nil { - log.Fatal("Banco indisponível:", err) + // Iniciar o servidor + if err := application.Start(ctx); err != nil { + log.Fatalf("Erro fatal na aplicação: %v", err) + os.Exit(1) } - - // Inicializar componentes - repo := task.NewPgTaskRepository(db) - service := task.NewTaskService(repo) - r := router.NewRouter(service) - - // Adicionar middleware de telemetria - handler := telemetry.TracingMiddleware(r) - - // Configurar e iniciar o servidor - port := cfg.Server.Port - srv := &http.Server{ - Addr: ":" + port, - Handler: handler, - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, - } - - // Iniciar o servidor em uma goroutine - go func() { - log.Printf("Servidor rodando em http://localhost:%s", port) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Erro ao iniciar servidor: %v", err) - } - }() - - // Graceful shutdown - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("Desligando servidor...") - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { - log.Fatalf("Erro durante shutdown do servidor: %v", err) - } - - log.Println("Servidor desligado com sucesso") } diff --git a/learning-otel-go/otel-collector-config.yaml b/learning-otel-go/otel-collector-config.yaml new file mode 100644 index 0000000..f8a8335 --- /dev/null +++ b/learning-otel-go/otel-collector-config.yaml @@ -0,0 +1,34 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + +exporters: + otlp: + endpoint: "jaeger:4317" + tls: + insecure: true + debug: + verbosity: detailed + +service: + telemetry: + logs: + level: "debug" + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp, debug] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [debug] # Você pode adicionar Prometheus aqui