# Go

## Instrumentação Go com Elven Observability

***

#### Sumário

* Visão geral
* Pré-requisitos
* Exemplo de referência
* Configuração de variáveis de ambiente
* Bootstrap OpenTelemetry
* Instrumentando o HTTP server
* Instrumentando o HTTP client
* Instrumentando PostgreSQL
* Instrumentando Redis
* Spans manuais
* Métricas customizadas de negócio
* Golden Signals mapeados
* Worker, retry e DLQ
* Estrutura de projeto recomendada
* Como rodar localmente
* Como validar no collector da Elven
* Como adaptar para a sua aplicação
* Testes
* Troubleshooting
* Checklist de deploy

***

#### Visão geral

A instrumentação Go da Elven usa o **SDK oficial do OpenTelemetry para Go** com exportação OTLP HTTP. Ela cobre instrumentação automática de HTTP server e client, PostgreSQL e Redis, além de instrumentação manual de spans e métricas de negócio (golden signals).

O **Collector OTLP fica sempre no ambiente do cliente**. A aplicação envia traces e métricas para esse Collector local/do cliente, e o Collector encaminha os dados para Tempo e Mimir conforme a arquitetura contratada.

* **Grafana Tempo** para traces
* **Grafana Mimir** para métricas
* **Grafana** para consulta, correlação, painéis e alertas

| Componente       | O que faz                                                                      |
| ---------------- | ------------------------------------------------------------------------------ |
| `otelhttp`       | Instrumentação automática de HTTP server e client. Gera spans e métricas HTTP. |
| `otelsql`        | Instrumentação de `database/sql` com spans de queries e métricas de pool.      |
| `redisotel`      | Instrumentação de Redis com spans de operações e métricas de cache/fila.       |
| `otlptracehttp`  | Exporter OTLP HTTP para traces.                                                |
| `otlpmetrichttp` | Exporter OTLP HTTP para métricas.                                              |
| `otelprom`       | Exporter Prometheus para expor métricas localmente em `/metrics`.              |

***

#### Pré-requisitos

* **Go 1.21+** instalado
* Docker Desktop ou Docker Engine funcionando
* Acesso de rede ao collector da Elven
* Collector OTLP HTTP aceitando `POST /v1/traces` e `POST /v1/metrics`

**Baseline técnico**

| Item                          | Versão    |
| ----------------------------- | --------- |
| Go                            | `1.21+`   |
| `go.opentelemetry.io/otel`    | `v1.42.0` |
| `go.opentelemetry.io/contrib` | `v1.42.0` |
| `otelhttp`                    | `v0.67.0` |
| `otlptracehttp`               | `v1.42.0` |
| `otlpmetrichttp`              | `v1.42.0` |
| `otelprom`                    | `v0.64.0` |
| `chi`                         | `v5.2.5`  |
| `pgx/v5`                      | `v5.8.0`  |
| `go-redis/v9`                 | `v9.18.0` |
| `redisotel/v9`                | `v9.18.0` |
| `otelsql`                     | `v0.41.0` |

**Checklist do collector**

Antes de subir a aplicação, confirme:

1. O endpoint de traces está correto
2. O endpoint de métricas está correto
3. O header de autenticação está definido (se necessário)
4. O collector aceita `http/protobuf`

> **Atenção:** se o collector exigir token, o valor em `OTEL_EXPORTER_OTLP_HEADERS` deve estar URL-encoded. Exemplo:
>
> ```
> OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20SEU_TOKEN
> ```

***

#### Exemplo de referência

A Elven disponibiliza uma aplicação demo completa em Go que demonstra todos os conceitos desta documentação:

👉 [elven-observability/go-otel-app](https://github.com/elven-observability/go-otel-app)

***

#### Configuração de variáveis de ambiente

Copie o arquivo de exemplo e preencha com os dados do seu collector:

```bash
cp .env.example .env
```

Configuração mínima funcional:

```env
APP_NAME=go-otel-app
APP_ENV=local
APP_VERSION=1.0.0
HTTP_ADDR=:8080
PUBLIC_BASE_URL=http://localhost:8080

DATABASE_URL=postgres://postgres:postgres@localhost:5432/mydb?sslmode=disable
REDIS_ADDR=localhost:6379

OTEL_SERVICE_NAME=go-otel-app
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local,service.version=1.0.0
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=none
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://SEU-COLLECTOR:4318/v1/traces
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://SEU-COLLECTOR:4318/v1/metrics
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20SEU_TOKEN
OTEL_SEMCONV_STABILITY_OPT_IN=*
```

> **Sobre `OTEL_SEMCONV_STABILITY_OPT_IN`:** definir como `database` (ou `*`) ativa a semântica de banco de dados mais recente, incluindo a métrica `db.client.operation.duration` em segundos. Sem isso, algumas instrumentações podem expor métricas legadas em milissegundos.

***

#### Bootstrap OpenTelemetry

O ponto central da instrumentação é criar um `TracerProvider` e um `MeterProvider` com dois destinos para métricas: o collector OTLP e o endpoint `/metrics` local (Prometheus).

```go
func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
    res, err := newResource()
    if err != nil {
        return nil, err
    }

    traceProvider, err := newTraceProvider(ctx, res)
    if err != nil {
        return nil, err
    }
    otel.SetTracerProvider(traceProvider)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{},
        propagation.Baggage{},
    ))

    meterProvider, err := newMeterProvider(ctx, res)
    if err != nil {
        return nil, err
    }
    otel.SetMeterProvider(meterProvider)

    shutdown = func(ctx context.Context) error {
        var errs []error
        if err := traceProvider.Shutdown(ctx); err != nil {
            errs = append(errs, err)
        }
        if err := meterProvider.Shutdown(ctx); err != nil {
            errs = append(errs, err)
        }
        return errors.Join(errs...)
    }
    return shutdown, nil
}
```

***

#### Instrumentando o HTTP server

Use o middleware `otelhttp.NewHandler` para instrumentar automaticamente todas as rotas:

```go
router := chi.NewRouter()
router.Use(middleware.RequestID)
router.Use(middleware.RealIP)

// Wrap com otelhttp para instrumentação automática
handler := otelhttp.NewHandler(router, "server",
    otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents),
)

server := &http.Server{
    Addr:    cfg.HTTPAddr,
    Handler: handler,
}
```

Isso gera automaticamente:

* Spans para cada request com atributos HTTP (método, rota, status code)
* Métricas `http.server.request.duration` e `http.server.active_requests`

***

#### Instrumentando o HTTP client

```go
httpClient := &http.Client{
    Transport: otelhttp.NewTransport(http.DefaultTransport),
    Timeout:   10 * time.Second,
}
```

Isso propaga automaticamente o `traceparent` header para serviços downstream.

***

#### Instrumentando PostgreSQL

```go
db, err := otelsql.Open("pgx", cfg.DatabaseURL,
    otelsql.WithAttributes(semconv.DBSystemPostgreSQL),
    otelsql.WithDBName("mydb"),
)
if err != nil {
    return nil, fmt.Errorf("failed to open database: %w", err)
}

// Registra métricas de pool de conexões
if err := otelsql.RegisterDBStatsMetrics(db,
    otelsql.WithAttributes(semconv.DBSystemPostgreSQL),
); err != nil {
    return nil, fmt.Errorf("failed to register db stats: %w", err)
}
```

Métricas geradas automaticamente:

* `db.client.operation.duration` — latência de queries
* `db.client.connection.count` — pool de conexões
* `db.client.connection.idle` — conexões ociosas

***

#### Instrumentando Redis

```go
rdb := redis.NewClient(&redis.Options{
    Addr: cfg.RedisAddr,
})

if err := redisotel.InstrumentTracing(rdb); err != nil {
    return nil, fmt.Errorf("failed to instrument redis tracing: %w", err)
}
if err := redisotel.InstrumentMetrics(rdb); err != nil {
    return nil, fmt.Errorf("failed to instrument redis metrics: %w", err)
}
```

***

#### Spans manuais

Para instrumentar operações de negócio específicas:

```go
func (s *OrderService) ProcessOrder(ctx context.Context, order Order) error {
    ctx, span := otel.Tracer("order-service").Start(ctx, "ProcessOrder",
        trace.WithAttributes(
            attribute.String("order.id", order.ID),
            attribute.String("order.customer_id", order.CustomerID),
            attribute.Float64("order.total", order.Total),
        ),
    )
    defer span.End()

    if err := s.validateOrder(ctx, order); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return err
    }

    span.SetStatus(codes.Ok, "order processed successfully")
    return nil
}
```

***

#### Métricas customizadas de negócio

```go
meter := otel.Meter("go-otel-app")

// Contador de pedidos
orderCounter, _ := meter.Int64Counter("orders.total",
    metric.WithDescription("Total number of orders processed"),
    metric.WithUnit("{order}"),
)

// Histograma de valor dos pedidos
orderValue, _ := meter.Float64Histogram("orders.value",
    metric.WithDescription("Value of orders processed"),
    metric.WithUnit("USD"),
)

// Uso
orderCounter.Add(ctx, 1, metric.WithAttributes(
    attribute.String("status", "success"),
    attribute.String("payment_method", "credit_card"),
))
orderValue.Record(ctx, order.Total)
```

***

#### Golden Signals mapeados

| Signal        | Métrica OTel                                 | Como usar                         |
| ------------- | -------------------------------------------- | --------------------------------- |
| **Latência**  | `http.server.request.duration`               | Histograma gerado pelo `otelhttp` |
| **Tráfego**   | `http.server.request.body.size`              | Gerado automaticamente            |
| **Erros**     | `http.server.request.duration{error.type=*}` | Status codes 4xx/5xx              |
| **Saturação** | `db.client.connection.count`                 | Pool de conexões do banco         |

***

#### Worker, retry e DLQ

Para workers que processam filas com retry e Dead Letter Queue (DLQ):

```go
func (w *Worker) ProcessMessage(ctx context.Context, msg Message) error {
    ctx, span := otel.Tracer("worker").Start(ctx, "ProcessMessage",
        trace.WithAttributes(
            attribute.String("messaging.system", "redis"),
            attribute.String("messaging.destination", msg.Queue),
            attribute.Int("messaging.retry_count", msg.RetryCount),
        ),
    )
    defer span.End()

    if err := w.handler(ctx, msg); err != nil {
        span.RecordError(err)
        if msg.RetryCount >= w.maxRetries {
            // Envia para DLQ
            w.dlq.Push(ctx, msg)
            span.SetAttributes(attribute.Bool("messaging.dead_letter", true))
        }
        return err
    }

    return nil
}
```

***

#### Estrutura de projeto recomendada

```
go-otel-app/
├── cmd/
│   └── api/
│       └── main.go          # Entrypoint, wiring de dependências
├── internal/
│   ├── config/              # Leitura de variáveis de ambiente
│   ├── otel/                # Bootstrap do SDK OTel
│   ├── server/              # HTTP server e rotas
│   ├── handler/             # Handlers HTTP
│   ├── service/             # Regras de negócio
│   ├── repository/          # Acesso ao banco
│   └── worker/              # Workers de fila
├── .env.example
├── docker-compose.yml
└── go.mod
```

***

#### Como rodar localmente

```bash
# 1. Clone o repositório de exemplo
git clone https://github.com/elven-observability/go-otel-app
cd go-otel-app

# 2. Configure as variáveis de ambiente
cp .env.example .env
# Edite .env com os endpoints do seu collector

# 3. Suba as dependências (Postgres, Redis)
docker compose up -d postgres redis

# 4. Rode a aplicação
go run ./cmd/api
```

***

#### Como validar no collector da Elven

Após subir a aplicação, valide que os dados estão chegando:

1. Acesse o **Grafana Explore** e selecione o datasource **Tempo**
2. Busque por `service.name = "go-otel-app"`
3. Verifique que os traces aparecem com spans de HTTP, banco e Redis
4. Troque para o datasource **Mimir** e busque a métrica `http_server_request_duration_seconds_count`

***

#### Como adaptar para a sua aplicação

1. Substitua `go-otel-app` pelo nome do seu serviço em `OTEL_SERVICE_NAME` e nos atributos do resource
2. Remova os handlers e services de exemplo, mantenha apenas o bootstrap OTel em `internal/otel/`
3. Adicione `otelhttp.NewHandler` no seu HTTP server
4. Adicione `otelhttp.NewTransport` nos seus HTTP clients
5. Instrumente o banco e Redis conforme as seções acima
6. Adicione spans manuais nas operações críticas de negócio

***

#### Testes

Para testar a instrumentação sem um collector real, use o `InMemoryExporter`:

```go
func TestOrderService(t *testing.T) {
    // Setup in-memory exporter
    exp := tracetest.NewInMemoryExporter()
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithSyncer(exp),
    )
    otel.SetTracerProvider(tp)

    svc := NewOrderService(...)
    err := svc.ProcessOrder(context.Background(), testOrder)

    assert.NoError(t, err)

    spans := exp.GetSpans()
    assert.Len(t, spans, 1)
    assert.Equal(t, "ProcessOrder", spans[0].Name)
}
```

***

#### Troubleshooting

| Problema                           | Causa provável                               | Solução                                                           |
| ---------------------------------- | -------------------------------------------- | ----------------------------------------------------------------- |
| Traces não aparecem no Tempo       | Endpoint incorreto ou sem conectividade      | Verifique `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` e teste com `curl` |
| Métricas não aparecem no Mimir     | Autenticação incorreta                       | Verifique `OTEL_EXPORTER_OTLP_HEADERS` com token URL-encoded      |
| Erro `context deadline exceeded`   | Collector inacessível                        | Verifique firewall e rede entre a aplicação e o collector         |
| Métricas de banco em milissegundos | `OTEL_SEMCONV_STABILITY_OPT_IN` não definido | Adicione `OTEL_SEMCONV_STABILITY_OPT_IN=*` no `.env`              |
| Spans sem nome de rota (`/`)       | `otelhttp` sem `WithRouteTag`                | Use `otelhttp.WithRouteTag(route, w, r)` nos handlers             |

***

#### Checklist de deploy

* \[ ] `OTEL_SERVICE_NAME` definido com o nome correto do serviço
* \[ ] `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` apontando para o collector
* \[ ] `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` apontando para o collector
* \[ ] `OTEL_EXPORTER_OTLP_HEADERS` com token URL-encoded (se necessário)
* \[ ] `OTEL_SEMCONV_STABILITY_OPT_IN=*` definido
* \[ ] `otelhttp.NewHandler` aplicado no HTTP server
* \[ ] HTTP clients usando `otelhttp.NewTransport`
* \[ ] Banco instrumentado com `otelsql`
* \[ ] Redis instrumentado com `redisotel`
* \[ ] Traces visíveis no Grafana Tempo
* \[ ] Métricas visíveis no Grafana Mimir


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.elven.works/elven-platform/elven-observability/integracao-e-instrumentacao/go.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
