Em tempos de microserviços e sistemas distribuídos, a gente sabe que as coisas podem ficar meio caóticas. Imagina só: vários serviços se comunicando entre si, uma avalanche de requisições, e tudo isso espalhado por diferentes servidores e regiões. Manter tudo funcionando perfeitamente, sem nem saber exatamente o que está acontecendo por baixo do capô, é como dirigir um carro no escuro sem faróis. É aí que entra a observabilidade!
Observabilidade é como ligar os faróis e, de quebra, instalar um GPS para saber exatamente onde os problemas estão e como resolvê-los antes que se tornem desastres 🔥
Com ferramentas como Prometheus e OpenTelemetry, a gente consegue coletar métricas, traces distribuídos e ter uma visão completa do comportamento da aplicação. É basicamente um superpoder para desenvolvedores que lidam com sistemas complexos.
Neste artigo, vou te mostrar como criar uma aplicação Go real, e integrar com OpenTelemetry para coletar métricas e traces. Além disso, vamos expor esses dados para o Prometheus e usar o Jaeger para visualizar todos os traces bonitinhos. Tudo isso seguindo as melhores práticas para que sua arquitetura seja escalável e fácil de manter. Bora lá?
Objetivos do nosso app
- Criar uma aplicação Go real usando o Echo Framework.
- Integrar o OpenTelemetry para coleta de métricas e traces.
- Configurar a exportação de métricas para o Prometheus.
- Aplicar as melhores práticas para uma arquitetura escalável.
Estrutura do Projeto
.
├── cmd
│ └── service
│ └── main.go
├── internal
│ ├── observability
│ │ ├── metrics.go
│ │ └── tracing.go
│ └── server
│ └── main.go
├── pkg
│ └── api
│ └── handlers.go
├── Dockerfile
├── compose.yml
├── go.mod
├── go.sum
└── prometheus.yml
Lembrando que você encontra esse projeto no github
Configuração de Métricas com Prometheus
Por que Prometheus?
Prometheus é amplamente utilizado para monitoramento devido à sua capacidade de coletar métricas numéricas de séries temporais. Isso é essencial para analisar a performance em tempo real da sua aplicação, permitindo agir rapidamente ao detectar anomalias.
Implementação de Métricas
No arquivo internal/observability/metrics.go
, vamos configurar um Prometheus Exporter que coleta métricas de performance.
package observability
import (
"context"
"log"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/prometheus"
m "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
)
var (
meterProvider *metric.MeterProvider
requestCounter m.Int64Counter
activeRequests m.Int64UpDownCounter
requestHistogram m.Float64Histogram
customRequestCount m.Int64Counter
)
func InitMeterProvider() {
exporter, err := prometheus.New()
if err != nil {
log.Fatalf("Prometheus exporter: %v", err)
}
meterProvider = metric.NewMeterProvider(
metric.WithReader(exporter),
metric.WithResource(resource.Default()),
)
otel.SetMeterProvider(meterProvider)
meter := meterProvider.Meter("observability-api")
requestCounter, err = meter.Int64Counter(
"http_requests_total",
m.WithDescription("Total number of HTTP requests processed"),
)
if err != nil {
log.Fatalf("Failed to create request counter: %v", err)
}
activeRequests, err = meter.Int64UpDownCounter(
"http_active_requests",
m.WithDescription("Number of active HTTP requests being processed"),
)
if err != nil {
log.Fatalf("Failed to create active request gauge: %v", err)
}
requestHistogram, err = meter.Float64Histogram(
"http_request_duration_seconds",
m.WithDescription("The distribution of request durations in seconds"),
)
if err != nil {
log.Fatalf("Failed to create request duration histogram: %v", err)
}
customRequestCount, err = meter.Int64Counter(
"custom_request_count",
m.WithDescription("Custom request count for specific endpoints"),
)
if err != nil {
log.Fatalf("Failed to create request counter: %v", err)
}
}
func RecordRequestMetrics(ctx context.Context, duration time.Duration) {
requestCounter.Add(ctx, 1)
activeRequests.Add(ctx, 1)
defer activeRequests.Add(ctx, -1)
requestHistogram.Record(ctx, duration.Seconds())
}
func RecordCustomRequestMetrics(ctx context.Context) {
customRequestCount.Add(ctx, 1)
}
O que fizemos nesse "carinha":
Prometheus Exporter: Utilizamos o prometheus.New()
para configurar o Prometheus Exporter, que atua capturando as métricas geradas pela aplicação e as expondo no formato compreensível para o Prometheus. Esse processo garante que as métricas estejam disponíveis para coleta e monitoramento em tempo real.
MeterProvider: O MeterProvider é o componente principal responsável pela captura e registro das métricas da aplicação. Usamos o metric.NewMeterProvider()
para criar o provider, e o otel.SetMeterProvider()
para defini-lo como o padrão global da aplicação. Isso assegura que todos os componentes e rotas da aplicação possam gerar e exportar métricas de forma centralizada. Através do Meter, que é obtido a partir do MeterProvider, foram criados diversos instrumentos de medição, como contadores e histogramas.
Métricas Criadas:
- requestCounter: Contador que monitora o número total de requisições HTTP processadas.
- activeRequests: Um UpDownCounter que rastreia o número de requisições ativas em um dado momento.
- requestHistogram: Histograma que mede a duração das requisições HTTP em segundos, permitindo monitorar a performance da aplicação com base na latência das requisições.
- customRequestCount: Um contador personalizado para rastrear requisições feitas a endpoints específicos da aplicação. Essas métricas permitem monitorar o comportamento da aplicação de forma detalhada, coletando dados sobre o tráfego e a performance para posterior análise no Prometheus.
Implementando Tracing
Por que usar Jaeger?
Em sistemas distribuídos, como uma arquitetura de microserviços, é difícil identificar onde um problema de performance está ocorrendo. Tracing distribuído é fundamental para entender o ciclo de vida de uma requisição que passa por múltiplos serviços. Jaeger permite visualizar essa jornada, facilitando o diagnóstico de gargalos e falhas.
Implementação de Tracing em Go
No arquivo internal/observability/tracing.go
, configuraremos o Jaeger como o Tracing Exporter.
package observability
import (
"context"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func InitTracer() *sdktrace.TracerProvider {
client := otlptracehttp.NewClient()
exporter, err := otlptrace.New(context.Background(), client)
if err != nil {
log.Fatalf("Error creating OTLP exporter: %v", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
"",
attribute.String("service.name", "observability-api"),
attribute.String("environment", "development"),
attribute.String("version", "1.0.0"),
)),
)
otel.SetTracerProvider(tp)
return tp
}
func ShutdownTracerProvider(tp *sdktrace.TracerProvider) error {
return tp.Shutdown(context.Background())
}
Detalhes da implementação:
O OTLP é o protocolo de exportação do OpenTelemetry, que pode ser utilizado para enviar traces para backends de observabilidade, incluindo Jaeger, Zipkin ou outros compatíveis com o OTLP.
Estamos usando o cliente otlptracehttp.NewClient()
para configurar o transporte HTTP, o que é útil para sistemas distribuídos onde a exportação via HTTP é necessária para compatibilidade e flexibilidade.
trace.NewTracerProvider
:
WithBatcher: Usamos a exportação em lote para otimizar o envio de dados de tracing, diminuindo a carga de rede.
resource.NewSchemaless
: Estamos associando atributos relevantes à nossa aplicação, como o nome do serviço (service.name), o ambiente (environment), e a versão da aplicação (version). Isso ajuda a organizar e filtrar os traces nos backends de tracing.
ShutdownTracerProvider:
Garantimos o shutdown adequado do TracerProvider
para garantir que todos os spans (traces) sejam exportados antes de finalizar o processo, evitando a perda de dados.
Registrando as Rotas e Handlers da Aplicação
Agora que temos a observabilidade configurada, vamos instrumentar as rotas da aplicação para capturar métricas e traces. No arquivo pkg/api/handlers.go
, definiremos os endpoints e instrumentaremos as requisições.
package api
import (
"math/rand"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
)
type Response struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func RegisterRoutes(e *echo.Echo) {
e.GET("/", HomeHandler)
e.GET("/metrics", MetricsHandler())
e.GET("/process", ProcessHandler)
}
func HomeHandler(c echo.Context) error {
return c.JSON(http.StatusOK, &Response{Message: "Hello, World!"})
}
func ProcessHandler(c echo.Context) error {
_, span := otel.Tracer("process-tracer").Start(c.Request().Context(), "ProcessData")
defer span.End()
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
if rand.Intn(100) < 20 {
span.SetStatus(codes.Error, "error")
response := &Response{
Message: "Error during processing",
}
return c.JSON(http.StatusInternalServerError, response)
}
response := &Response{
Message: "completed",
Data: map[string]interface{}{
"processed_at": time.Now().Format(time.RFC3339),
"duration_ms": rand.Intn(200),
},
}
return c.JSON(http.StatusOK, response)
}
-
IncrementRequestCount: Instrumentamos o handler HomeHandler para incrementar o contador personalizado de requisições sempre que o endpoint raiz (
/
) for chamado. Isso é útil para monitorar o volume de requisições que a aplicação está recebendo. - Tracing no ProcessHandler: No ProcessHandler, utilizamos o tracing distribuído para capturar o ciclo de vida de uma requisição. Criamos um span e simulamos um tempo de latência, além de simular erros em 20% das requisições.
Expondo Métricas para o Prometheus
Para que o Prometheus possa monitorar sua aplicação, você precisa expor as métricas. No Echo, isso é feito utilizando o promhttp.Handler
.
func MetricsHandler() echo.HandlerFunc {
return echo.WrapHandler(promhttp.Handler())
}
Esse handler é registrado na rota /metrics
, permitindo que o Prometheus "scrape" as métricas no formato que ele entende.
Configurando o Prometheus
Hora de criamos o arquivo que passará as configurações para o Prometheus. Nele colocaremos o intervalo de atualização de nossas métricas e o target que iremos atingir:
# prometheus.yml
global:
scrape_interval: 5s
scrape_configs:
- job_name: 'observability-api'
static_configs:
- targets: ['app:8080']
Subindo nosso app com Docker
Lógico que não poderiamos deixar de lado nosso grande amigo Docker, ele nos ajudará a subir todas as dependencias do projeto, em apenas um lugar e sem ter que instalar nada. Apenas utilizando o docker compose
.
Dockerfile da aplicação
# syntax=docker/dockerfile:1
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o obv-api ./cmd/service
FROM scratch
WORKDIR /app
COPY --from=builder /app/obv-api .
EXPOSE 8080
CMD ["./obv-api"]
Docker compose
services:
app:
build: .
ports:
- "8080:8080"
networks:
- observability
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
networks:
- observability
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686"
- "14268:14268"
networks:
- observability
networks:
observability:
driver: bridge
Testando a aplicação
Agora que implementamos o TracerProvider com OTLP, configuramos o Prometheus para métricas e usamos o Jaeger para tracing distribuído, o próximo passo é testar essa aplicação para garantir que a observabilidade esteja funcionando corretamente.
1. Subir os serviços com Docker Compose
Navegue até a pasta do projeto onde o arquivo compose.yml
está localizado.
Execute o comando a seguir para compilar e iniciar os serviços:
docker compose up --build
Isso vai:
- Compilar a aplicação Go.
- Iniciar a aplicação Go na porta 8080.
- Iniciar o Prometheus na porta 9090 para coleta de métricas.
- Iniciar o Jaeger com suporte ao OTLP para coleta de traces distribuídos.
2. Simulando tráfego na aplicação
Agora que todos os serviços estão em execução, precisamos gerar tráfego na aplicação para que métricas e traces sejam coletados e exportados.
Simulando requisições via cURL
Abra um terminal separado e execute o seguinte script bash para simular requisições contínuas à aplicação Go:
while true; do
curl -s http://localhost:8080/
curl -s http://localhost:8080/process
sleep 1
done
Comportamento Esperado
- As requisições na rota
/
vão gerar métricas que o Prometheus irá coletar, como o número total de requisições. - As requisições na rota
/process
vão gerar spans que serão enviados para o Jaeger para visualização do tracing distribuído.
Verificando as métricas no Prometheus
Depois de gerar tráfego, você pode verificar se o Prometheus está coletando as métricas expostas pela aplicação.
Abra o prometheus em seu navegador http://localhost:9090
. Vamos começar a consultar os dados.
Consultas Prometheus
Aqui estão algumas consultas para verificar as métricas personalizadas e as expostas por padrão:
- Contador de Requisições Personalizado No Prometheus, execute a seguinte consulta para ver o contador de requisições personalizado que criamos:
custom_request_count_total
Essa consulta mostrará o número total de requisições recebidas pela rota raiz da aplicação.
- Métricas HTTP Padrão Além do contador personalizado, você pode verificar as métricas padrão de latência e contagem de requisições HTTP:
http_server_duration_seconds_count
Essa métrica mostra a contagem total de requisições HTTP processadas pela aplicação e o tempo total que elas levaram.
Visualizando as Métricas
Após rodar as consultas, o Prometheus mostrará gráficos e valores das métricas coletadas, permitindo que você monitore o comportamento da aplicação.
Conclusão
Neste artigo, implementamos observabilidade em uma aplicação Go utilizando OpenTelemetry, Prometheus e Jaeger. Exploramos o motivo de usar cada uma dessas ferramentas e detalhamos como configurar métricas e tracing de forma eficiente, expondo essas informações para sistemas de monitoramento como o Prometheus e o Jaeger.
Com essas ferramentas em mãos, você será capaz de ter uma visão clara e abrangente do comportamento da sua aplicação, identificando gargalos de performance e diagnosticando problemas de maneira eficiente em sistemas distribuídos.
Essa configuração pode ser expandida e personalizada para qualquer aplicação Go, permitindo que você escale sua solução de monitoramento à medida que sua infraestrutura cresce.
Top comments (3)
vc é foda!!!!!!!!!!1
Top artigo rafa <3
Valeu Fer!