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.
Desafio de Implementação
-
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"} .
-
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"} .
-
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"} .
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.
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
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)
}
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
./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]
./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"]
./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
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
}
./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,
}
}
./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,
}
}
./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{}
}
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
}
./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
}
./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)
}
./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)
}
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:
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"]
./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
)
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:
-
Configure as variáveis de ambiente:
Crie um arquivo
.env
na raiz do projeto e defina as variáveis necessárias, comoSERVICE1_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
-
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
Acesse o serviço:
Após iniciar os serviços, você pode acessar o serviço principal emhttp://localhost:8081/service1
.Parar os serviços:
Para parar todos os serviços, execute:
docker-compose down
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
- 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:
- Vá para "Data Sources" no menu de configurações (⚙️).
- Adicione Prometheus, Loki e Tempo como fontes de dados.
- Para Prometheus, use http://prometheus:9090 .
- Para Loki, use http://loki:3100 .
- Para Tempo, use http://tempo:3200 .
Criando Dashboards no Grafana
- Criar um Novo Dashboard:
- No menu lateral, clique em "+" e selecione "Dashboard".
- Clique em "Add new panel" para adicionar um painel.
- 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.
- 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.
- Logs: No Grafana, vá para "Explore" e selecione a fonte de dados Loki. Realize consultas para visualizar logs.
- Métricas: No Grafana, use os painéis configurados para visualizar métricas em tempo real.
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
}
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.
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.
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.
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.
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.
E por fim teremos uma dashboard com todas as métricas para análise.
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)
Great article mate.
Muito bom cara, parabéns, excelente explicação!