[hw-7] add metrics, tracing

This commit is contained in:
Никита Шубин
2025-07-26 14:15:40 +00:00
parent 342bd3f726
commit 4396bebe80
38 changed files with 717 additions and 36 deletions

View File

@@ -11,9 +11,17 @@ import (
"github.com/rs/zerolog/log"
"route256/cart/internal/app"
"route256/cart/internal/infra/tracing"
)
func main() {
shut, err := tracing.InitOTLP(context.Background(), "cart", "jaeger:4318")
if err != nil {
log.Fatal().Err(err).Msg("tracing init failed")
}
defer func() {
_ = shut(context.Background())
}()
srv, err := app.NewApp(os.Getenv("CONFIG_FILE"))
if err != nil {
log.Fatal().Err(err).Msg("failed creating app")

View File

@@ -15,5 +15,5 @@ product_service:
burst: 10
loms_service:
host: localhost
host: loms
port: 8083

View File

@@ -7,6 +7,9 @@ require (
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.37.0
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
google.golang.org/grpc v1.72.2
gopkg.in/yaml.v3 v3.0.1
@@ -16,7 +19,9 @@ require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
@@ -35,7 +40,8 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
@@ -46,12 +52,16 @@ require (
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/ozontech/allure-go/pkg/allure v0.6.14 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
@@ -59,15 +69,13 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
@@ -77,6 +85,8 @@ require (
github.com/gojuno/minimock/v3 v3.4.5
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/prometheus/client_golang v1.22.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0
go.uber.org/goleak v1.3.0
golang.org/x/sys v0.33.0 // indirect
)

View File

@@ -6,8 +6,12 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
@@ -65,12 +69,14 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5uk
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
@@ -96,6 +102,8 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -110,6 +118,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=

View File

@@ -8,6 +8,7 @@ import (
"os"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
@@ -90,6 +91,7 @@ func (app *App) setupCartService() (*service.CartService, error) {
// Product service client
transport := http.DefaultTransport
transport = round_trippers.NewLogRoundTripper(transport)
transport = round_trippers.NewMetricsRoundTripper(transport)
transport = round_trippers.NewRetryRoundTripper(transport, productsRetryAttemptsDefault, productsInitialDelaySecDefault)
httpClient := http.Client{
@@ -133,7 +135,11 @@ func (app *App) BootstrapHandlers(cartService *service.CartService) http.Handler
mx.HandleFunc("DELETE /user/{user_id}/cart/{sku_id}", s.DeleteItemHandler)
mx.HandleFunc("DELETE /user/{user_id}/cart", s.DeleteItemsByUserIDHandler)
h := middlewares.NewTimerMiddleware(mx)
mx.Handle("GET /metrics", promhttp.Handler())
h := middlewares.NewTracingMiddleware(mx)
h = middlewares.NewMetricsMiddleware(h)
h = middlewares.NewTimerMiddleware(h)
return h
}

View File

@@ -6,6 +6,7 @@ import (
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
"route256/cart/internal/infra/tracing"
pbLoms "route256/pkg/api/loms/v1"
)
@@ -21,6 +22,9 @@ func NewLomsService(grpcClient pbLoms.LOMSClient) *Service {
}
func (s *Service) OrderCreate(ctx context.Context, cart *model.Cart) (int64, error) {
ctx, span := tracing.Tracer().Start(ctx, "Loms.OrderCreate")
defer span.End()
items := make([]*pbLoms.OrderItem, len(cart.Items))
for i, item := range cart.Items {
items[i] = &pbLoms.OrderItem{
@@ -43,6 +47,9 @@ func (s *Service) OrderCreate(ctx context.Context, cart *model.Cart) (int64, err
}
func (s *Service) StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error) {
ctx, span := tracing.Tracer().Start(ctx, "Loms.StocksInfo")
defer span.End()
req := &pbLoms.StocksInfoRequest{
Sku: int64(sku),
}

View File

@@ -6,6 +6,7 @@ import (
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
"route256/cart/internal/infra/http/metrics"
)
type storage = map[entity.UID]*entity.Cart
@@ -34,6 +35,7 @@ func (r *InMemoryRepository) AddItem(_ context.Context, userID entity.UID, item
}
r.storage[userID] = cart
metrics.SetInMemoryObjects(len(r.storage))
}
if _, ok := cart.ItemCount[item.Product.Sku]; !ok {
@@ -112,6 +114,7 @@ func (r *InMemoryRepository) DeleteItemsByUserID(_ context.Context, userID entit
_, ok := r.storage[userID]
if ok {
delete(r.storage, userID)
metrics.SetInMemoryObjects(len(r.storage))
}
return nil

View File

@@ -7,6 +7,7 @@ import (
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
"route256/cart/internal/infra/tracing"
)
//go:generate minimock -i Repository -o ./mock -s _mock.go
@@ -43,6 +44,9 @@ func NewCartService(repository Repository, productService ProductService, lomsSe
}
func (s *CartService) AddItem(ctx context.Context, userID entity.UID, item *model.Item) error {
ctx, span := tracing.Tracer().Start(ctx, "AddItem")
defer span.End()
if err := item.Validate(); err != nil {
return fmt.Errorf("invalid requested values: %w", err)
}
@@ -76,6 +80,9 @@ func (s *CartService) AddItem(ctx context.Context, userID entity.UID, item *mode
// and return a list of the collected items.
// In case of failed request to product-service, return nothing and error.
func (s *CartService) GetItemsByUserID(ctx context.Context, userID entity.UID) (*model.Cart, error) {
ctx, span := tracing.Tracer().Start(ctx, "GetItemsByUserID")
defer span.End()
if userID <= 0 {
return nil, fmt.Errorf("userID invalid")
}
@@ -122,6 +129,9 @@ func (s *CartService) GetItemsByUserID(ctx context.Context, userID entity.UID) (
}
func (s *CartService) DeleteItem(ctx context.Context, userID entity.UID, sku entity.Sku) error {
ctx, span := tracing.Tracer().Start(ctx, "DeleteItem")
defer span.End()
if userID <= 0 {
return fmt.Errorf("userID invalid")
}
@@ -138,6 +148,9 @@ func (s *CartService) DeleteItem(ctx context.Context, userID entity.UID, sku ent
}
func (s *CartService) DeleteItemsByUserID(ctx context.Context, userID entity.UID) error {
ctx, span := tracing.Tracer().Start(ctx, "DeleteItemsByUserID")
defer span.End()
if userID <= 0 {
return fmt.Errorf("userID invalid")
}
@@ -150,6 +163,9 @@ func (s *CartService) DeleteItemsByUserID(ctx context.Context, userID entity.UID
}
func (s *CartService) CheckoutUserCart(ctx context.Context, userID entity.UID) (int64, error) {
ctx, span := tracing.Tracer().Start(ctx, "CheckoutUserCart")
defer span.End()
if userID <= 0 {
return 0, fmt.Errorf("userID invalid")
}

View File

@@ -119,7 +119,8 @@ func TestCartService_AddItem(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
AddItemMock.
Expect(ctx, 1337, &testItem).
ExpectUserIDParam2(1337).
ExpectItemParam3(&testItem).
Return(nil),
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
@@ -190,7 +191,8 @@ func TestCartService_AddItem(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
AddItemMock.
Expect(ctx, 1337, &testItem).
ExpectUserIDParam2(1337).
ExpectItemParam3(&testItem).
Return(assert.AnError),
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
@@ -304,7 +306,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(testEntityCart, nil),
productService: &productServiceFake{},
},
@@ -320,7 +322,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(entity.Cart{
UserID: testUID,
Items: []entity.Sku{testSKU},
@@ -353,7 +355,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(entity.Cart{
UserID: testUID,
Items: []entity.Sku{testSKU, 1},
@@ -407,7 +409,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(entity.Cart{}, assert.AnError),
productService: nil,
},
@@ -423,7 +425,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(entity.Cart{}, nil),
productService: nil,
},
@@ -439,7 +441,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(entity.Cart{
UserID: testUID,
Items: []entity.Sku{2},
@@ -459,7 +461,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(entity.Cart{
UserID: testUID,
Items: []entity.Sku{2},
@@ -522,7 +524,8 @@ func TestCartService_DeleteItem(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
DeleteItemMock.
Expect(ctx, testUID, testSKU).
ExpectUserIDParam2(testUID).
ExpectSkuParam3(testSKU).
Return(nil),
},
args: args{
@@ -561,7 +564,8 @@ func TestCartService_DeleteItem(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
DeleteItemMock.
Expect(ctx, testUID, testSKU).
ExpectUserIDParam2(testUID).
ExpectSkuParam3(testSKU).
Return(assert.AnError),
},
args: args{
@@ -613,7 +617,7 @@ func TestCartService_DeleteItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
DeleteItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(nil),
},
args: args{
@@ -638,7 +642,7 @@ func TestCartService_DeleteItemsByUserID(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
DeleteItemsByUserIDMock.
Expect(ctx, testUID).
ExpectUserIDParam2(testUID).
Return(assert.AnError),
},
args: args{
@@ -697,10 +701,10 @@ func TestCartService_CheckoutUserCart(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, 1337).
ExpectUserIDParam2(1337).
Return(testCart, nil).
DeleteItemsByUserIDMock.
Expect(ctx, 1337).
ExpectUserIDParam2(1337).
Return(nil),
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
@@ -720,7 +724,7 @@ func TestCartService_CheckoutUserCart(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, 1337).
ExpectUserIDParam2(1337).
Return(entity.Cart{}, assert.AnError),
},
args: args{ctx: ctx, userID: 1337},
@@ -731,7 +735,7 @@ func TestCartService_CheckoutUserCart(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, 1111).
ExpectUserIDParam2(1111).
Return(testCart, nil),
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
@@ -744,10 +748,10 @@ func TestCartService_CheckoutUserCart(t *testing.T) {
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, 1337).
ExpectUserIDParam2(1337).
Return(testCart, nil).
DeleteItemsByUserIDMock.
Expect(ctx, 1337).
ExpectUserIDParam2(1337).
Return(assert.AnError),
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},

View File

@@ -0,0 +1,52 @@
package metrics
import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
requestCounter = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "app",
Name: "requests_total",
Help: "Total amount of request by handler",
}, []string{"method", "path", "status_code"})
requestDurationHistogram = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "app",
Name: "request_duration_seconds",
Help: "Latency of handler processing, seconds",
Buckets: prometheus.DefBuckets,
}, []string{"method", "path", "status_code"})
outboundCounter = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "app",
Name: "outbound_requests_total",
Help: "Total HTTP requests to external services",
}, []string{"method", "url", "status_code"})
outboundDurationHistogram = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "app",
Name: "outbound_request_duration_seconds",
Help: "Latency of outbound HTTP requests, seconds",
Buckets: prometheus.DefBuckets,
}, []string{"method", "url", "status_code"})
)
func IncRequestHandlerCount(method, path, statusCode string) {
requestCounter.WithLabelValues(method, path, statusCode).Inc()
}
func StoreHandlerRequestDuration(method, path, statusCode string, d time.Duration) {
requestDurationHistogram.WithLabelValues(method, path, statusCode).Observe(d.Seconds())
}
func IncOutboundRequestCount(method, url, status string) {
outboundCounter.WithLabelValues(method, url, status).Inc()
}
func StoreOutboundRequestDuration(method, url, status string, d time.Duration) {
outboundDurationHistogram.WithLabelValues(method, url, status).Observe(d.Seconds())
}

View File

@@ -0,0 +1,16 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var inMemoryObjectsGauge = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "app",
Name: "inmemory_repo_objects",
Help: "Current in-memory repository size",
})
func SetInMemoryObjects(n int) {
inMemoryObjectsGauge.Set(float64(n))
}

View File

@@ -0,0 +1,38 @@
package middlewares
import (
"net/http"
"strconv"
"time"
"route256/cart/internal/infra/http/metrics"
)
type statusWriter struct {
http.ResponseWriter
statusCode int
}
func (w *statusWriter) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
func NewMetricsMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &statusWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
start := time.Now()
h.ServeHTTP(sw, r)
path := r.URL.Path
status := strconv.Itoa(sw.statusCode)
metrics.IncRequestHandlerCount(r.Method, path, status)
metrics.StoreHandlerRequestDuration(r.Method, path, status, time.Since(start))
})
}

View File

@@ -0,0 +1,17 @@
package middlewares
import (
"net/http"
"route256/cart/internal/infra/tracing"
)
func NewTracingMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span := tracing.Tracer().Start(r.Context(),
r.Method+" "+r.URL.Path)
defer span.End()
h.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -0,0 +1,37 @@
package round_trippers
import (
"net/http"
"strconv"
"time"
"route256/cart/internal/infra/http/metrics"
)
type MetricsRoundTripper struct {
rt http.RoundTripper
}
func NewMetricsRoundTripper(rt http.RoundTripper) http.RoundTripper {
return &MetricsRoundTripper{
rt: rt,
}
}
func (m *MetricsRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
start := time.Now()
resp, err := m.rt.RoundTrip(r)
status := "error"
if resp != nil {
status = strconv.Itoa(resp.StatusCode)
}
url := r.URL.Path
metrics.IncOutboundRequestCount(r.Method, url, status)
metrics.StoreOutboundRequestDuration(r.Method, url, status, time.Since(start))
return resp, err
}

View File

@@ -0,0 +1,48 @@
package tracing
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
oteltrace "go.opentelemetry.io/otel/trace"
)
var (
globalTracer oteltrace.Tracer
provider *trace.TracerProvider
)
func NewTracer(serviceName string, option ...trace.TracerProviderOption) oteltrace.Tracer {
option = append([]trace.TracerProviderOption{
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
)),
}, option...)
provider = trace.NewTracerProvider(option...)
otel.SetTracerProvider(provider)
globalTracer = otel.GetTracerProvider().Tracer("cart")
return globalTracer
}
func Shutdown(ctx context.Context) error {
if provider != nil {
return provider.Shutdown(ctx)
}
return nil
}
func Tracer() oteltrace.Tracer {
if globalTracer == nil {
return otel.Tracer("cart")
}
return globalTracer
}

View File

@@ -0,0 +1,22 @@
package tracing
import (
"context"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func InitOTLP(ctx context.Context, serviceName, endpoint string) (shutdown func(context.Context) error, err error) {
exp, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpoint(endpoint),
otlptracehttp.WithInsecure(),
)
if err != nil {
return nil, err
}
NewTracer(serviceName, sdktrace.WithBatcher(exp))
return Shutdown, nil
}