Golang, observabilidade e métricas com SigNoz e OpenTelemetry
Tags: go, metrics, api, signoz
Published: 2022-07-13 19:36:58

Quem nunca passou por um aperto com uma api, endpoit, serviço ou qualquer coisa em produção e simplesmente não achou o problema ou demorou muito tempo para metrificar e descobrir o gargalo que fazia o sistema cair? É, aquela hora do dia que o sistema simplesmente ficava inutilizável e ninguém sabia explicar o motivo? Se você não passou por isso sempre vai ter a primeira vez... Brincadeiras a parte, hoje veremos como integrar um serviço criado com golang com o [SigNoz](https://signoz.io/) usando [OpenTelemetry](https://opentelemetry.io/)

Bora lá! Primeiro passo é ter um serviço para metrificar! rsrs... Vamos criar algo muito simples para não perdermos tempo. O foco aqui é a integração com o SigNoz e não uma API completa com Golang.

# Aplicação
Iniciaremos com:

```bash
mkdir go-signoz-otl
cd go-signoz-otl
go mod init github.com/booscaaa/go-signoz-otl
```

Vamos configurar nossa migration de produtos para o exemplo.

```bash
migrate create -ext sql -dir database/migrations -seq create_product_table
```

No nosso arquivo `database/migrations/000001_create_product_table.up.sql`

```sql
CREATE TABLE product(
    id serial primary key not null,
    name varchar(100) not null
);

INSERT INTO product (name) VALUES 
    ('Cadeira'),
    ('Mesa'),
    ('Toalha'),
    ('Fogão'),
    ('Batedeira'),
    ('Pia'),
    ('Torneira'),
    ('Forno'),
    ('Gaveta'),
    ('Copo');
```
Com a migration em mãos, bora criar já de início nosso conector com o postgres usando a lib sqlx.
`adapter/postgres/connector.go`

```go
package postgres

import (
	"context"
	"log"

	"github.com/golang-migrate/migrate/v4"
	"github.com/jmoiron/sqlx"
	"github.com/spf13/viper"

	_ "github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file"
	_ "github.com/lib/pq"
)

// GetConnection return connection pool from postgres drive SQLX
func GetConnection(context context.Context) *sqlx.DB {
	databaseURL := viper.GetString("database.url")

	db, err := sqlx.ConnectContext(
		context,
		"postgres",
		databaseURL,
	)
	if err != nil {
		log.Fatal(err)
	}

	return db
}

// RunMigrations run scripts on path database/migrations
func RunMigrations() {
	databaseURL := viper.GetString("database.url")
	m, err := migrate.New("file://database/migrations", databaseURL)
	if err != nil {
		log.Println(err)
	}

	if err := m.Up(); err != nil {
		log.Println(err)
	}
}

```

Vamos criar as abstrações e implementações no nosso dominio/adapters da aplicação.

`core/domain/product.go`
```go
package domain

import (
	"context"

	"github.com/gin-gonic/gin"
)

// Product is entity of table product database column
type Product struct {
	ID   int32  `json:"id" db:"id"`
	Name string `json:"name" db:"name"`
}

// ProductService is a contract of http adapter layer
type ProductService interface {
	Fetch(*gin.Context)
}

// ProductUseCase is a contract of business rule layer
type ProductUseCase interface {
	Fetch(context.Context) (*[]Product, error)
}

// ProductRepository is a contract of database connection adapter layer
type ProductRepository interface {
	Fetch(context.Context) (*Product, error)
}
```

`core/usecase/productusecase/new.go`
```go
package productusecase

import "github.com/booscaaa/go-signoz-otl/core/domain"

type usecase struct {
	repository domain.ProductRepository
}

// New returns contract implementation of ProductUseCase
func New(repository domain.ProductRepository) domain.ProductUseCase {
	return &usecase{
		repository: repository,
	}
}
```

`core/usecase/productusecase/fetch.go`
```go
package productusecase

import (
	"context"

	"github.com/booscaaa/go-signoz-otl/core/domain"
)

func (usecase usecase) Fetch(ctx context.Context) (*[]domain.Product, error) {
	products, err := usecase.repository.Fetch(ctx)

	if err != nil {
		return nil, err
	}

	return products, err
}
```
`adapter/postgres/productrepository/new.go`
```go
package productrepository

import (
	"github.com/booscaaa/go-signoz-otl/core/domain"
	"github.com/jmoiron/sqlx"
)

type repository struct {
	db *sqlx.DB
}

// New returns contract implementation of ProductRepository
func New(db *sqlx.DB) domain.ProductRepository {
	return &repository{
		db: db,
	}
}
```

`adapter/postgres/productrepository/fetch.go`
```go
package productrepository

import (
	"context"

	"github.com/booscaaa/go-signoz-otl/core/domain"
)

func (repository repository) Fetch(ctx context.Context) (*[]domain.Product, error) {
	products := []domain.Product{}

	err := repository.db.SelectContext(ctx, &products, "SELECT * FROM product;")

	if err != nil {
		return nil, err
	}

	return &products, nil
}
```

`adapter/http/productservice/new.go`
```go
package productservice

import "github.com/booscaaa/go-signoz-otl/core/domain"

type service struct {
	usecase domain.ProductUseCase
}

// New returns contract implementation of ProductService
func New(usecase domain.ProductUseCase) domain.ProductService {
	return &service{
		usecase: usecase,
	}
}
```

`adapter/http/productservice/fetch.go`
```go
package productservice

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func (service service) Fetch(c *gin.Context) {
	products, err := service.usecase.Fetch(c.Request.Context())

	if err != nil {
		c.JSON(http.StatusInternalServerError, err)
		return
	}

	c.JSON(http.StatusOK, products)
}
```
`di/product.go`
```go
package di

import (
	"github.com/booscaaa/go-signoz-otl/adapter/http/productservice"
	"github.com/booscaaa/go-signoz-otl/adapter/postgres/productrepository"
	"github.com/booscaaa/go-signoz-otl/core/domain"
	"github.com/booscaaa/go-signoz-otl/core/usecase/productusecase"
	"github.com/jmoiron/sqlx"
)

func ConfigProductDI(conn *sqlx.DB) domain.ProductService {
	productRepository := productrepository.New(conn)
	productUsecase := productusecase.New(productRepository)
	productService := productservice.New(productUsecase)

	return productService
}
```

`adapter/http/main.go`
```go
package main

import (
	"context"

	"github.com/booscaaa/go-signoz-otl/adapter/postgres"
	"github.com/booscaaa/go-signoz-otl/di"
	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
)

func init() {
	viper.SetConfigFile(`config.json`)
	err := viper.ReadInConfig()
	if err != nil {
		panic(err)
	}
}

func main() {
	ctx := context.Background()
	conn := postgres.GetConnection(ctx)
	defer conn.Close()

	postgres.RunMigrations()

	productService := di.ConfigProductDI(conn)

	router := gin.Default()
	router.GET("/product", productService.Fetch)
	router.Run(":3000")
}
```
`config.json`
```js
{
    "database": {
        "url": "postgres://postgres:postgres@localhost:5432/devtodb"
    },
    "server": {
        "port": "3000"
    },
    "otl": {
        "service_name": "devto_goapp",
        "otel_exporter_otlp_endpoint": "localhost:4317",
        "insecure_mode": true
    }
}
```
Por fim basta rodar a aplicação e ver se tudo ficou funcionando certinho!
No primeiro terminal:
```bash
go run adapter/http/main.go
```
No segundo terminal:
```bash
curl --location --request GET 'localhost:3000/product'
```

# SigNoz

Com a aplicação pronta, vamos iniciar as devidas implementações para integrar as métricas com o SigNoz e ver a magia acontecer!
Primeiro passo então é instalarmos o SigNoz na nossa máquina, para isso usaremos o docker-compose.
```bash
git clone -b main https://github.com/SigNoz/signoz.git && cd signoz/deploy/

docker-compose -f docker/clickhouse-setup/docker-compose.yaml up -d
```

Feito isso basta acessar o endereço [localhost:3301](url) no seu navegador.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sylyf1be3d64mka8i87n.png)

Crie uma conta e acesse o painel do SigNoz.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mj0jbohcbrx443bdcng2.png)

No Dashboard inicial ainda não temos nada que nos interesse, mas fique a vontade para explorar os dados ja existentes da aplicação.

Por fim vamos realizar a integração e analisar os dados que serão mostrados no SigNoz.

Vamos começar alterando o conector com o banco de dados, criando um wrapper do sqlx com a lib otelsqlx, com isso vamos conseguir captar informações de queries que serão executadas no banco.

`core/postgres/connector.go`

```go
package postgres

import (
	"context"
	"log"

	"github.com/golang-migrate/migrate/v4"
	"github.com/jmoiron/sqlx"
	"github.com/spf13/viper"
	"github.com/uptrace/opentelemetry-go-extra/otelsql"
	"github.com/uptrace/opentelemetry-go-extra/otelsqlx"
	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"

	_ "github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file"
	_ "github.com/lib/pq"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

// GetConnection return connection pool from postgres drive SQLX
func GetConnection(context context.Context, provider *sdktrace.TracerProvider) *sqlx.DB {
	databaseURL := viper.GetString("database.url")

	db, err := otelsqlx.ConnectContext(
		context,
		"postgres",
		databaseURL,
		otelsql.WithAttributes(semconv.DBSystemPostgreSQL),
		otelsql.WithTracerProvider(provider),
	)
	if err != nil {
		log.Fatal(err)
	}

	return db
}

// RunMigrations run scripts on path database/migrations
func RunMigrations() {
	databaseURL := viper.GetString("database.url")
	m, err := migrate.New("file://database/migrations", databaseURL)
	if err != nil {
		log.Println(err)
	}

	if err := m.Up(); err != nil {
		log.Println(err)
	}
}
```

Feito isso criaremos o arquivo `util/tracer.go` para inicializar a captura das informações.
```go
package util

import (
	"context"
	"log"

	"github.com/spf13/viper"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/sdk/resource"
	"google.golang.org/grpc/credentials"

	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

var (
	ServiceName  = ""
	CollectorURL = ""
	Insecure     = false
)

func InitTracer() *sdktrace.TracerProvider {
	ServiceName = viper.GetString("otl.service_name")
	CollectorURL = viper.GetString("otl.otel_exporter_otlp_endpoint")
	Insecure = viper.GetBool("otl.insecure_mode")

	secureOption := otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, ""))
	if Insecure {
		secureOption = otlptracegrpc.WithInsecure()
	}

	ctx := context.Background()

	exporter, err := otlptrace.New(
		ctx,
		otlptracegrpc.NewClient(
			secureOption,
			otlptracegrpc.WithEndpoint(CollectorURL),
		),
	)

	if err != nil {
		log.Fatal(err)
	}

	resources, err := resource.New(
		ctx,
		resource.WithAttributes(
			attribute.String("service.name", ServiceName),
			attribute.String("library.language", "go"),
		),
	)

	if err != nil {
		log.Printf("Could not set resources: %v", err)
	}

	provider := sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resources),
	)

	otel.SetTracerProvider(
		provider,
	)
	return provider
}
```
E por último, mas não menos importante, vamos configurar o middleware para o gin no arquivo `adapter/http/main.go`
```go
package main

import (
	"context"

	"github.com/booscaaa/go-signoz-otl/adapter/postgres"
	"github.com/booscaaa/go-signoz-otl/di"
	"github.com/booscaaa/go-signoz-otl/util"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
)

func init() {
	viper.SetConfigFile(`config.json`)
	err := viper.ReadInConfig()
	if err != nil {
		panic(err)
	}
}

func main() {
	tracerProvider := util.InitTracer()
	ctx := context.Background()
	conn := postgres.GetConnection(ctx, tracerProvider)
	defer conn.Close()

	postgres.RunMigrations()

	productService := di.ConfigProductDI(conn)

	router := gin.Default()
	router.Use(otelgin.Middleware(util.ServiceName))
	router.GET("/product", productService.Fetch)
	router.Run(":3000")
}
```
Vamos rodar novamente a aplicação e criar um script para realizar diversas chamadas na api.
No primeiro terminal:
```bash
go run adapter/http/main.go
```
No segundo terminal:
```bash
while :
do
    curl --location --request GET 'localhost:3000/product'
done
```
Voltando para o painel do SigNoz basta esperar a aplicação aparecer no dashboard.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hsvrmewxi21o39a2j96i.png)

Clicando no app que acabou de aparecer já conseguimos analisar dados muito importantes como:

- Media de tempo de cada request.
- Quantidade de requests por segundo.
- Qual o endpoint mais acessado da aplicação.
- Porcentagem de erros que ocorreram.

E ao clicar em uma request que por ventura demorou muito para retornar ou deu erro, chegaremos a uma nova tela onde é possivel analisar o tempo interno de cada camada, além de ver exatamente a query que pode estar causando problemas na aplicação.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8sgbb29b9r2yuf3yasdv.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rfutibuogqy502iuwwod.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f29hu4mypcv54sz9i9ge.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3bg2v6nrwwe69gdwfv39.png)