diff --git a/Dockerfile b/Dockerfile index 0b2e2f5..19f1401 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,6 @@ COPY --from=builder /app/server ./ ENV GO_ENV production -EXPOSE 3000 +EXPOSE 3004 CMD ["./server"] \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 77bddd5..c13f410 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,7 +14,9 @@ import ( "github.com/isd-sgcu/rpkm67-checkin/database" "github.com/isd-sgcu/rpkm67-checkin/internal/checkin" "github.com/isd-sgcu/rpkm67-checkin/logger" + "github.com/isd-sgcu/rpkm67-checkin/tracer" checkinProto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/checkin/checkin/v1" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/health" @@ -30,20 +32,34 @@ func main() { logger := logger.New(conf) + tp, err := tracer.New(conf) + if err != nil { + panic(fmt.Sprintf("Failed to create tracer: %v", err)) + } + defer func() { + if err := tp.Shutdown(context.Background()); err != nil { + panic(fmt.Sprintf("Failed to shutdown tracer: %v", err)) + } + }() + + tracer := tp.Tracer("rpkm67-checkin") + db, err := database.InitDatabase(&conf.Db, conf.App.IsDevelopment()) if err != nil { panic(fmt.Sprintf("Failed to connect to database: %v", err)) } - checkinRepo := checkin.NewRepository(db) - checkinSvc := checkin.NewService(checkinRepo, logger.Named("checkinSvc")) + checkinRepo := checkin.NewRepository(db, tracer) + checkinSvc := checkin.NewService(checkinRepo, logger.Named("checkinSvc"), tracer) listener, err := net.Listen("tcp", fmt.Sprintf(":%v", conf.App.Port)) if err != nil { panic(fmt.Sprintf("Failed to listen: %v", err)) } - grpcServer := grpc.NewServer() + grpcServer := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), + ) grpc_health_v1.RegisterHealthServer(grpcServer, health.NewServer()) checkinProto.RegisterCheckInServiceServer(grpcServer, checkinSvc) diff --git a/config/config.go b/config/config.go index 28b444f..509c2d1 100644 --- a/config/config.go +++ b/config/config.go @@ -7,17 +7,23 @@ import ( ) type AppConfig struct { - Port string - Env string + Port string + Env string + ServiceName string } type DbConfig struct { Url string } +type TracerConfig struct { + Endpoint string +} + type Config struct { - App AppConfig - Db DbConfig + App AppConfig + Db DbConfig + Tracer TracerConfig } func LoadConfig() (*Config, error) { @@ -29,17 +35,23 @@ func LoadConfig() (*Config, error) { } appConfig := AppConfig{ - Port: os.Getenv("APP_PORT"), - Env: os.Getenv("APP_ENV"), + Port: os.Getenv("APP_PORT"), + Env: os.Getenv("APP_ENV"), + ServiceName: os.Getenv("APP_SERVICE_NAME"), } dbConfig := DbConfig{ Url: os.Getenv("DB_URL"), } + tracerConfig := TracerConfig{ + Endpoint: os.Getenv("TRACER_ENDPOINT"), + } + return &Config{ - App: appConfig, - Db: dbConfig, + App: appConfig, + Db: dbConfig, + Tracer: tracerConfig, }, nil } diff --git a/docker-compose.qa.template.yml b/docker-compose.qa.template.yml index f3b58bb..7c14e5e 100644 --- a/docker-compose.qa.template.yml +++ b/docker-compose.qa.template.yml @@ -44,7 +44,7 @@ services: - ./microservices/auth:/app/config/staffs ports: - "3002:3002" - + backend: image: ghcr.io/isd-sgcu/rpkm67-backend:latest container_name: backend @@ -128,7 +128,7 @@ services: container_name: prometheus restart: unless-stopped networks: - - rpkm67 + - rpkm67 volumes: - ./microservices/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml ports: @@ -147,6 +147,30 @@ services: ports: - "3006:3000" + otel-collector: + image: otel/opentelemetry-collector-contrib:0.104.0 + command: ["--config=/etc/opentelemetry.yml"] + volumes: + - ./microservices/opentelemetry/opentelemetry.yml:/etc/opentelemetry.yml + ports: + - 4317:4317 # OTLP gRPC receiver + - 4318:4318 # OTLP HTTP receiver + networks: + - rpkm67 + depends_on: + - jaeger + + jaeger: + image: jaegertracing/all-in-one:1.58 + environment: + - COLLECTOR_OTLP_ENABLED=true + ports: + - 9091:16686 # Jaeger UI + - 14250:14250 # Jaeger gRPC collector + - 14268:14268 # Jaeger HTTP collector + networks: + - rpkm67 + networks: rpkm67: name: rpkm67 diff --git a/go.mod b/go.mod index aea811e..01cecc2 100644 --- a/go.mod +++ b/go.mod @@ -8,25 +8,34 @@ require ( github.com/isd-sgcu/rpkm67-go-proto v0.4.6 github.com/isd-sgcu/rpkm67-model v0.0.4 github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 google.golang.org/grpc v1.65.0 gorm.io/gorm v1.25.10 ) require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -36,6 +45,9 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/joho/godotenv v1.5.1 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 go.uber.org/zap v1.27.0 gorm.io/driver/postgres v1.5.9 ) diff --git a/go.sum b/go.sum index 9696476..9584b7d 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,23 @@ github.com/bxcodec/faker/v4 v4.0.0-beta.3 h1:gqYNBvN72QtzKkYohNDKQlm+pg+uwBDVMN28nWHS18k= github.com/bxcodec/faker/v4 v4.0.0-beta.3/go.mod h1:m6+Ch1Lj3fqW/unZmvkXIdxWS5+XQWPWxcbbQW2X+Ho= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/isd-sgcu/rpkm67-go-proto v0.4.4 h1:ScXOEVS+n5airgje1c3kjtkEzENXEfxwZMb+d5xFfRA= -github.com/isd-sgcu/rpkm67-go-proto v0.4.4/go.mod h1:w+UCeQnJ3wBuJ7Tyf8LiBiPZVb1KlecjMNCB7kBeL7M= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/isd-sgcu/rpkm67-go-proto v0.4.6 h1:yUBzUY3ftBfnI2x/MT+MsynBOlV5pOR143c9OTrVGuU= github.com/isd-sgcu/rpkm67-go-proto v0.4.6/go.mod h1:w+UCeQnJ3wBuJ7Tyf8LiBiPZVb1KlecjMNCB7kBeL7M= github.com/isd-sgcu/rpkm67-model v0.0.4 h1:tk6z6pXnhWBoG2SaSIoyLxNnwRaXwdbSIEEa/cSi8EY= @@ -29,8 +36,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -43,6 +50,22 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -81,8 +104,10 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= diff --git a/internal/checkin/checkin.repository.go b/internal/checkin/checkin.repository.go index 4727de5..0f73105 100644 --- a/internal/checkin/checkin.repository.go +++ b/internal/checkin/checkin.repository.go @@ -1,32 +1,45 @@ package checkin import ( + "context" + "github.com/isd-sgcu/rpkm67-model/model" + "go.opentelemetry.io/otel/trace" "gorm.io/gorm" ) type Repository interface { - Create(checkIn *model.CheckIn) error - FindByEmail(email string, checkIns *[]*model.CheckIn) error - FindByUserId(userId string, checkIns *[]*model.CheckIn) error + Create(ctx context.Context, checkIn *model.CheckIn) error + FindByEmail(ctx context.Context, email string, checkIns *[]*model.CheckIn) error + FindByUserId(ctx context.Context, userId string, checkIns *[]*model.CheckIn) error } type repositoryImpl struct { - Db *gorm.DB + Db *gorm.DB + tracer trace.Tracer } -func NewRepository(db *gorm.DB) Repository { - return &repositoryImpl{Db: db} +func NewRepository(db *gorm.DB, tracer trace.Tracer) Repository { + return &repositoryImpl{Db: db, tracer: tracer} } -func (r *repositoryImpl) Create(checkIn *model.CheckIn) error { +func (r *repositoryImpl) Create(ctx context.Context, checkIn *model.CheckIn) error { + _, span := r.tracer.Start(ctx, "repository.checkin.Create") + defer span.End() + return r.Db.Create(checkIn).Error } -func (r *repositoryImpl) FindByEmail(email string, checkIns *[]*model.CheckIn) error { +func (r *repositoryImpl) FindByEmail(ctx context.Context, email string, checkIns *[]*model.CheckIn) error { + _, span := r.tracer.Start(ctx, "repository.checkin.FindByEmail") + defer span.End() + return r.Db.Where("email = ?", email).Find(&checkIns).Error } -func (r *repositoryImpl) FindByUserId(userId string, checkIns *[]*model.CheckIn) error { +func (r *repositoryImpl) FindByUserId(ctx context.Context, userId string, checkIns *[]*model.CheckIn) error { + _, span := r.tracer.Start(ctx, "repository.checkin.FindByUserId") + defer span.End() + return r.Db.Where("user_id = ?", userId).Find(&checkIns).Error } diff --git a/internal/checkin/checkin.service.go b/internal/checkin/checkin.service.go index c578bff..5a3849a 100644 --- a/internal/checkin/checkin.service.go +++ b/internal/checkin/checkin.service.go @@ -7,7 +7,8 @@ import ( "github.com/isd-sgcu/rpkm67-checkin/constant" proto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/checkin/checkin/v1" "github.com/isd-sgcu/rpkm67-model/model" - + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -20,15 +21,23 @@ type Service interface { type serviceImpl struct { proto.UnimplementedCheckInServiceServer - repo Repository - log *zap.Logger + repo Repository + log *zap.Logger + tracer trace.Tracer } -func NewService(repo Repository, log *zap.Logger) Service { - return &serviceImpl{repo: repo, log: log} +func NewService(repo Repository, log *zap.Logger, tracer trace.Tracer) Service { + return &serviceImpl{repo: repo, log: log, tracer: tracer} } -func (s *serviceImpl) Create(_ context.Context, req *proto.CreateCheckInRequest) (*proto.CreateCheckInResponse, error) { +func (s *serviceImpl) Create(ctx context.Context, req *proto.CreateCheckInRequest) (*proto.CreateCheckInResponse, error) { + span := trace.SpanFromContext(ctx) + span.SetAttributes( + attribute.String("req.user.id", req.UserId), + attribute.String("req.user.email", req.Email), + attribute.String("req.event", req.Event), + ) + checkin := &model.CheckIn{ Email: req.Email, Event: req.Event, @@ -36,7 +45,7 @@ func (s *serviceImpl) Create(_ context.Context, req *proto.CreateCheckInRequest) } var checkin_userIds []*model.CheckIn - err := s.repo.FindByUserId(req.UserId, &checkin_userIds) + err := s.repo.FindByUserId(ctx, req.UserId, &checkin_userIds) if err != nil { s.log.Named("Create").Error("Create: ", zap.Error(err)) return nil, status.Error(codes.Internal, constant.InternalServerErrorMessage) @@ -45,12 +54,12 @@ func (s *serviceImpl) Create(_ context.Context, req *proto.CreateCheckInRequest) if v.Event == req.Event && v.UserID == req.UserId { s.log.Named("Create").Warn("Create: User already checkin this event") - return &proto.CreateCheckInResponse{ - CheckIn: ModelToProto(checkin), - }, nil + return nil, status.Error(codes.AlreadyExists, constant.AlreadyCheckinErrorMessage) } } - err = s.repo.Create(checkin) + span.AddEvent("Verify user checkin not duplicate") + + err = s.repo.Create(ctx, checkin) if err != nil { s.log.Named("Create").Error("Create: ", zap.Error(err)) if status.Code(err) == codes.AlreadyExists { @@ -64,20 +73,28 @@ func (s *serviceImpl) Create(_ context.Context, req *proto.CreateCheckInRequest) } return nil, status.Error(codes.Internal, constant.InternalServerErrorMessage) } + span.AddEvent("Checkin created") return &proto.CreateCheckInResponse{ CheckIn: ModelToProto(checkin), }, nil } -func (s *serviceImpl) FindByEmail(_ context.Context, req *proto.FindByEmailCheckInRequest) (*proto.FindByEmailCheckInResponse, error) { +func (s *serviceImpl) FindByEmail(ctx context.Context, req *proto.FindByEmailCheckInRequest) (*proto.FindByEmailCheckInResponse, error) { + span := trace.SpanFromContext(ctx) + defer span.End() + + span.SetAttributes( + attribute.String("req.user.email", req.Email), + ) + if req.Email == "" { s.log.Named("FindByUserEmail").Error("FindByUserEmail: invalid user Email") return nil, status.Error(codes.InvalidArgument, constant.ArgumentEmptyErrorMessage) } var checkins []*model.CheckIn - err := s.repo.FindByEmail(req.Email, &checkins) + err := s.repo.FindByEmail(ctx, req.Email, &checkins) if err != nil { s.log.Named("FindByEmail").Error("FindByEmail: ", zap.Error(err)) if status.Code(err) == codes.Canceled { @@ -88,22 +105,31 @@ func (s *serviceImpl) FindByEmail(_ context.Context, req *proto.FindByEmailCheck } return nil, status.Error(codes.Internal, constant.InternalServerErrorMessage) } + span.AddEvent("Checkin found") return &proto.FindByEmailCheckInResponse{ CheckIns: ModelToProtoList(checkins), }, nil } -func (s *serviceImpl) FindByUserId(_ context.Context, req *proto.FindByUserIdCheckInRequest) (*proto.FindByUserIdCheckInResponse, error) { +func (s *serviceImpl) FindByUserId(ctx context.Context, req *proto.FindByUserIdCheckInRequest) (*proto.FindByUserIdCheckInResponse, error) { + ctx, span := s.tracer.Start(ctx, "service.checkin.FindByUserId") + span.SetAttributes( + attribute.String("req.user.id", req.UserId), + ) + defer span.End() + if req.UserId == "" { s.log.Named("FindByUserId").Error("FindByUserId: invalid user ID") + span.RecordError(status.Error(codes.InvalidArgument, constant.ArgumentEmptyErrorMessage)) return nil, status.Error(codes.InvalidArgument, constant.ArgumentEmptyErrorMessage) } var checkins []*model.CheckIn - err := s.repo.FindByUserId(req.UserId, &checkins) + err := s.repo.FindByUserId(ctx, req.UserId, &checkins) if err != nil { s.log.Named("FindByUserId").Error("FindByUserId: ", zap.Error(err)) + span.RecordError(status.Error(codes.InvalidArgument, constant.ArgumentEmptyErrorMessage)) if status.Code(err) == codes.Canceled { return nil, status.Error(codes.Canceled, constant.RequestCancelledErrorMessage) diff --git a/internal/checkin/test/checkin.service_test.go b/internal/checkin/test/checkin.service_test.go index 6b327aa..34c7b06 100644 --- a/internal/checkin/test/checkin.service_test.go +++ b/internal/checkin/test/checkin.service_test.go @@ -5,12 +5,15 @@ import ( "testing" "github.com/golang/mock/gomock" + "github.com/isd-sgcu/rpkm67-checkin/config" "github.com/isd-sgcu/rpkm67-checkin/constant" "github.com/isd-sgcu/rpkm67-checkin/internal/checkin" mock_checkin "github.com/isd-sgcu/rpkm67-checkin/mocks/checkin" + "github.com/isd-sgcu/rpkm67-checkin/tracer" proto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/checkin/checkin/v1" "github.com/isd-sgcu/rpkm67-model/model" "github.com/stretchr/testify/suite" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -18,22 +21,28 @@ import ( type CheckinServiceTest struct { suite.Suite - controller *gomock.Controller - logger *zap.Logger - checkinsModel []*model.CheckIn - checkinModel *model.CheckIn - checkinsProto []*proto.CheckIn - checkinProto *proto.CheckIn + controller *gomock.Controller + logger *zap.Logger + tracer trace.Tracer + checkinsModel []*model.CheckIn + checkinModel *model.CheckIn + checkinsProto []*proto.CheckIn + checkinProto *proto.CheckIn createCheckInProtoRequest *proto.CreateCheckInRequest findByEmailCheckInRequest *proto.FindByEmailCheckInRequest findByUserIdCheckInRequest *proto.FindByUserIdCheckInRequest } -func TestPinService(t *testing.T) { +func TestCheckinService(t *testing.T) { suite.Run(t, new(CheckinServiceTest)) } func (t *CheckinServiceTest) SetupTest() { + tracer, err := tracer.New(&config.Config{}) + if err != nil { + t.T().Fatal(err) + } + t.tracer = tracer.Tracer("test") t.controller = gomock.NewController(t.T()) t.logger = zap.NewNop() t.checkinsModel = MockCheckInsModel() @@ -41,9 +50,9 @@ func (t *CheckinServiceTest) SetupTest() { t.checkinsProto = checkin.ModelToProtoList(t.checkinsModel) t.checkinProto = t.checkinsProto[0] t.createCheckInProtoRequest = &proto.CreateCheckInRequest{ - Email: t.checkinProto.Email, + Email: t.checkinProto.Email, UserId: t.checkinProto.UserId, - Event: t.checkinProto.Event, + Event: t.checkinProto.Event, } t.findByEmailCheckInRequest = &proto.FindByEmailCheckInRequest{ Email: t.checkinProto.Email, @@ -54,15 +63,15 @@ func (t *CheckinServiceTest) SetupTest() { } func (t *CheckinServiceTest) TestCreateSuccess() { - repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + repo := mock_checkin.NewMockRepository(t.controller) + svc := checkin.NewService(repo, t.logger, t.tracer) - expectedResp := &proto.CreateCheckInResponse { + expectedResp := &proto.CreateCheckInResponse{ CheckIn: t.checkinProto, } - - repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any()).Return(nil) - repo.EXPECT().Create(t.checkinModel).Return(nil) + + repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + repo.EXPECT().Create(gomock.Any(), t.checkinModel).Return(nil) res, err := svc.Create(context.Background(), t.createCheckInProtoRequest) @@ -73,11 +82,11 @@ func (t *CheckinServiceTest) TestCreateSuccess() { func (t *CheckinServiceTest) TestCreateInternalError() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + svc := checkin.NewService(repo, t.logger, t.tracer) expectedErr := status.Error(codes.Internal, constant.InternalServerErrorMessage) - repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any()).Return(expectedErr) + repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedErr) res, err := svc.Create(context.Background(), t.createCheckInProtoRequest) @@ -88,12 +97,12 @@ func (t *CheckinServiceTest) TestCreateInternalError() { func (t *CheckinServiceTest) TestCreateAlreadyCheckinError() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + svc := checkin.NewService(repo, t.logger, t.tracer) expectedErr := status.Error(codes.AlreadyExists, constant.AlreadyCheckinErrorMessage) - repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any()).Return(nil) - repo.EXPECT().Create(gomock.Any()).Return(expectedErr) + repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + repo.EXPECT().Create(gomock.Any(), t.checkinModel).Return(expectedErr) res, err := svc.Create(context.Background(), t.createCheckInProtoRequest) @@ -103,12 +112,12 @@ func (t *CheckinServiceTest) TestCreateAlreadyCheckinError() { func (t *CheckinServiceTest) TestCreateInvalidArgumentError() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + svc := checkin.NewService(repo, t.logger, t.tracer) expectedErr := status.Error(codes.InvalidArgument, constant.InvalidDataErrorMessage) - repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any()).Return(nil) - repo.EXPECT().Create(gomock.Any()).Return(expectedErr) + repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + repo.EXPECT().Create(gomock.Any(), gomock.Any()).Return(expectedErr) res, err := svc.Create(context.Background(), t.createCheckInProtoRequest) @@ -118,15 +127,15 @@ func (t *CheckinServiceTest) TestCreateInvalidArgumentError() { func (t *CheckinServiceTest) TestFindByEmailSuccess() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + svc := checkin.NewService(repo, t.logger, t.tracer) - expectedResp := &proto.FindByEmailCheckInResponse { + expectedResp := &proto.FindByEmailCheckInResponse{ CheckIns: t.checkinsProto, } email := t.checkinModel.Email - repo.EXPECT().FindByEmail(email, gomock.Any()).SetArg(1, t.checkinsModel).Return(nil) + repo.EXPECT().FindByEmail(gomock.Any(), email, gomock.Any()).SetArg(2, t.checkinsModel).Return(nil) res, err := svc.FindByEmail(context.Background(), t.findByEmailCheckInRequest) @@ -136,12 +145,12 @@ func (t *CheckinServiceTest) TestFindByEmailSuccess() { func (t *CheckinServiceTest) TestFindByEmailInternalError() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + svc := checkin.NewService(repo, t.logger, t.tracer) email := t.checkinModel.Email expectedErr := status.Error(codes.Internal, constant.InternalServerErrorMessage) - repo.EXPECT().FindByEmail(email, gomock.Any()).SetArg(1, t.checkinsModel).Return(expectedErr) + repo.EXPECT().FindByEmail(gomock.Any(), email, gomock.Any()).SetArg(2, t.checkinsModel).Return(expectedErr) res, err := svc.FindByEmail(context.Background(), t.findByEmailCheckInRequest) @@ -151,12 +160,10 @@ func (t *CheckinServiceTest) TestFindByEmailInternalError() { func (t *CheckinServiceTest) TestFindByEmailRequestCanceledError() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) - - email := t.checkinModel.Email + svc := checkin.NewService(repo, t.logger, t.tracer) expectedErr := status.Error(codes.Canceled, constant.RequestCancelledErrorMessage) - repo.EXPECT().FindByEmail(email, gomock.Any()).SetArg(1, t.checkinsModel).Return(expectedErr) + repo.EXPECT().FindByEmail(gomock.Any(), t.checkinModel.Email, gomock.Any()).SetArg(2, t.checkinsModel).Return(expectedErr) res, err := svc.FindByEmail(context.Background(), t.findByEmailCheckInRequest) @@ -166,13 +173,13 @@ func (t *CheckinServiceTest) TestFindByEmailRequestCanceledError() { func (t *CheckinServiceTest) TestFindByUserIdSuccess() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + svc := checkin.NewService(repo, t.logger, t.tracer) - expectedResp := &proto.FindByUserIdCheckInResponse { + expectedResp := &proto.FindByUserIdCheckInResponse{ CheckIns: t.checkinsProto, } - repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any()).SetArg(1, t.checkinsModel).Return(nil) + repo.EXPECT().FindByUserId(gomock.Any(), t.checkinModel.UserID, gomock.Any()).SetArg(2, t.checkinsModel).Return(nil) res, err := svc.FindByUserId(context.Background(), t.findByUserIdCheckInRequest) @@ -182,10 +189,10 @@ func (t *CheckinServiceTest) TestFindByUserIdSuccess() { func (t *CheckinServiceTest) TestFindByUserIdInternalError() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + svc := checkin.NewService(repo, t.logger, t.tracer) expectedErr := status.Error(codes.Internal, constant.InternalServerErrorMessage) - repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any()).Return(expectedErr) + repo.EXPECT().FindByUserId(gomock.Any(), t.checkinModel.UserID, gomock.Any()).Return(expectedErr) res, err := svc.FindByUserId(context.Background(), t.findByUserIdCheckInRequest) @@ -195,10 +202,10 @@ func (t *CheckinServiceTest) TestFindByUserIdInternalError() { func (t *CheckinServiceTest) TestFindByUserIdRequestCanceledError() { repo := mock_checkin.NewMockRepository(t.controller) - svc := checkin.NewService(repo, t.logger) + svc := checkin.NewService(repo, t.logger, t.tracer) expectedErr := status.Error(codes.Canceled, constant.RequestCancelledErrorMessage) - repo.EXPECT().FindByUserId(gomock.Any(), gomock.Any()).Return(expectedErr) + repo.EXPECT().FindByUserId(gomock.Any(), t.checkinModel.UserID, gomock.Any()).Return(expectedErr) res, err := svc.FindByUserId(context.Background(), t.findByUserIdCheckInRequest) diff --git a/microservices/opentelemetry/opentelemetry.yml b/microservices/opentelemetry/opentelemetry.yml new file mode 100644 index 0000000..3fce568 --- /dev/null +++ b/microservices/opentelemetry/opentelemetry.yml @@ -0,0 +1,27 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 1s + +exporters: + otlp: + endpoint: jaeger:4317 + tls: + insecure: true + otlphttp: + endpoint: jaeger:4318 + debug: + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp, otlphttp, debug] diff --git a/mocks/checkin/checkin.repository.go b/mocks/checkin/checkin.repository.go index edb2f8f..1838cff 100644 --- a/mocks/checkin/checkin.repository.go +++ b/mocks/checkin/checkin.repository.go @@ -5,6 +5,7 @@ package mock_checkin import ( + context "context" reflect "reflect" gomock "github.com/golang/mock/gomock" @@ -35,43 +36,43 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { } // Create mocks base method. -func (m *MockRepository) Create(checkIn *model.CheckIn) error { +func (m *MockRepository) Create(ctx context.Context, checkIn *model.CheckIn) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", checkIn) + ret := m.ctrl.Call(m, "Create", ctx, checkIn) ret0, _ := ret[0].(error) return ret0 } // Create indicates an expected call of Create. -func (mr *MockRepositoryMockRecorder) Create(checkIn interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Create(ctx, checkIn interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), checkIn) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), ctx, checkIn) } // FindByEmail mocks base method. -func (m *MockRepository) FindByEmail(email string, checkIns *[]*model.CheckIn) error { +func (m *MockRepository) FindByEmail(ctx context.Context, email string, checkIns *[]*model.CheckIn) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindByEmail", email, checkIns) + ret := m.ctrl.Call(m, "FindByEmail", ctx, email, checkIns) ret0, _ := ret[0].(error) return ret0 } // FindByEmail indicates an expected call of FindByEmail. -func (mr *MockRepositoryMockRecorder) FindByEmail(email, checkIns interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) FindByEmail(ctx, email, checkIns interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByEmail", reflect.TypeOf((*MockRepository)(nil).FindByEmail), email, checkIns) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByEmail", reflect.TypeOf((*MockRepository)(nil).FindByEmail), ctx, email, checkIns) } // FindByUserId mocks base method. -func (m *MockRepository) FindByUserId(userId string, checkIns *[]*model.CheckIn) error { +func (m *MockRepository) FindByUserId(ctx context.Context, userId string, checkIns *[]*model.CheckIn) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindByUserId", userId, checkIns) + ret := m.ctrl.Call(m, "FindByUserId", ctx, userId, checkIns) ret0, _ := ret[0].(error) return ret0 } // FindByUserId indicates an expected call of FindByUserId. -func (mr *MockRepositoryMockRecorder) FindByUserId(userId, checkIns interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) FindByUserId(ctx, userId, checkIns interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByUserId", reflect.TypeOf((*MockRepository)(nil).FindByUserId), userId, checkIns) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByUserId", reflect.TypeOf((*MockRepository)(nil).FindByUserId), ctx, userId, checkIns) } diff --git a/tracer/tracer.go b/tracer/tracer.go new file mode 100644 index 0000000..1339139 --- /dev/null +++ b/tracer/tracer.go @@ -0,0 +1,47 @@ +package tracer + +import ( + "context" + + "github.com/isd-sgcu/rpkm67-checkin/config" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +func New(conf *config.Config) (*trace.TracerProvider, error) { + exporter, err := otlptracegrpc.New( + context.Background(), + otlptracegrpc.WithInsecure(), + otlptracegrpc.WithEndpoint(conf.Tracer.Endpoint), + ) + if err != nil { + return nil, err + } + + var environment string + if conf.App.IsDevelopment() { + environment = "development" + } else { + environment = "production" + } + + tp := trace.NewTracerProvider( + trace.WithBatcher(exporter), + trace.WithResource( + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String(conf.App.ServiceName), + attribute.String("environment", environment), + ), + ), + ) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + return tp, nil +}