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 usando OpenTelemetry
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:
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.
migrate create -ext sql -dir database/migrations -seq create_product_table
No nosso arquivo database/migrations/000001_create_product_table.up.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
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
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
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
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
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
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
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
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
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
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
{
"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:
go run adapter/http/main.go
No segundo terminal:
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.
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 no seu navegador.
Crie uma conta e acesse o painel do SigNoz.
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
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.
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
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:
go run adapter/http/main.go
No segundo terminal:
while :
do
curl --location --request GET 'localhost:3000/product'
done
Voltando para o painel do SigNoz basta esperar a aplicação aparecer no dashboard.
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.
Top comments (0)