DEV Community

Cover image for Desbloqueie a Observabilidade: Guia prático com OpenTelemetry e Prometheus
Rafael Pazini
Rafael Pazini

Posted on

Desbloqueie a Observabilidade: Guia prático com OpenTelemetry e Prometheus

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

  1. Criar uma aplicação Go real usando o Echo Framework.
  2. Integrar o OpenTelemetry para coleta de métricas e traces.
  3. Configurar a exportação de métricas para o Prometheus.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
  • 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())
}
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Isso vai:

  1. Compilar a aplicação Go.
  2. Iniciar a aplicação Go na porta 8080.
  3. Iniciar o Prometheus na porta 9090 para coleta de métricas.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Prometheus Dashboard

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 (1)

Collapse
 
lys profile image
Lys

vc é foda!!!!!!!!!!1