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!