DEV Community

Cover image for Implementando Observabilidade em Aplicações Go com OpenTelemetry, Prometheus, Loki, Tempo e Grafana
Vinícius Boscardin
Vinícius Boscardin

Posted on

Implementando Observabilidade em Aplicações Go com OpenTelemetry, Prometheus, Loki, Tempo e Grafana

Vamos mergulhar no mundo da observabilidade em aplicações Go. Se você já se perguntou como monitorar o que está acontecendo nos bastidores do seu código, está no lugar certo!

A observabilidade é como ter superpoderes para entender o comportamento e a saúde das suas aplicações, especialmente quando estão rodando em produção. Com as ferramentas certas, podemos detectar e resolver problemas antes que eles afetem seus usuários.

Vamos usar uma combinação poderosa de ferramentas: OpenTelemetry, Prometheus, Loki, Tempo e Grafana. Cada uma delas tem um papel especial na coleta, armazenamento e visualização dos dados que importam.

Ferramentas de Observabilidade Utilizadas

OpenTelemetry

Um framework de código aberto que fornece APIs, bibliotecas e agentes para coletar dados de telemetria, como métricas, logs e traces. Ele é a base para instrumentar seu código, permitindo que você colete dados de desempenho e comportamento de suas aplicações.

Prometheus

Uma ferramenta de monitoramento e alerta que coleta e armazena métricas em um banco de dados de séries temporais. Ele é ideal para monitorar a performance de aplicações, oferecendo uma linguagem de consulta poderosa para analisar dados e criar alertas baseados em condições específicas.

Loki

Uma solução de agregação de logs que funciona de forma semelhante ao Prometheus, mas para logs. Ele permite que você colete, armazene e consulte logs de forma eficiente, sem a necessidade de indexação completa, tornando-o uma solução leve e escalável para gerenciamento de logs.

Tempo

Uma ferramenta de rastreamento distribuído que permite visualizar e analisar traces de suas aplicações. Com o Tempo, você pode entender o fluxo de requisições através de diferentes serviços, identificar gargalos e otimizar o desempenho de suas aplicações.

Grafana

Uma plataforma de visualização de dados que se integra com Prometheus, Loki, Tempo e outras fontes de dados. Ele permite criar dashboards interativos e visualizações personalizadas para monitorar métricas, logs e traces em tempo real, facilitando a análise e o diagnóstico de problemas.

Vamos Colocar a Mão na Massa!

Agora que já conhecemos as ferramentas que vamos usar, é hora de colocar a mão na massa e começar a implementar a observabilidade na nossa aplicação Go. Vamos configurar o ambiente, instrumentar nosso código e integrar tudo para que possamos monitorar e analisar o desempenho da nossa aplicação em tempo real.

Pré-requisitos

Antes de começarmos, certifique-se de que você tem o Go e o Docker instalados em sua máquina. Eles são essenciais para configurar o ambiente de desenvolvimento e executar os serviços necessários para implementar a observabilidade.

Guia de Instalação do Go

Guia de Instalação do Docker

Desafio de Implementação

  1. Service1 ( http://localhost:8081/service1 ):

    • Função: Service1 recebe uma requisição do cliente. Ele processa a requisição e faz uma chamada para Service2.
    • Resposta: Após receber a resposta de Service2, Service1 retorna uma resposta ao cliente com um objeto JSON contendo a palavra concatenada, por exemplo, {"word": "CBA"} .
  2. Service2 ( http://localhost:8081/service2 ):

    • Função: Service2 recebe uma requisição de Service1. Ele processa a requisição e faz uma chamada para Service3.
    • Resposta: Após receber a resposta de Service3, Service2 retorna uma resposta para Service1 com um objeto JSON contendo a palavra concatenada, por exemplo, {"word": "CB"} .
  3. Service3 ( http://localhost:8081/service3 ):

    • Função: Service3 recebe uma requisição de Service2. Ele processa a requisição e retorna uma resposta para Service2.
    • Resposta: Service3 retorna um objeto JSON contendo uma palavra, por exemplo, {"word": "C"} .

flow

Fluxo de Requisições

  • O cliente inicia o fluxo enviando uma requisição para Service1.
  • Service1 processa a requisição e encaminha para Service2.
  • Service2 processa a requisição e encaminha para Service3.
  • Service3 processa a requisição e retorna uma resposta para Service2.
  • Service2 recebe a resposta de Service3, processa e retorna para Service1.
  • Service1 recebe a resposta de Service2, processa e retorna para o cliente.

Basicamente, cada serviço gera uma palavra com um número específico de caracteres e a concatena com a resposta recebida do próximo serviço. Isso cria uma cadeia de palavras que é passada de um serviço para o outro, até que a resposta final seja enviada de volta ao cliente.

diagram

Arquitetura do Projeto

O projeto foi estruturado utilizando o padrão de arquitetura Ports and Adapters, também conhecido como Arquitetura Hexagonal. Esse padrão promove a separação de preocupações, permitindo que a lógica de negócio seja isolada de detalhes de implementação, como frameworks e bibliotecas externas. Aqui está uma descrição de como a estrutura e a arquitetura do projeto foram montadas:

├── Dockerfile
├── LICENSE
├── cmd
│   ├── cmd
│   │   ├── root.go
│   │   └── serve.go
│   └── main.go
├── config-files
│   ├── loki.yaml
│   ├── otel.yaml
│   ├── prometheus.yaml
│   └── tempo.yaml
├── docker-compose.yaml
├── go.mod
├── go.sum
├── internals
│   ├── domain
│   │   ├── core
│   │   │   └── domain
│   │   │       └── example.go
│   │   └── usecase
│   │       └── example.go
│   └── infra
│       ├── controller
│       │   └── example.go
│       └── repository
│           └── example_http.go
├── pkg
│   ├── adapter
│   │   ├── instrumentation
│   │   │   └── otel.go
│   │   ├── metric
│   │   │   └── metric.go
│   │   └── rest
│   │       └── rest.go
│   └── di
│       └── example.go
└── post.md
Enter fullscreen mode Exit fullscreen mode

Estrutura do Projeto

cmd : Contém o ponto de entrada da aplicação, que é implementado usando o cobra-cli . O arquivo serve.go define o comando para iniciar o serviço HTTP instrumentado com OpenTelemetry.

./cmd/cmd/serve.go

package cmd

import (
    "context"
    "time"

    "github.com/booscaaa/observability-go-example/pkg/adapter/instrumentation"
    "github.com/booscaaa/observability-go-example/pkg/adapter/metric"
    "github.com/booscaaa/observability-go-example/pkg/adapter/rest"
    "github.com/spf13/cobra"
    "go.opentelemetry.io/otel"
)

var serveCmd = &cobra.Command{
    Use:   "serve",
    Short: "Start an OpenTelemetry-instrumented HTTP service",
    Long: `Starts an HTTP service that is instrumented with OpenTelemetry for observability.

This service exposes metrics, traces, and logs that can be collected and analyzed
by OpenTelemetry collectors. The service responds with its name and demonstrates
distributed tracing capabilities when called through the Traefik reverse proxy.

Example usage:
  observability-go-example serve`,
    Run: func(cmd *cobra.Command, args []string) {
        shutdown, err := instrumentation.Initialize(cmd.Context())
        if err != nil {
            panic(err)
        }

        defer func() {
            ctx, cancel := context.WithTimeout(cmd.Context(), time.Second*5)
            defer cancel()
            if err := shutdown(ctx); err != nil {
                panic("failed to shutdown TracerProvider")
            }
        }()

        metric.Initialize(otel.GetMeterProvider().Meter(instrumentation.Name))

        rest.Initialize()
    },
}

func init() {
    rootCmd.AddCommand(serveCmd)
}
Enter fullscreen mode Exit fullscreen mode

config-files : Armazena arquivos de configuração para as ferramentas de observabilidade, como Loki, OpenTelemetry, Prometheus e Tempo. Esses arquivos definem como cada ferramenta deve ser configurada e integrada ao projeto.

./config-files/loki.yaml

auth_enabled: false

server:
  http_listen_port: 3100

ingester:
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
  chunk_idle_period: 5m
  chunk_retain_period: 30s

schema_config:
  configs:
    - from: 2020-10-24
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  boltdb_shipper:
    active_index_directory: /loki/index
    cache_location: /loki/index_cache
    shared_store: filesystem

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h

chunk_store_config:
  max_look_back_period: 0s

Enter fullscreen mode Exit fullscreen mode

./config-files/otel.yaml

receivers:
  otlp:
    protocols:
      http:
        endpoint: "0.0.0.0:4318"
      grpc:
        endpoint: "0.0.0.0:4317"

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"
    resource_to_telemetry_conversion:
      enabled: true

  loki:
    endpoint: "http://loki:3100/loki/api/v1/push"

  otlp:
    endpoint: "tempo:4319"
    tls:
      insecure: true

processors:
  batch:

service:
  telemetry:
    metrics:
      level: detailed


  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp]

    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]

    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [loki]
Enter fullscreen mode Exit fullscreen mode

./config-files/prometheus.yaml

global:
  scrape_interval: 10s

scrape_configs:
  - job_name: "otel-collector"
    static_configs:
      - targets: ["otel-collector:8889"]
  - job_name: "tempo"
    static_configs:
      - targets: ["tempo:3200"]
Enter fullscreen mode Exit fullscreen mode

./config-files/tempo.yaml

server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: "0.0.0.0:4319"
        http:
  forwarders:
    - name: otlp
      backend: otlpgrpc
      otlpgrpc:
        tls:
          insecure: true

storage:
  trace:
    backend: local
    local:
      path: /var/tempo/traces
Enter fullscreen mode Exit fullscreen mode

internals : Contém a lógica de negócio e a infraestrutura da aplicação.

  • domain : Define as entidades e interfaces principais do domínio da aplicação. Por exemplo, Example representa uma entidade de domínio.
  • usecase : Implementa os casos de uso da aplicação, que são as operações principais que a aplicação pode realizar.
  • infra : Contém a implementação dos controladores e repositórios que interagem com o mundo externo.

./internals/domain/core/domain/example.go

package domain

import (
    "context"
    "net/http"
)

type Example struct {
    Word string `json:"word"`
}

type ExampleHttpRepository interface {
    GetExample(context.Context) (*Example, error)
}

type ExampleUseCase interface {
    GetExample(context.Context) (*Example, error)
}

type ExampleController interface {
    GetExample(http.ResponseWriter, *http.Request)
}

func (example *Example) ConcatenateWord(word string) {
    example.Word += word
}

Enter fullscreen mode Exit fullscreen mode

./internals/core/usecase/example.go

package usecase

import (
    "context"
    "os"

    "math/rand"

    "github.com/booscaaa/observability-go-example/internals/domain/core/domain"
    "github.com/booscaaa/observability-go-example/pkg/adapter/instrumentation"
)

type exampleUseCase struct {
    exampleHttpRepository domain.ExampleHttpRepository
}

func (usecase *exampleUseCase) GetExample(ctx context.Context) (*domain.Example, error) {
    ctx, span := instrumentation.Tracer.Start(ctx, "usecase.GetExample")
    defer span.End()

    instrumentation.Logger.InfoContext(ctx, "Processing example use case")

    url := os.Getenv("SERVICE_CALL_URL")

    length := rand.Intn(10) + 1
    letters := make([]rune, length)
    for i := range letters {
        letters[i] = rune('A' + rand.Intn(26))
    }
    word := string(letters)

    if url == "" {
        instrumentation.Logger.InfoContext(ctx, "No service URL configured, returning local letter",
            "word", word,
        )
        return &domain.Example{
            Word: word,
        }, nil
    }

    example, err := usecase.exampleHttpRepository.GetExample(ctx)
    if err != nil {
        instrumentation.Logger.ErrorContext(ctx, "Failed to get example from repository",
            "error", err,
            "url", url,
        )
        span.RecordError(err)
        return nil, err
    }

    example.ConcatenateWord(word)
    instrumentation.Logger.InfoContext(ctx, "Successfully processed example",
        "final_word", example.Word,
    )

    return example, nil
}

func NewExampleHttpRepository(exampleHttpRepository domain.ExampleHttpRepository) domain.ExampleUseCase {
    return &exampleUseCase{
        exampleHttpRepository: exampleHttpRepository,
    }
}

Enter fullscreen mode Exit fullscreen mode

./internals/infra/controller/example.go

package controller

import (
    "encoding/json"
    "net/http"
    "time"

    "github.com/booscaaa/observability-go-example/internals/domain/core/domain"
    "github.com/booscaaa/observability-go-example/pkg/adapter/instrumentation"
    "github.com/booscaaa/observability-go-example/pkg/adapter/metric"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/propagation"
)

type exampleController struct {
    exampleUseCase domain.ExampleUseCase
}

func (controller *exampleController) GetExample(response http.ResponseWriter, request *http.Request) {
    ctx := request.Context()
    propagator := otel.GetTextMapPropagator()
    ctx = propagator.Extract(ctx, propagation.HeaderCarrier(request.Header))

    ctx, span := instrumentation.Tracer.Start(ctx, "controller.GetExample")
    defer span.End()

    start := time.Now()
    metric.RequestInFlight.Add(ctx, 1)
    metric.RequestCounter.Add(ctx, 1)
    defer func() {
        metric.RequestInFlight.Add(ctx, -1)
        metric.RequestDuration.Record(ctx, time.Since(start).Seconds())
    }()

    instrumentation.Logger.InfoContext(ctx, "Processing request",
        "method", request.Method,
        "path", request.URL.Path,
    )

    example, err := controller.exampleUseCase.GetExample(ctx)
    if err != nil {
        instrumentation.Logger.ErrorContext(ctx, "Failed to get example",
            "error", err,
            "method", request.Method,
            "path", request.URL.Path,
            "status", http.StatusInternalServerError,
        )
        span.RecordError(err)
        response.WriteHeader(http.StatusInternalServerError)
        response.Write([]byte(err.Error()))
        return
    }

    metric.LetterCounter.Add(ctx, 1)
    metric.WordLengthGauge.Record(ctx, int64(len(example.Word)))

    response.WriteHeader(http.StatusOK)
    err = json.NewEncoder(response).Encode(example)
    if err != nil {
        instrumentation.Logger.ErrorContext(ctx, "Failed to encode response",
            "error", err,
            "method", request.Method,
            "path", request.URL.Path,
            "status", http.StatusInternalServerError,
        )
        span.RecordError(err)
        response.WriteHeader(http.StatusInternalServerError)
        response.Write([]byte(err.Error()))
        return
    }
}

func NewExampleController(exampleUseCase domain.ExampleUseCase) domain.ExampleController {
    return &exampleController{
        exampleUseCase: exampleUseCase,
    }
}

Enter fullscreen mode Exit fullscreen mode

./internals/infra/repository/example_http.go

package repository

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/booscaaa/observability-go-example/internals/domain/core/domain"
    "github.com/booscaaa/observability-go-example/pkg/adapter/instrumentation"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/propagation"
)

type exampleHttpRepository struct{}

func (repository *exampleHttpRepository) GetExample(ctx context.Context) (*domain.Example, error) {
    ctx, span := instrumentation.Tracer.Start(ctx, "repository.GetExample")
    defer span.End()

    time.Sleep(10 * time.Second)

    url := os.Getenv("SERVICE_CALL_URL")
    instrumentation.Logger.InfoContext(ctx, "Making HTTP request", "url", url)

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)

    if err != nil {
        instrumentation.Logger.ErrorContext(ctx, "Failed to create request",
            "error", err,
            "url", url,
        )
        span.RecordError(err)
        return nil, err
    }

    propagator := otel.GetTextMapPropagator()
    propagator.Inject(ctx, propagation.HeaderCarrier(req.Header))

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        instrumentation.Logger.ErrorContext(ctx, "Failed to execute request",
            "error", err,
            "url", url,
        )
        span.RecordError(err)
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        err := fmt.Errorf("unexpected status code: %d", resp.StatusCode)
        instrumentation.Logger.ErrorContext(ctx, "Received non-200 status code",
            "error", err,
            "status_code", resp.StatusCode,
            "url", url,
        )
        span.RecordError(err)
        return nil, err
    }

    var exampleResponse domain.Example
    if err := json.NewDecoder(resp.Body).Decode(&exampleResponse); err != nil {
        instrumentation.Logger.ErrorContext(ctx, "Failed to decode response",
            "error", err,
            "url", url,
        )
        span.RecordError(err)
        return nil, err
    }

    instrumentation.Logger.InfoContext(ctx, "Successfully retrieved example",
        "word", exampleResponse.Word,
    )

    return &exampleResponse, nil
}

func NewExampleHttpRepository() domain.ExampleHttpRepository {
    return &exampleHttpRepository{}
}

Enter fullscreen mode Exit fullscreen mode

pkg : Contém adaptadores para entrada e saída de dados, além da configuração de injeção de dependência.

  • adapter : Implementa a instrumentação, métricas e endpoints REST.
  • di : Configura a injeção de dependência, criando instâncias dos controladores e casos de uso.

./pkg/adapter/instrumentation/instrumentation.go

package instrumentation

import (
    "context"
    "errors"
    "os"
    "time"

    "go.opentelemetry.io/contrib/bridges/otelslog"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
    "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    "go.opentelemetry.io/otel/log/global"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/log"
    "go.opentelemetry.io/otel/sdk/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)

const Name = "github.com/booscaa/observability-go-example/example-word"

var (
    Tracer = otel.Tracer(Name)
    Meter  = otel.Meter(Name)
    Logger = otelslog.NewLogger(Name)
)

func Initialize(ctx context.Context) (func(context.Context) error, error) {
    return setupOTelSDK(ctx)
}

func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
    var shutdownFuncs []func(context.Context) error

    shutdown = func(ctx context.Context) error {
        var err error
        for _, fn := range shutdownFuncs {
            err = errors.Join(err, fn(ctx))
        }
        shutdownFuncs = nil
        return err
    }

    handleErr := func(inErr error) {
        err = errors.Join(inErr, shutdown(ctx))
    }

    res := resource.NewWithAttributes(
        semconv.SchemaURL,
        semconv.ServiceNameKey.String(os.Getenv("SERVICE_NAME")),
        semconv.ServiceVersionKey.String("1.0.0"),
        semconv.ServiceInstanceIDKey.String("abcdef12345"),
    )

    prop := newPropagator()
    otel.SetTextMapPropagator(prop)

    tracerProvider, err := newTraceProvider(res)
    if err != nil {
        handleErr(err)
        return
    }
    shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
    otel.SetTracerProvider(tracerProvider)

    meterProvider, err := newMeterProvider(res)
    if err != nil {
        handleErr(err)
        return
    }
    shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
    otel.SetMeterProvider(meterProvider)

    loggerProvider, err := newLoggerProvider(res)
    if err != nil {
        handleErr(err)
        return
    }
    shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown)
    global.SetLoggerProvider(loggerProvider)

    return
}

func newPropagator() propagation.TextMapPropagator {
    return propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{},
        propagation.Baggage{},
    )
}

func newTraceProvider(resource *resource.Resource) (*trace.TracerProvider, error) {
    traceExporter, err := otlptracehttp.New(
        context.Background(),
        otlptracehttp.WithInsecure(),
        otlptracehttp.WithEndpoint("otel-collector:4318"),
    )
    if err != nil {
        return nil, err
    }

    traceProvider := trace.NewTracerProvider(
        trace.WithBatcher(traceExporter,
            trace.WithBatchTimeout(time.Second)),
        trace.WithResource(resource),
    )
    return traceProvider, nil
}

func newMeterProvider(resource *resource.Resource) (*metric.MeterProvider, error) {
    metricExporter, err := otlpmetrichttp.New(
        context.Background(),
        otlpmetrichttp.WithInsecure(),
        otlpmetrichttp.WithEndpoint("otel-collector:4318"),
    )
    if err != nil {
        return nil, err
    }

    meterProvider := metric.NewMeterProvider(
        metric.WithReader(metric.NewPeriodicReader(metricExporter,
            metric.WithInterval(3*time.Second)),
        ),
        metric.WithResource(resource),
    )
    return meterProvider, nil
}

func newLoggerProvider(resource *resource.Resource) (*log.LoggerProvider, error) {
    logExporter, err := otlploghttp.New(
        context.Background(),
        otlploghttp.WithInsecure(),
        otlploghttp.WithEndpoint("otel-collector:4318"),
    )
    if err != nil {
        return nil, err
    }

    loggerProvider := log.NewLoggerProvider(
        log.WithProcessor(log.NewBatchProcessor(logExporter)),
        log.WithResource(resource),
    )
    return loggerProvider, nil
}
Enter fullscreen mode Exit fullscreen mode

./pkg/adapter/metric/metric.go

package metric

import "go.opentelemetry.io/otel/metric"

var (
    // Request metrics
    RequestCounter  metric.Int64Counter
    RequestDuration metric.Float64Histogram
    RequestInFlight metric.Int64UpDownCounter

    // Business metrics
    LetterCounter   metric.Int64Counter
    WordLengthGauge metric.Int64Gauge
)

func Initialize(meter metric.Meter) error {
    var err error

    RequestCounter, err = meter.Int64Counter(
        "request_total",
        metric.WithDescription("Total number of requests processed"),
        metric.WithUnit("1"),
    )
    if err != nil {
        return err
    }

    RequestDuration, err = meter.Float64Histogram(
        "request_duration_seconds",
        metric.WithDescription("Duration of requests"),
        metric.WithUnit("s"),
    )
    if err != nil {
        return err
    }

    RequestInFlight, err = meter.Int64UpDownCounter(
        "requests_in_flight",
        metric.WithDescription("Current number of requests being processed"),
        metric.WithUnit("1"),
    )
    if err != nil {
        return err
    }

    LetterCounter, err = meter.Int64Counter(
        "letter_concatenations_total",
        metric.WithDescription("Total number of letter concatenations"),
        metric.WithUnit("1"),
    )
    if err != nil {
        return err
    }

    WordLengthGauge, err = meter.Int64Gauge(
        "word_length",
        metric.WithDescription("Current length of the word"),
        metric.WithUnit("chars"),
    )
    if err != nil {
        return err
    }

    return nil
}

Enter fullscreen mode Exit fullscreen mode

./pkg/adapter/rest/rest.go

package rest

import (
    "net/http"

    "github.com/booscaaa/observability-go-example/pkg/di"
)

func Initialize() {
    exampleController := di.NewExampleController()
    mux := http.NewServeMux()

    mux.HandleFunc("/", exampleController.GetExample)
    http.ListenAndServe(":8080", mux)
}

Enter fullscreen mode Exit fullscreen mode

./pkg/di/di.go

package di

import (
    "github.com/booscaaa/observability-go-example/internals/domain/core/domain"
    "github.com/booscaaa/observability-go-example/internals/domain/usecase"
    "github.com/booscaaa/observability-go-example/internals/infra/controller"
    "github.com/booscaaa/observability-go-example/internals/infra/repository"
)

func NewExampleController() domain.ExampleController {
    exampleHttpRepository := repository.NewExampleHttpRepository()
    exampleUseCase := usecase.NewExampleHttpRepository(exampleHttpRepository)
    return controller.NewExampleController(exampleUseCase)
}

Enter fullscreen mode Exit fullscreen mode

docker-compose.yaml : Define os serviços Docker necessários para executar a aplicação e as ferramentas de observabilidade. Inclui serviços como Traefik, Prometheus, Tempo, Loki, OpenTelemetry Collector e Grafana.

./docker-compose.yaml

services:
  traefik:
    container_name: traefik
    image: traefik:v2.10
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:8081"
    ports:
      - "8080:8080"
      - "8081:8081"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - observability-network

  service1:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ${SERVICE1_NAME}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.service1.rule=PathPrefix(`/service1`)"
      - "traefik.http.routers.service1.entrypoints=web"
      - "traefik.http.routers.service1.middlewares=strip-service1-prefix"
      - "traefik.http.middlewares.strip-service1-prefix.stripprefix.prefixes=/service1"
      - "traefik.http.services.service1.loadbalancer.server.port=8080"
    environment:
      - SERVICE_NAME=${SERVICE1_NAME}
      - SERVICE_CALL_URL=${SERVICE2_CALL_URL}
    networks:
      - observability-network

  service2:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ${SERVICE2_NAME}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.service2.rule=PathPrefix(`/service2`)"
      - "traefik.http.routers.service2.entrypoints=web"
      - "traefik.http.routers.service2.middlewares=strip-service2-prefix"
      - "traefik.http.middlewares.strip-service2-prefix.stripprefix.prefixes=/service2"
      - "traefik.http.services.service2.loadbalancer.server.port=8080"
    environment:
      - SERVICE_NAME=${SERVICE2_NAME}
      - SERVICE_CALL_URL=${SERVICE3_CALL_URL}
    networks:
      - observability-network

  service3:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ${SERVICE3_NAME}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.service3.rule=PathPrefix(`/service3`)"
      - "traefik.http.routers.service3.entrypoints=web"
      - "traefik.http.routers.service3.middlewares=strip-service3-prefix"
      - "traefik.http.middlewares.strip-service3-prefix.stripprefix.prefixes=/service3"
      - "traefik.http.services.service3.loadbalancer.server.port=8080"
    environment:
      - SERVICE_NAME=${SERVICE3_NAME}
    networks:
      - observability-network

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
      - ./config-files/prometheus.yaml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - observability-network

  tempo:
    image: grafana/tempo:latest
    container_name: tempo
    command: [ "-config.file=/etc/tempo.yaml" ]
    volumes:
      - ./config-files/tempo.yaml:/etc/tempo.yaml
      - tempo_data:/tmp/tempo
    ports:
      - "3200:3200"
      - "4319:4319"
    networks:
      - observability-network

  loki:
    image: grafana/loki:latest
    container_name: loki
    volumes:
      - ./config-files/loki.yaml:/etc/loki/loki-config.yaml
      - loki_data:/loki
    ports:
      - "3100:3100"
    networks:
      - observability-network

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    container_name: otel-collector
    command: [ "--config=/etc/otel-collector-config.yaml" ]
    volumes:
      - ./config-files/otel.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317" 
      - "4318:4318"
      - "8888:8888"
      - "8889:8889"
    networks:
      - observability-network
    depends_on:
      - tempo
      - loki
      - prometheus

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    volumes:
      - grafana_data:/var/lib/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on:
      - prometheus
      - tempo
      - loki
    networks:
      - observability-network

networks:
  observability-network:
    driver: bridge

volumes:
  prometheus_data:
  tempo_data:
  loki_data:
  grafana_data:
Enter fullscreen mode Exit fullscreen mode

Dockerfile : Especifica como construir a imagem Docker da aplicação Go, incluindo a instalação de dependências e a compilação do binário.

./Dockerfile

FROM golang:1.23

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod tidy

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/main.go

EXPOSE 8080

CMD ["./main", "serve"]
Enter fullscreen mode Exit fullscreen mode

./go.mod

module github.com/booscaaa/observability-go-example

go 1.23.3

require (
    github.com/spf13/cobra v1.8.1
    go.opentelemetry.io/contrib/bridges/otelslog v0.9.0
    go.opentelemetry.io/otel v1.34.0
    go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0
    go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0
    go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0
    go.opentelemetry.io/otel/log v0.10.0
    go.opentelemetry.io/otel/metric v1.34.0
    go.opentelemetry.io/otel/sdk v1.34.0
    go.opentelemetry.io/otel/sdk/log v0.10.0
    go.opentelemetry.io/otel/sdk/metric v1.34.0
)
Enter fullscreen mode Exit fullscreen mode

Ports : Representadas pelas interfaces definidas no pacote domain . Elas definem os contratos que os adaptadores devem implementar para interagir com a lógica de negócio.
Adapters : Implementados nos pacotes infra e pkg . Eles adaptam as interfaces externas (como HTTP e OpenTelemetry) para as interfaces internas definidas no domínio.
Core : Contém a lógica de negócio central, implementada nos pacotes domain e usecase . Essa camada é independente de frameworks e bibliotecas externas, facilitando testes e manutenção.
Essa arquitetura permite que a aplicação seja facilmente extensível e testável, além de facilitar a integração com ferramentas de observabilidade para monitoramento e análise de desempenho.

Como Rodar o Projeto

Para executar o projeto, siga os passos abaixo:

  1. Configure as variáveis de ambiente: Crie um arquivo .env na raiz do projeto e defina as variáveis necessárias, como SERVICE1_NAME, SERVICE2_CALL_URL, etc.
SERVICE1_NAME=service1
SERVICE2_CALL_URL=http://traefik:8081/service2

SERVICE2_NAME=service2
SERVICE3_CALL_URL=http://traefik:8081/service3

SERVICE3_NAME=service3
Enter fullscreen mode Exit fullscreen mode
  1. Construa e inicie os serviços Docker: Certifique-se de que o Docker está em execução e execute o comando abaixo para construir e iniciar os serviços definidos no docker-compose.yaml:
   docker-compose up --build
Enter fullscreen mode Exit fullscreen mode
  1. Acesse o serviço:
    Após iniciar os serviços, você pode acessar o serviço principal em http://localhost:8081/service1.

  2. Parar os serviços:
    Para parar todos os serviços, execute:

   docker-compose down
Enter fullscreen mode Exit fullscreen mode

Certifique-se de que todas as dependências estão instaladas e que o Docker está configurado corretamente antes de iniciar o projeto.

Configuração das Ferramentas de Observabilidade

  1. Grafana:
  • Acesso: Após iniciar os serviços com Docker Compose, acesse o Grafana em http://localhost:3000 .
  • Login: Use as credenciais padrão ( admin / admin ) e altere a senha após o primeiro login.
  • Adicionar Fonte de Dados:

Criando Dashboards no Grafana

  1. Criar um Novo Dashboard:
  • No menu lateral, clique em "+" e selecione "Dashboard".
  • Clique em "Add new panel" para adicionar um painel.
  1. Configurar Painéis:
  • Métricas: Use a fonte de dados Prometheus para adicionar gráficos de métricas. Utilize consultas PromQL para definir as métricas. Exemplo:

      rate(request_duration_seconds_sum[5m]) / rate(request_duration_seconds_count[5m])
    
  • Logs: Use a fonte de dados Loki para adicionar painéis de logs. Utilize consultas LogQL para filtrar e visualizar logs.
    Exemplo:

      {job="service1"}
    
  • Traces: Use a fonte de dados Tempo para adicionar painéis de traces. Visualize o fluxo de requisições e identifique gargalos.

  1. Salvar o Dashboard:
  • Após configurar os painéis, clique em "Save dashboard" no canto superior direito.
  • Dê um nome ao dashboard e salve.

Visualizando Traces, Logs e Métricas

  • Traces: No Grafana, vá para "Explore" e selecione a fonte de dados Tempo. Realize consultas para visualizar traces específicos.

tempo

  • Logs: No Grafana, vá para "Explore" e selecione a fonte de dados Loki. Realize consultas para visualizar logs.

loki

  • Métricas: No Grafana, use os painéis configurados para visualizar métricas em tempo real.

prometheus

Recapitulando

Lembra que configuramos algumas métricas no path pkg/metrics/metrics.go?

RequestCounter, err = meter.Int64Counter(
        "request_total",
        metric.WithDescription("Total number of requests processed"),
        metric.WithUnit("1"),
    )
    if err != nil {
        return err
    }

    RequestDuration, err = meter.Float64Histogram(
        "request_duration_seconds",
        metric.WithDescription("Duration of requests"),
        metric.WithUnit("s"),
    )
    if err != nil {
        return err
    }

    RequestInFlight, err = meter.Int64UpDownCounter(
        "requests_in_flight",
        metric.WithDescription("Current number of requests being processed"),
        metric.WithUnit("1"),
    )
    if err != nil {
        return err
    }

    LetterCounter, err = meter.Int64Counter(
        "letter_concatenations_total",
        metric.WithDescription("Total number of letter concatenations"),
        metric.WithUnit("1"),
    )
    if err != nil {
        return err
    }

    WordLengthGauge, err = meter.Int64Gauge(
        "word_length",
        metric.WithDescription("Current length of the word"),
        metric.WithUnit("chars"),
    )
    if err != nil {
        return err
    }
Enter fullscreen mode Exit fullscreen mode

Vamos montar um dashboard para acompanhar elas no Grafana.

Adicionando um dashboard, como vimos logo acima. Vamos adicionar os seguintes painéis:

Adicione uma visualização no seu dashboard, selecione o datasource prometheus e adicione a métrica request_total. Troque o tipo de chart para time series.

request_total

Adicione uma visualização no seu dashboard, selecione o datasource prometheus e adicione a métrica request_duration_seconds. Troque o tipo de chart para time series.

request_duration_seconds

Adicione uma visualização no seu dashboard, selecione o datasource prometheus e adicione a métrica requests_in_flight. Troque o tipo de chart para gauge.

requests_in_flight

Adicione uma visualização no seu dashboard, selecione o datasource prometheus e adicione a métrica letter_concatenations_total. Troque o tipo de chart para time series.

letter_concatenations_total

Adicione uma visualização no seu dashboard, selecione o datasource prometheus e adicione a métrica word_length. Troque o tipo de chart para gauge.

word_length

E por fim teremos uma dashboard com todas as métricas para análise.

dashboard

Ao seguir estas etapas detalhadamente, você estabelecerá um ambiente de observabilidade robusto e abrangente, capacitando-se a monitorar, analisar e visualizar de forma ainda mais eficaz o desempenho e o comportamento de suas aplicações Go.

Repositório: https://github.com/booscaaa/observability-go-example

Top comments (2)

Collapse
 
milaila profile image
Mila Lavratti • Edited

Great article mate.

Collapse
 
jacksonsantin profile image
Jackson Dhanyel Santin

Muito bom cara, parabéns, excelente explicação!