├── .idea └── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── app ├── comments │ ├── application.go │ ├── comments.pb.go │ ├── comments.proto │ ├── domain.go │ ├── grpc_server.go │ ├── repository.go │ └── service.go ├── gateway │ ├── application.go │ ├── comments │ │ ├── dto.go │ │ ├── http_server.go │ │ ├── repository.go │ │ └── service.go │ ├── docs │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── events │ │ └── main.go │ ├── hotels │ │ ├── dto.go │ │ ├── http_server.go │ │ ├── repository.go │ │ └── service.go │ └── users │ │ ├── dto.go │ │ ├── middleware.go │ │ └── service.go ├── hotels │ ├── application.go │ ├── domain.go │ ├── grpc_server.go │ ├── hotels.pb.go │ ├── hotels.proto │ ├── rabbit_conf.go │ ├── rabbit_consumer.go │ ├── rabbit_publisher.go │ ├── repository.go │ └── service.go ├── images │ ├── application.go │ ├── aws_storage_client.go │ ├── domain.go │ ├── grpc_server.go │ ├── image.pb.go │ ├── image.proto │ ├── image_procecess.go │ ├── rabbit_conf.go │ ├── rabbit_consumer.go │ ├── rabbit_publisher.go │ ├── repository.go │ └── service.go ├── sessions │ ├── application.go │ ├── csrf_repository.go │ ├── csrf_service.go │ ├── domain.go │ ├── grpc_server.go │ ├── repository.go │ ├── service.go │ ├── session.pb.go │ └── session.proto └── users │ ├── application.go │ ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml │ ├── domain.go │ ├── grpc_server.go │ ├── http_server.go │ ├── middleware.go │ ├── rabbit_config.go │ ├── rabbit_consumer.go │ ├── rabbit_publisher.go │ ├── redis_repository.go │ ├── repository.go │ ├── service.go │ ├── ssl │ └── instructions.sh │ ├── user.pb.go │ └── user.proto ├── cmd ├── comments │ ├── Dockerfile │ ├── Makefile │ ├── config │ │ ├── config-docker.yml │ │ └── config-local.yml │ ├── main.go │ └── migrations │ │ ├── 01_create_initial_tables.down.sql │ │ └── 01_create_initial_tables.up.sql ├── gateway │ ├── Dockerfile │ ├── Makefile │ ├── config │ │ ├── config-docker.yml │ │ └── config-local.yml │ ├── main.go │ └── ssl │ │ ├── ca.crt │ │ ├── ca.key │ │ ├── instructions.sh │ │ ├── server.crt │ │ ├── server.csr │ │ ├── server.key │ │ └── server.pem ├── hotels │ ├── Dockerfile │ ├── Makefile │ ├── config │ │ ├── config-docker.yml │ │ └── config-local.yml │ ├── main.go │ └── migrations │ │ ├── 01_create_initial_tables.down.sql │ │ └── 01_create_initial_tables.up.sql ├── images │ ├── Dockerfile │ ├── Makefile │ ├── config │ │ ├── config-docker.yml │ │ └── config-local.yml │ ├── main.go │ └── migrations │ │ ├── 01_create_initial_tables.down.sql │ │ └── 01_create_initial_tables.up.sql ├── sessions │ ├── Dockerfile │ ├── config │ │ ├── config-docker.yaml │ │ └── config-local.yml │ └── main.go └── users │ ├── Dockerfile │ ├── Makefile │ ├── config │ ├── config-docker.yml │ └── config-local.yml │ ├── main.go │ ├── migrations │ ├── 01_create_initial_tables.down.sql │ └── 01_create_initial_tables.up.sql │ └── ssl │ ├── ca.crt │ ├── ca.key │ ├── instructions.sh │ ├── server.crt │ ├── server.csr │ ├── server.key │ └── server.pem ├── docker-compose.local.yml ├── docker-compose.yml ├── docker ├── ReadMe.Md ├── alerts.yml ├── prometheus-local.yml └── prometheus.yml ├── go.mod └── pkg ├── config └── config.go ├── grpc_client ├── grpc_client.go └── interceptor.go ├── grpc_errors └── grpc_errors.go ├── http_errors └── http_errors.go ├── jaeger └── jaeger.go ├── logger └── logger.go ├── pagination └── pagination.go ├── postgres ├── postgres.go └── types.go ├── rabbitmq └── rabbitmq.go └── redis └── redis.go /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | /aws.xml 7 | /markdown.xml 8 | /microservices-demo.iml 9 | /modules.xml 10 | /inspectionProfiles/Project_Default.xml 11 | /protoeditor.xml 12 | /sqldialects.xml 13 | /vcs.xml 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo Microservices 2 | 3 | Project implements ingredients for your startup named BNB, but beware that Air is taken already! 4 | 5 | This repository represents a showcase of microservices written in Go language. 6 | 7 | The repository was created in April 2021 (using Go 1.16) and updated in March 2023 (using Go 1.20). 8 | 9 | Yes, it's highly over-engineered with the purpose of showcasing the "how to" different techniques and conventions. 10 | 11 | ## Setup 12 | 13 | I'm using Linux. For other operating systems, you have to figure out the potential problems you might have. 14 | 15 | ### Prerequisites 16 | 17 | `docker` and `docker-compose` - no instructions here. 18 | 19 | `protoc` - I've used version 3.12.3 (you can check your version with `apt list -a protobuf-compiler`) - (to install, 20 | run `sudo apt install -y protobuf-compiler`) 21 | 22 | `swag` - to install, run `go install github.com/swaggo/swag/cmd/swag@v1.8.10` 23 | 24 | `migrate` - to install run `go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.15.2` 25 | 26 | Note : you can install `swag` and `migrate` if needed by running `make install_swag_and_migrate` (NOT inside this repo) 27 | 28 | ### Steps 29 | 30 | #### DockerCompose 31 | 32 | * run dependencies locally `docker-compose -f docker-compose.local.yml up` 33 | 34 | * (later) to stop containers gracefully use (so you can start them) `docker-compose -f docker-compose.local.yml stop` 35 | * (later) ... then pick up where you stopped the containers `docker-compose -f docker-compose.local.yml start` 36 | * (later) to delete containers `docker-compose -f docker-compose.local.yml down` 37 | 38 | #### Containers 39 | 40 | The following containers will get started : 41 | `postgis_images_container` - this is the postgresql database for images microservice 42 | `postgis_comments_container` - this is the postgresql database for comments microservice 43 | `postgis_users_container` - this is the postgresql database for users microservice 44 | `postgis_hotels_container` - this is the postgresql database for hotels microservice 45 | 46 | `redis_users` - this is the redis container, 47 | 48 | `node_exporter_container` - 49 | 50 | `microservices-demo_rabbitmq_1` - this is the rabbitmq container, navigate 51 | to [http://localhost:15672](http://localhost:15672) for the UI. Use `guest` for both user and password. 52 | `grafana_container` - grafana container, navigate to [http://localhost:3000](http://localhost:3000) for the beautiful 53 | UI. Use `admin` for both password and user. 54 | `prometheus_container` - prometheus container, navigate to [http://localhost:9090](http://localhost:9090) for the 55 | UI. 56 | `jaeger_container` - jaeger container, navigate to [http://localhost:16686](http://localhost:16686) to see the UI. 57 | `minio_aws_container` - 58 | 59 | TODO : add `adminer` 60 | 61 | ## Initial Database Setup using Make 62 | 63 | * migrate all databases `make init_databases` 64 | 65 | ## Generate Swagger 66 | 67 | run: `make swagger_api` 68 | 69 | ### Swagger UI: 70 | 71 | * https://localhost:8081/swagger/index.html (note this is a HTTPS route - see certificate troubleshooting) 72 | 73 | Note : navigate to `chrome://flags/#allow-insecure-localhost` and set it to `enabled` to get rid of the warning. 74 | 75 | ## Packages choices 76 | 77 | 1. Uber Zap for logging, because I consider it to be flexible and well written. By flexible, I understand that it can 78 | log to Kafka if you want to. 79 | 2. Echo 80 | 81 | ## Interfaces (in Go) Design Principles: 82 | 83 | 1. Define the interface where it is being used 84 | 2. Expand request fields to make Service / Repository interface methods more readable and independent of transport layer 85 | 3. GRPC client should be passed as factory functions (which returns the actual interface of the client) 86 | 4. 87 | -------------------------------------------------------------------------------- /app/comments/application.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/go-playground/validator/v10" 13 | grpcRecovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" 14 | grpcCtxTags "github.com/grpc-ecosystem/go-grpc-middleware/tags" 15 | grpcOpenTracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing" 16 | grpcPrometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 17 | 18 | "github.com/jackc/pgx/v4/pgxpool" 19 | "github.com/labstack/echo/v4" 20 | "github.com/opentracing/opentracing-go" 21 | "github.com/prometheus/client_golang/prometheus/promhttp" 22 | "google.golang.org/grpc" 23 | "google.golang.org/grpc/keepalive" 24 | "google.golang.org/grpc/reflection" 25 | 26 | "github.com/badu/microservices-demo/app/users" 27 | "github.com/badu/microservices-demo/pkg/config" 28 | "github.com/badu/microservices-demo/pkg/grpc_client" 29 | "github.com/badu/microservices-demo/pkg/logger" 30 | ) 31 | 32 | type Application struct { 33 | e *echo.Echo 34 | logger logger.Logger 35 | cfg *config.Config 36 | pgxPool *pgxpool.Pool 37 | tracer opentracing.Tracer 38 | } 39 | 40 | func NewApplication(logger logger.Logger, cfg *config.Config, pgxPool *pgxpool.Pool, tracer opentracing.Tracer) Application { 41 | return Application{e: echo.New(), logger: logger, cfg: cfg, pgxPool: pgxPool, tracer: tracer} 42 | } 43 | 44 | func UsersGRPCClientFactory( 45 | logger logger.Logger, 46 | cfg *config.Config, 47 | tracer opentracing.Tracer, 48 | ) func(ctx context.Context) (*grpc.ClientConn, users.UserServiceClient, error) { 49 | commonMW := grpc_client.NewClientMiddleware(logger, cfg, tracer) 50 | return func(ctx context.Context) (*grpc.ClientConn, users.UserServiceClient, error) { 51 | conn, err := grpc_client.NewGRPCClientServiceConn(ctx, commonMW, cfg.GRPC.UserServicePort) 52 | if err != nil { 53 | return nil, nil, err 54 | } 55 | return conn, users.NewUserServiceClient(conn), nil 56 | } 57 | } 58 | 59 | func (a *Application) Run() error { 60 | ctx, cancel := context.WithCancel(context.Background()) 61 | defer cancel() 62 | 63 | validate := validator.New() 64 | 65 | repository := NewRepository(a.pgxPool) 66 | service := NewService(&repository, a.logger, UsersGRPCClientFactory(a.logger, a.cfg, a.tracer)) 67 | server := NewServer(&service, a.logger, a.cfg, validate) 68 | 69 | listener, err := net.Listen("tcp", a.cfg.GRPCServer.Port) 70 | if err != nil { 71 | return errors.Join(err, errors.New("comments application listener")) 72 | } 73 | defer listener.Close() 74 | 75 | go func() { 76 | router := echo.New() 77 | router.GET("/metrics", echo.WrapHandler(promhttp.Handler())) 78 | a.logger.Infof("Metrics server is running on port: %s", a.cfg.Metrics.Port) 79 | if err := router.Start(a.cfg.Metrics.Port); err != nil { 80 | a.logger.Error(err) 81 | cancel() 82 | } 83 | }() 84 | 85 | grpcServer := grpc.NewServer( 86 | grpc.KeepaliveParams( 87 | keepalive.ServerParameters{ 88 | MaxConnectionIdle: a.cfg.GRPCServer.MaxConnectionIdle * time.Minute, 89 | Timeout: a.cfg.GRPCServer.Timeout * time.Second, 90 | MaxConnectionAge: a.cfg.GRPCServer.MaxConnectionAge * time.Minute, 91 | Time: a.cfg.GRPCServer.Timeout * time.Minute, 92 | }, 93 | ), 94 | grpc.ChainUnaryInterceptor( 95 | grpcCtxTags.UnaryServerInterceptor(), 96 | grpcOpenTracing.UnaryServerInterceptor(), 97 | grpcPrometheus.UnaryServerInterceptor, 98 | grpcRecovery.UnaryServerInterceptor(), 99 | ), 100 | ) 101 | 102 | RegisterCommentsServiceServer(grpcServer, &server) 103 | 104 | grpcPrometheus.Register(grpcServer) 105 | 106 | go func() { 107 | a.logger.Infof("GRPC Server is listening on port: %v", a.cfg.GRPCServer.Port) 108 | a.logger.Fatal(grpcServer.Serve(listener)) 109 | }() 110 | 111 | if a.cfg.ProductionMode() { 112 | reflection.Register(grpcServer) 113 | } 114 | 115 | quit := make(chan os.Signal, 1) 116 | signal.Notify(quit, os.Interrupt, syscall.SIGTERM) 117 | 118 | select { 119 | case v := <-quit: 120 | a.logger.Errorf("signal.Notify: %v", v) 121 | case done := <-ctx.Done(): 122 | a.logger.Errorf("ctx.Done: %v", done) 123 | } 124 | 125 | if err := a.e.Server.Shutdown(ctx); err != nil { 126 | return errors.Join(err, errors.New("comments application server shutdown")) 127 | } 128 | 129 | grpcServer.GracefulStop() 130 | a.logger.Info("grpc server shutdown correctly") 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /app/comments/comments.proto: -------------------------------------------------------------------------------- 1 | // protoc --go_out=plugins=grpc:. *.proto --go_opt=paths=source_relative users.proto ./app/users/users.proto 2 | 3 | syntax = "proto3"; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | import "app/users/user.proto"; 7 | 8 | package comments; 9 | 10 | option go_package = "github.com/badu/microservices-demo/app/comments;comments"; 11 | 12 | message Comment { 13 | google.protobuf.Timestamp CreatedAt = 7; 14 | google.protobuf.Timestamp UpdatedAt = 8; 15 | string CommentID = 1; 16 | string HotelID = 2; 17 | string UserID = 3; 18 | string Message = 4; 19 | repeated string Photos = 5; 20 | double Rating = 6; 21 | } 22 | 23 | message CommentFull { 24 | google.protobuf.Timestamp CreatedAt = 7; 25 | google.protobuf.Timestamp UpdatedAt = 8; 26 | users.User User = 3; 27 | string CommentID = 1; 28 | string HotelID = 2; 29 | string Message = 4; 30 | repeated string Photos = 5; 31 | double Rating = 6; 32 | } 33 | 34 | message CreateCommentRequest { 35 | string HotelID = 1; 36 | string UserID = 2; 37 | string Message = 3; 38 | repeated string Photos = 4; 39 | double Rating = 5; 40 | } 41 | 42 | message CreateCommentResponse { 43 | Comment Comment = 1; 44 | } 45 | 46 | message GetCommentByIDRequest { 47 | string CommentID = 1; 48 | } 49 | 50 | message GetCommentByIDResponse { 51 | Comment Comment = 1; 52 | } 53 | 54 | message UpdateCommentRequest { 55 | string CommentID = 1; 56 | string Message = 2; 57 | repeated string Photos = 3; 58 | double Rating = 4; 59 | } 60 | 61 | message UpdateCommentResponse { 62 | Comment Comment = 1; 63 | } 64 | 65 | message GetCommentsByHotelRequest { 66 | string HotelID = 1; 67 | int64 page = 2; 68 | int64 size = 3; 69 | } 70 | 71 | message GetCommentsByHotelResponse { 72 | repeated CommentFull Comments = 6; 73 | int64 TotalCount = 1; 74 | int64 TotalPages = 2; 75 | int64 Page = 3; 76 | int64 Size = 4; 77 | bool HasMore = 5; 78 | } 79 | 80 | service commentsService { 81 | rpc CreateComment(CreateCommentRequest) returns (CreateCommentResponse) {} 82 | rpc GetCommByID(GetCommentByIDRequest) returns (GetCommentByIDResponse) {} 83 | rpc UpdateComment(UpdateCommentRequest) returns (UpdateCommentResponse) {} 84 | rpc GetByHotelID(GetCommentsByHotelRequest) returns (GetCommentsByHotelResponse) {} 85 | } 86 | -------------------------------------------------------------------------------- /app/comments/domain.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | "google.golang.org/protobuf/types/known/timestamppb" 8 | 9 | "github.com/badu/microservices-demo/app/users" 10 | ) 11 | 12 | type CommentDO struct { 13 | CreatedAt *time.Time `json:"created_at"` 14 | UpdatedAt *time.Time `json:"updated_at"` 15 | Message string `json:"message" validate:"required,min=5,max=500"` 16 | Photos []string `json:"photos,omitempty"` 17 | Rating float64 `json:"rating" validate:"required,min=0,max=10"` 18 | CommentID uuid.UUID `json:"comment_id"` 19 | HotelID uuid.UUID `json:"hotel_id"` 20 | UserID uuid.UUID `json:"user_id"` 21 | } 22 | 23 | func (c *CommentDO) ToProto() *Comment { 24 | return &Comment{ 25 | CommentID: c.CommentID.String(), 26 | HotelID: c.HotelID.String(), 27 | UserID: c.UserID.String(), 28 | Message: c.Message, 29 | Photos: c.Photos, 30 | Rating: c.Rating, 31 | CreatedAt: timestamppb.New(*c.CreatedAt), 32 | UpdatedAt: timestamppb.New(*c.UpdatedAt), 33 | } 34 | } 35 | 36 | // All Comments response with pagination 37 | type List struct { 38 | Comments []*CommentDO `json:"comments"` 39 | TotalCount int `json:"totalCount"` 40 | TotalPages int `json:"totalPages"` 41 | Page int `json:"page"` 42 | Size int `json:"size"` 43 | HasMore bool `json:"hasMore"` 44 | } 45 | 46 | // All Comments response with pagination 47 | type FullList struct { 48 | Comments []*CommentFull `json:"comments"` 49 | TotalCount int `json:"totalCount"` 50 | TotalPages int `json:"totalPages"` 51 | Page int `json:"page"` 52 | Size int `json:"size"` 53 | HasMore bool `json:"hasMore"` 54 | } 55 | 56 | type FullCommentsList struct { 57 | UsersList []users.UserResponse 58 | CommentsList List 59 | } 60 | 61 | func (h *List) ToProto() []*Comment { 62 | commentsList := make([]*Comment, 0, len(h.Comments)) 63 | for _, hotel := range h.Comments { 64 | commentsList = append(commentsList, hotel.ToProto()) 65 | } 66 | return commentsList 67 | } 68 | 69 | func (h *List) ToHotelByIDProto(userz []*users.User) []*CommentFull { 70 | userMap := make(map[string]*users.User, len(userz)) 71 | for _, user := range userz { 72 | userMap[user.UserID] = user 73 | } 74 | 75 | commentsList := make([]*CommentFull, 0, len(h.Comments)) 76 | for _, comm := range h.Comments { 77 | user := userMap[comm.UserID.String()] 78 | 79 | commentsList = append(commentsList, &CommentFull{ 80 | CommentID: comm.CommentID.String(), 81 | HotelID: comm.HotelID.String(), 82 | User: &users.User{ 83 | UserID: user.UserID, 84 | FirstName: user.FirstName, 85 | LastName: user.LastName, 86 | Email: user.Email, 87 | Avatar: user.Avatar, 88 | Role: user.Role, 89 | }, 90 | Message: comm.Message, 91 | Photos: comm.Photos, 92 | Rating: comm.Rating, 93 | CreatedAt: timestamppb.New(*comm.CreatedAt), 94 | UpdatedAt: timestamppb.New(*comm.UpdatedAt), 95 | }) 96 | } 97 | return commentsList 98 | } 99 | -------------------------------------------------------------------------------- /app/comments/service.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | uuid "github.com/satori/go.uuid" 9 | "google.golang.org/grpc" 10 | 11 | "github.com/badu/microservices-demo/pkg/pagination" 12 | 13 | "github.com/badu/microservices-demo/app/users" 14 | "github.com/badu/microservices-demo/pkg/logger" 15 | ) 16 | 17 | type Repository interface { 18 | Create(ctx context.Context, comment *CommentDO) (*CommentDO, error) 19 | GetByID(ctx context.Context, commentID uuid.UUID) (*CommentDO, error) 20 | Update(ctx context.Context, comment *CommentDO) (*CommentDO, error) 21 | GetByHotelID(ctx context.Context, hotelID uuid.UUID, query *pagination.Pagination) (*List, error) 22 | } 23 | 24 | type ServiceImpl struct { 25 | repository Repository 26 | logger logger.Logger 27 | grpcClientFactory func(ctx context.Context) (*grpc.ClientConn, users.UserServiceClient, error) 28 | } 29 | 30 | func NewService( 31 | repository Repository, 32 | logger logger.Logger, 33 | grpcClientFactory func(ctx context.Context) (*grpc.ClientConn, users.UserServiceClient, error), 34 | ) ServiceImpl { 35 | return ServiceImpl{repository: repository, logger: logger, grpcClientFactory: grpcClientFactory} 36 | } 37 | 38 | func (s *ServiceImpl) Create(ctx context.Context, comment *CommentDO) (*CommentDO, error) { 39 | span, ctx := opentracing.StartSpanFromContext(ctx, "comments_service.Create") 40 | defer span.Finish() 41 | return s.repository.Create(ctx, comment) 42 | } 43 | 44 | func (s *ServiceImpl) GetByID(ctx context.Context, commentID uuid.UUID) (*CommentDO, error) { 45 | span, ctx := opentracing.StartSpanFromContext(ctx, "comments_service.GetByID") 46 | defer span.Finish() 47 | return s.repository.GetByID(ctx, commentID) 48 | } 49 | 50 | func (s *ServiceImpl) Update(ctx context.Context, comment *CommentDO) (*CommentDO, error) { 51 | span, ctx := opentracing.StartSpanFromContext(ctx, "comments_service.Update") 52 | defer span.Finish() 53 | return s.repository.Update(ctx, comment) 54 | } 55 | 56 | func (s *ServiceImpl) GetByHotelID(ctx context.Context, hotelID uuid.UUID, query *pagination.Pagination) (*FullList, error) { 57 | span, ctx := opentracing.StartSpanFromContext(ctx, "comments_service.GetByHotelID") 58 | defer span.Finish() 59 | 60 | commentsList, err := s.repository.GetByHotelID(ctx, hotelID, query) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | uniqUserIDsMap := make(map[string]struct{}, len(commentsList.Comments)) 66 | for _, comm := range commentsList.Comments { 67 | uniqUserIDsMap[comm.UserID.String()] = struct{}{} 68 | } 69 | 70 | userIDS := make([]string, 0, len(commentsList.Comments)) 71 | for key := range uniqUserIDsMap { 72 | userIDS = append(userIDS, key) 73 | } 74 | 75 | conn, client, err := s.grpcClientFactory(ctx) 76 | if err != nil { 77 | return nil, errors.Join(err, errors.New("comments service GetByHotelID")) 78 | } 79 | defer conn.Close() 80 | 81 | usersByIDs, err := client.GetUsersByIDs(ctx, &users.GetUsersByIDsRequest{UsersIDs: userIDS}) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return &FullList{ 87 | TotalCount: commentsList.TotalCount, 88 | TotalPages: commentsList.TotalPages, 89 | Page: commentsList.Page, 90 | Size: commentsList.Size, 91 | HasMore: commentsList.HasMore, 92 | Comments: commentsList.ToHotelByIDProto(usersByIDs.GetUsers()), 93 | }, nil 94 | } 95 | -------------------------------------------------------------------------------- /app/gateway/comments/dto.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | 8 | "github.com/badu/microservices-demo/app/gateway/users" 9 | 10 | "github.com/badu/microservices-demo/app/comments" 11 | ) 12 | 13 | type Comment struct { 14 | CreatedAt *time.Time `json:"created_at"` 15 | UpdatedAt *time.Time `json:"updated_at"` 16 | Message string `json:"message" validate:"required,min=5,max=500"` 17 | Photos []string `json:"photos,omitempty"` 18 | Rating float64 `json:"rating" validate:"required,min=0,max=10"` 19 | CommentID uuid.UUID `json:"comment_id"` 20 | HotelID uuid.UUID `json:"hotel_id"` 21 | UserID uuid.UUID `json:"user_id"` 22 | } 23 | 24 | type List struct { 25 | Comments []*CommentFull `json:"comments"` 26 | TotalCount int64 `json:"totalCount"` 27 | TotalPages int64 `json:"totalPages"` 28 | Page int64 `json:"page"` 29 | Size int64 `json:"size"` 30 | HasMore bool `json:"hasMore"` 31 | } 32 | 33 | func fromProto(comment *comments.Comment) (*Comment, error) { 34 | commUUID, err := uuid.FromString(comment.CommentID) 35 | if err != nil { 36 | return nil, err 37 | } 38 | userUUID, err := uuid.FromString(comment.UserID) 39 | if err != nil { 40 | return nil, err 41 | } 42 | hotelUUID, err := uuid.FromString(comment.HotelID) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if err := comment.CreatedAt.CheckValid(); err != nil { 47 | return nil, err 48 | } 49 | if err := comment.UpdatedAt.CheckValid(); err != nil { 50 | return nil, err 51 | } 52 | createdAt := comment.CreatedAt.AsTime() 53 | updatedAt := comment.UpdatedAt.AsTime() 54 | 55 | return &Comment{ 56 | CommentID: commUUID, 57 | HotelID: hotelUUID, 58 | UserID: userUUID, 59 | Message: comment.Message, 60 | Photos: comment.Photos, 61 | Rating: comment.Rating, 62 | CreatedAt: &createdAt, 63 | UpdatedAt: &updatedAt, 64 | }, nil 65 | } 66 | 67 | type CommentFull struct { 68 | User *users.CommentUser `json:"user"` 69 | CreatedAt *time.Time `json:"createdAt"` 70 | UpdatedAt *time.Time `json:"updatedAt"` 71 | Message string `json:"message"` 72 | Photos []string `json:"photos"` 73 | Rating float64 `json:"rating"` 74 | CommentID uuid.UUID `json:"comment_id"` 75 | HotelID uuid.UUID `json:"hotel_id"` 76 | } 77 | 78 | func fromProtoToFull(comm *comments.CommentFull) (*CommentFull, error) { 79 | commUUID, err := uuid.FromString(comm.GetCommentID()) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | hotelUUID, err := uuid.FromString(comm.GetHotelID()) 85 | if err != nil { 86 | return nil, err 87 | } 88 | if err := comm.CreatedAt.CheckValid(); err != nil { 89 | return nil, err 90 | } 91 | if err := comm.UpdatedAt.CheckValid(); err != nil { 92 | return nil, err 93 | } 94 | createdAt := comm.CreatedAt.AsTime() 95 | updatedAt := comm.UpdatedAt.AsTime() 96 | 97 | user := &users.CommentUser{} 98 | user.FromProto(comm.GetUser()) 99 | 100 | return &CommentFull{ 101 | CommentID: commUUID, 102 | HotelID: hotelUUID, 103 | User: user, 104 | Message: comm.GetMessage(), 105 | Photos: comm.GetPhotos(), 106 | Rating: comm.GetRating(), 107 | CreatedAt: &createdAt, 108 | UpdatedAt: &updatedAt, 109 | }, nil 110 | } 111 | -------------------------------------------------------------------------------- /app/gateway/comments/repository.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "github.com/opentracing/opentracing-go" 12 | uuid "github.com/satori/go.uuid" 13 | ) 14 | 15 | const ( 16 | prefix = "comments:" 17 | expiration = time.Second * 3600 18 | ) 19 | 20 | type RepositoryImpl struct { 21 | client *redis.Client 22 | } 23 | 24 | func NewRepository(redisConn *redis.Client) RepositoryImpl { 25 | return RepositoryImpl{client: redisConn} 26 | } 27 | 28 | func (c *RepositoryImpl) CommentByID(ctx context.Context, commentID uuid.UUID) (*Comment, error) { 29 | span, ctx := opentracing.StartSpanFromContext(ctx, "gateway_comments_repository.CommentByID") 30 | defer span.Finish() 31 | 32 | rawComment, err := c.client.Get(ctx, c.createKey(commentID)).Bytes() 33 | if err != nil { 34 | return nil, errors.Join(err, errors.New("repository redis client getting comment by id")) 35 | } 36 | 37 | var result Comment 38 | if err := json.Unmarshal(rawComment, &result); err != nil { 39 | return nil, errors.Join(err, errors.New("repository unmarshalling json from redis")) 40 | } 41 | return &result, nil 42 | } 43 | 44 | func (c *RepositoryImpl) SetComment(ctx context.Context, comment *Comment) error { 45 | span, ctx := opentracing.StartSpanFromContext(ctx, "gateway_comments_repository.SetComment") 46 | defer span.Finish() 47 | 48 | jsonComment, err := json.Marshal(comment) 49 | if err != nil { 50 | return errors.Join(err, errors.New("repository marshalling json for redis")) 51 | } 52 | 53 | if err := c.client.SetEX(ctx, c.createKey(comment.CommentID), string(jsonComment), expiration).Err(); err != nil { 54 | return errors.Join(err, errors.New("repository storing comment in redis")) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (c *RepositoryImpl) DeleteComment(ctx context.Context, commentID uuid.UUID) error { 61 | span, ctx := opentracing.StartSpanFromContext(ctx, "gateway_comments_repository.DeleteComment") 62 | defer span.Finish() 63 | 64 | if err := c.client.Del(ctx, c.createKey(commentID)).Err(); err != nil { 65 | return errors.Join(err, errors.New("repository deleting comment from redis")) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (c *RepositoryImpl) createKey(commID uuid.UUID) string { 72 | return fmt.Sprintf("%s: %s", prefix, commID.String()) 73 | } 74 | -------------------------------------------------------------------------------- /app/gateway/events/main.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "google.golang.org/grpc" 8 | 9 | "github.com/badu/microservices-demo/app/comments" 10 | "github.com/badu/microservices-demo/app/hotels" 11 | "github.com/badu/microservices-demo/app/sessions" 12 | "github.com/badu/microservices-demo/app/users" 13 | ) 14 | 15 | type commonGRPCRequestEvent struct { 16 | Ctx context.Context 17 | wg *sync.WaitGroup 18 | Err error 19 | Conn *grpc.ClientConn 20 | } 21 | 22 | func (r *commonGRPCRequestEvent) Async() bool { 23 | return false 24 | } 25 | 26 | func (r *commonGRPCRequestEvent) Reply() { 27 | r.wg.Done() 28 | } 29 | 30 | func (r *commonGRPCRequestEvent) WaitReply() { 31 | r.wg.Wait() 32 | } 33 | 34 | func NewRequireHotelsGRPCClient(ctx context.Context) *RequireHotelsGRPCClient { 35 | var wg sync.WaitGroup 36 | wg.Add(1) 37 | result := RequireHotelsGRPCClient{commonGRPCRequestEvent: commonGRPCRequestEvent{Ctx: ctx, wg: &wg}} 38 | return &result 39 | } 40 | 41 | type RequireHotelsGRPCClient struct { 42 | commonGRPCRequestEvent 43 | Client hotels.HotelsServiceClient 44 | } 45 | 46 | func (r *RequireHotelsGRPCClient) EventID() string { 47 | return "RequireHotelsGRPCClientEvent" 48 | } 49 | 50 | func NewRequireCommentsGRPCClient(ctx context.Context) *RequireCommentsGRPCClient { 51 | var wg sync.WaitGroup 52 | wg.Add(1) 53 | result := RequireCommentsGRPCClient{commonGRPCRequestEvent: commonGRPCRequestEvent{Ctx: ctx, wg: &wg}} 54 | return &result 55 | } 56 | 57 | type RequireCommentsGRPCClient struct { 58 | commonGRPCRequestEvent 59 | Client comments.CommentsServiceClient 60 | } 61 | 62 | func (r *RequireCommentsGRPCClient) EventID() string { 63 | return "RequireCommentsGRPCClientEvent" 64 | } 65 | 66 | func NewRequireUsersGRPCClient(ctx context.Context) *RequireUsersGRPCClient { 67 | var wg sync.WaitGroup 68 | wg.Add(1) 69 | result := RequireUsersGRPCClient{commonGRPCRequestEvent: commonGRPCRequestEvent{Ctx: ctx, wg: &wg}} 70 | return &result 71 | } 72 | 73 | type RequireUsersGRPCClient struct { 74 | commonGRPCRequestEvent 75 | Client users.UserServiceClient 76 | } 77 | 78 | func (r *RequireUsersGRPCClient) EventID() string { 79 | return "RequireUsersGRPCClientEvent" 80 | } 81 | 82 | func NewRequireSessionsGRPCClient(ctx context.Context) *RequireSessionsGRPCClient { 83 | var wg sync.WaitGroup 84 | wg.Add(1) 85 | result := RequireSessionsGRPCClient{commonGRPCRequestEvent: commonGRPCRequestEvent{Ctx: ctx, wg: &wg}} 86 | return &result 87 | } 88 | 89 | type RequireSessionsGRPCClient struct { 90 | commonGRPCRequestEvent 91 | Client sessions.AuthorizationServiceClient 92 | } 93 | 94 | func (r *RequireSessionsGRPCClient) EventID() string { 95 | return "RequireSessionsGRPCClientEvent" 96 | } 97 | -------------------------------------------------------------------------------- /app/gateway/hotels/dto.go: -------------------------------------------------------------------------------- 1 | package hotels 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | 8 | hotelsService "github.com/badu/microservices-demo/app/hotels" 9 | ) 10 | 11 | type Hotel struct { 12 | Image *string `json:"image,omitempty"` 13 | UpdatedAt *time.Time `json:"updated_at"` 14 | CreatedAt *time.Time `json:"created_at"` 15 | Longitude *float64 `json:"longitude,omitempty"` 16 | Latitude *float64 `json:"latitude,omitempty"` 17 | Location string `json:"location" validate:"required,min=10,max=250"` 18 | Description string `json:"description,omitempty" validate:"required,min=10,max=250"` 19 | City string `json:"city,omitempty" validate:"required,min=3,max=25"` 20 | Country string `json:"country,omitempty" validate:"required,min=3,max=25"` 21 | Email string `json:"email,omitempty" validate:"required,email"` 22 | Name string `json:"name" validate:"required,min=3,max=25"` 23 | Photos []string `json:"photos,omitempty"` 24 | Rating float64 `json:"rating" validate:"required,min=0,max=10"` 25 | CommentsCount int `json:"comments_count,omitempty"` 26 | HotelID uuid.UUID `json:"hotel_id"` 27 | } 28 | 29 | type ListResult struct { 30 | Hotels []*Hotel `json:"hotels"` 31 | TotalCount int64 `json:"totalCount"` 32 | TotalPages int64 `json:"totalPages"` 33 | Page int64 `json:"page"` 34 | Size int64 `json:"size"` 35 | HasMore bool `json:"hasMore"` 36 | } 37 | 38 | func fromProto(v *hotelsService.Hotel) (*Hotel, error) { 39 | hotelUUID, err := uuid.FromString(v.GetHotelID()) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if err := v.CreatedAt.CheckValid(); err != nil { 44 | return nil, err 45 | } 46 | if err := v.UpdatedAt.CheckValid(); err != nil { 47 | return nil, err 48 | } 49 | createdAt := v.CreatedAt.AsTime() 50 | updatedAt := v.UpdatedAt.AsTime() 51 | return &Hotel{ 52 | HotelID: hotelUUID, 53 | Name: v.GetName(), 54 | Email: v.GetEmail(), 55 | Country: v.GetCountry(), 56 | City: v.GetCity(), 57 | Description: v.GetDescription(), 58 | Location: v.GetLocation(), 59 | Rating: v.GetRating(), 60 | Image: &v.Image, 61 | Photos: v.GetPhotos(), 62 | CommentsCount: int(v.GetCommentsCount()), 63 | Latitude: &v.Latitude, 64 | Longitude: &v.Longitude, 65 | CreatedAt: &createdAt, 66 | UpdatedAt: &updatedAt, 67 | }, nil 68 | } 69 | -------------------------------------------------------------------------------- /app/gateway/hotels/repository.go: -------------------------------------------------------------------------------- 1 | package hotels 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "github.com/opentracing/opentracing-go" 12 | uuid "github.com/satori/go.uuid" 13 | ) 14 | 15 | const ( 16 | prefix = "comments:" 17 | expiration = time.Second * 3600 18 | ) 19 | 20 | type RepositoryImpl struct { 21 | client *redis.Client 22 | } 23 | 24 | func NewRepository(redisClient *redis.Client) RepositoryImpl { 25 | return RepositoryImpl{client: redisClient} 26 | } 27 | 28 | func (h *RepositoryImpl) GetHotelByID(ctx context.Context, hotelID uuid.UUID) (*Hotel, error) { 29 | span, ctx := opentracing.StartSpanFromContext(ctx, "gateway_hotels_repository.GetHotelByID") 30 | defer span.Finish() 31 | 32 | rawJson, err := h.client.Get(ctx, h.createKey(hotelID)).Bytes() 33 | if err != nil { 34 | return nil, errors.Join(err, errors.New("getting hotel from redis in repository")) 35 | } 36 | 37 | var result Hotel 38 | if err := json.Unmarshal(rawJson, &result); err != nil { 39 | return nil, errors.Join(err, errors.New("unmarshalling hotel from redis in repository")) 40 | } 41 | 42 | return &result, nil 43 | } 44 | 45 | func (h *RepositoryImpl) SetHotel(ctx context.Context, hotel *Hotel) error { 46 | span, ctx := opentracing.StartSpanFromContext(ctx, "gateway_hotels_repository.SetHotel") 47 | defer span.Finish() 48 | 49 | rawJson, err := json.Marshal(hotel) 50 | if err != nil { 51 | return errors.Join(err, errors.New("marshalling hotel to json for redis in repository")) 52 | } 53 | 54 | if err := h.client.SetEX(ctx, h.createKey(hotel.HotelID), string(rawJson), expiration).Err(); err != nil { 55 | return errors.Join(err, errors.New("saving hotel as json to redis in repository")) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (h *RepositoryImpl) DeleteHotel(ctx context.Context, hotelID uuid.UUID) error { 62 | span, ctx := opentracing.StartSpanFromContext(ctx, "gateway_hotels_repository.DeleteHotel") 63 | defer span.Finish() 64 | 65 | if err := h.client.Del(ctx, h.createKey(hotelID)).Err(); err != nil { 66 | return errors.Join(err, errors.New("deleting hotel from redis in repository")) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (h *RepositoryImpl) createKey(hotelID uuid.UUID) string { 73 | return fmt.Sprintf("%s: %s", prefix, hotelID.String()) 74 | } 75 | -------------------------------------------------------------------------------- /app/gateway/users/dto.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | 8 | "github.com/badu/microservices-demo/app/sessions" 9 | 10 | "github.com/badu/microservices-demo/app/users" 11 | ) 12 | 13 | type UserResponse struct { 14 | Role *string `json:"role"` 15 | Avatar *string `json:"avatar" validate:"max=250" swaggertype:"string"` 16 | CreatedAt *time.Time `json:"created_at"` 17 | UpdatedAt *time.Time `json:"updated_at"` 18 | FirstName string `json:"first_name" validate:"required,min=3,max=25"` 19 | LastName string `json:"last_name" validate:"required,min=3,max=25"` 20 | Email string `json:"email" validate:"required,email"` 21 | UserID uuid.UUID `json:"user_id"` 22 | } 23 | 24 | func fromProto(user *users.User) (*UserResponse, error) { 25 | userUUID, err := uuid.FromString(user.GetUserID()) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | if err := user.CreatedAt.CheckValid(); err != nil { 31 | return nil, err 32 | } 33 | if err := user.UpdatedAt.CheckValid(); err != nil { 34 | return nil, err 35 | } 36 | createdAt := user.CreatedAt.AsTime() 37 | updatedAt := user.UpdatedAt.AsTime() 38 | 39 | return &UserResponse{ 40 | UserID: userUUID, 41 | FirstName: user.GetFirstName(), 42 | LastName: user.GetLastName(), 43 | Email: user.GetEmail(), 44 | Role: &user.Role, 45 | Avatar: &user.Avatar, 46 | CreatedAt: &createdAt, 47 | UpdatedAt: &updatedAt, 48 | }, nil 49 | } 50 | 51 | type CommentUser struct { 52 | UserID string `json:"userId"` 53 | FirstName string `json:"firstName"` 54 | LastName string `json:"lastName"` 55 | Email string `json:"email"` 56 | Avatar string `json:"avatar"` 57 | Role string `json:"role"` 58 | } 59 | 60 | func (u *CommentUser) FromProto(user *users.User) { 61 | u.UserID = user.UserID 62 | u.FirstName = user.FirstName 63 | u.LastName = user.LastName 64 | u.Email = user.Email 65 | u.Avatar = user.Avatar 66 | u.Role = user.Role 67 | } 68 | 69 | type Session struct { 70 | SessionID string `json:"session_id"` 71 | UserID uuid.UUID `json:"user_id"` 72 | } 73 | 74 | func (s *Session) FromProto(session *sessions.Session) (*Session, error) { 75 | userUUID, err := uuid.FromString(session.GetUserID()) 76 | if err != nil { 77 | return nil, err 78 | } 79 | s.UserID = userUUID 80 | s.SessionID = session.GetSessionID() 81 | return s, nil 82 | } 83 | -------------------------------------------------------------------------------- /app/gateway/users/middleware.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/opentracing/opentracing-go" 10 | uuid "github.com/satori/go.uuid" 11 | 12 | "github.com/badu/microservices-demo/pkg/config" 13 | httpErrors "github.com/badu/microservices-demo/pkg/http_errors" 14 | "github.com/badu/microservices-demo/pkg/logger" 15 | ) 16 | 17 | type Service interface { 18 | GetByID(ctx context.Context, userUUID uuid.UUID) (*UserResponse, error) 19 | GetSessionByID(ctx context.Context, sessionID string) (*Session, error) 20 | } 21 | 22 | type SessionMiddleware struct { 23 | logger logger.Logger 24 | cfg *config.Config 25 | service Service 26 | } 27 | 28 | func NewSessionMiddleware(logger logger.Logger, cfg *config.Config, service Service) SessionMiddleware { 29 | return SessionMiddleware{logger: logger, cfg: cfg, service: service} 30 | } 31 | 32 | type RequestCtxUser struct{} 33 | 34 | type RequestCtxSession struct{} 35 | 36 | func (m *SessionMiddleware) SessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 37 | return func(c echo.Context) error { 38 | span, ctx := opentracing.StartSpanFromContext(c.Request().Context(), "gateway_users_middleware.SessionMiddleware") 39 | defer span.Finish() 40 | 41 | cookie, err := c.Cookie(m.cfg.HttpServer.SessionCookieName) 42 | if err != nil { 43 | if errors.Is(err, http.ErrNoCookie) { 44 | m.logger.Errorf("SessionMiddleware.ErrNoCookie: %v", err) 45 | return httpErrors.ErrorCtxResponse(c, err) 46 | } 47 | m.logger.Errorf("SessionMiddleware.c.Cookie: %v", err) 48 | return httpErrors.ErrorCtxResponse(c, err) 49 | } 50 | 51 | sessionByID, err := m.service.GetSessionByID(ctx, cookie.Value) 52 | if err != nil { 53 | m.logger.Errorf("SessionMiddleware.GetSessionByID: %v", err) 54 | return httpErrors.ErrorCtxResponse(c, httpErrors.Unauthorized) 55 | } 56 | 57 | userResponse, err := m.service.GetByID(ctx, sessionByID.UserID) 58 | if err != nil { 59 | m.logger.Errorf("SessionMiddleware.service.GetByID: %v", err) 60 | return httpErrors.ErrorCtxResponse(c, err) 61 | } 62 | 63 | ctx = context.WithValue(c.Request().Context(), RequestCtxUser{}, userResponse) 64 | ctx = context.WithValue(ctx, RequestCtxSession{}, sessionByID) 65 | c.SetRequest(c.Request().WithContext(ctx)) 66 | 67 | return next(c) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/gateway/users/service.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/badu/bus" 8 | "github.com/opentracing/opentracing-go" 9 | uuid "github.com/satori/go.uuid" 10 | 11 | "github.com/badu/microservices-demo/app/gateway/events" 12 | "github.com/badu/microservices-demo/app/sessions" 13 | "github.com/badu/microservices-demo/app/users" 14 | "github.com/badu/microservices-demo/pkg/logger" 15 | ) 16 | 17 | type ServiceImpl struct { 18 | logger logger.Logger 19 | } 20 | 21 | func NewService( 22 | logger logger.Logger, 23 | ) ServiceImpl { 24 | return ServiceImpl{ 25 | logger: logger, 26 | } 27 | } 28 | 29 | func (s *ServiceImpl) GetByID(ctx context.Context, userUUID uuid.UUID) (*UserResponse, error) { 30 | span, ctx := opentracing.StartSpanFromContext(ctx, "gateway_users_service.GetByID") 31 | defer span.Finish() 32 | event := events.NewRequireUsersGRPCClient(ctx) 33 | bus.Pub(event) 34 | event.WaitReply() 35 | if event.Err != nil { 36 | return nil, event.Err 37 | } 38 | 39 | defer event.Conn.Close() 40 | 41 | user, err := event.Client.GetUserByID(ctx, &users.GetByIDRequest{UserID: userUUID.String()}) 42 | if err != nil { 43 | return nil, errors.Join(err, errors.New("grpc client responded with error")) 44 | } 45 | 46 | res, err := fromProto(user.GetUser()) 47 | if err != nil { 48 | return nil, errors.Join(err, errors.New("transforming grpc client response")) 49 | } 50 | 51 | return res, nil 52 | } 53 | 54 | func (s *ServiceImpl) GetSessionByID(ctx context.Context, sessionID string) (*Session, error) { 55 | span, ctx := opentracing.StartSpanFromContext(ctx, "gateway_users_service.GetSessionByID") 56 | defer span.Finish() 57 | 58 | event := events.NewRequireSessionsGRPCClient(ctx) 59 | bus.Pub(event) 60 | event.WaitReply() 61 | if event.Err != nil { 62 | return nil, event.Err 63 | } 64 | 65 | defer event.Conn.Close() 66 | 67 | sessionByID, err := event.Client.GetSessionByID(ctx, &sessions.GetSessionByIDRequest{SessionID: sessionID}) 68 | if err != nil { 69 | return nil, errors.Join(err, errors.New("grpc client responded with error")) 70 | } 71 | 72 | sess := &Session{} 73 | sess, err = sess.FromProto(sessionByID.GetSession()) 74 | if err != nil { 75 | return nil, errors.Join(err, errors.New("transforming grpc client response")) 76 | } 77 | 78 | return sess, nil 79 | } 80 | -------------------------------------------------------------------------------- /app/hotels/application.go: -------------------------------------------------------------------------------- 1 | package hotels 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/go-playground/validator/v10" 14 | "github.com/go-redis/redis/v8" 15 | "github.com/jackc/pgx/v4/pgxpool" 16 | "github.com/labstack/echo/v4" 17 | "github.com/opentracing/opentracing-go" 18 | "github.com/prometheus/client_golang/prometheus/promhttp" 19 | "google.golang.org/grpc" 20 | "google.golang.org/grpc/keepalive" 21 | "google.golang.org/grpc/reflection" 22 | 23 | grpcRecovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" 24 | grpcCtxTags "github.com/grpc-ecosystem/go-grpc-middleware/tags" 25 | grpcOpenTracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing" 26 | grpcPrometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 27 | 28 | "github.com/badu/microservices-demo/pkg/config" 29 | "github.com/badu/microservices-demo/pkg/logger" 30 | ) 31 | 32 | type Application struct { 33 | echo *echo.Echo 34 | logger logger.Logger 35 | cfg *config.Config 36 | redisConn *redis.Client 37 | pgxPool *pgxpool.Pool 38 | tracer opentracing.Tracer 39 | } 40 | 41 | func NewApplication(logger logger.Logger, cfg *config.Config, redisConn *redis.Client, pgxPool *pgxpool.Pool, tracer opentracing.Tracer) Application { 42 | return Application{logger: logger, cfg: cfg, redisConn: redisConn, pgxPool: pgxPool, echo: echo.New(), tracer: tracer} 43 | } 44 | 45 | func (s *Application) Run() error { 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | defer cancel() 48 | 49 | publisher, err := NewHotelsPublisher(s.cfg, s.logger) 50 | if err != nil { 51 | return errors.Join(err, errors.New("application trying to create the hotels publisher")) 52 | } 53 | 54 | validate := validator.New() 55 | repository := NewRepository(s.pgxPool) 56 | service := NewService(&repository, s.logger, &publisher) 57 | 58 | listener, err := net.Listen("tcp", s.cfg.GRPCServer.Port) 59 | if err != nil { 60 | return errors.Join(err, fmt.Errorf("application trying to listen grpc port %s", s.cfg.GRPCServer.Port)) 61 | } 62 | defer listener.Close() 63 | 64 | router := echo.New() 65 | router.GET("/metrics", echo.WrapHandler(promhttp.Handler())) 66 | 67 | consumer := NewHotelsConsumer(s.logger, s.cfg, &service) 68 | if err := consumer.Initialize(); err != nil { 69 | return errors.Join(err, errors.New("application trying to create the hotels consumer")) 70 | } 71 | consumer.RunConsumers(ctx, cancel) 72 | defer consumer.CloseChannels() 73 | 74 | go func() { 75 | if err := router.Start(s.cfg.Metrics.URL); err != nil { 76 | s.logger.Errorf("router.Start metrics: %v", err) 77 | cancel() 78 | } 79 | s.logger.Infof("Metrics available on: %v", s.cfg.Metrics.URL) 80 | }() 81 | 82 | grpcServer := grpc.NewServer( 83 | grpc.KeepaliveParams( 84 | keepalive.ServerParameters{ 85 | MaxConnectionIdle: s.cfg.GRPCServer.MaxConnectionIdle * time.Minute, 86 | Timeout: s.cfg.GRPCServer.Timeout * time.Second, 87 | MaxConnectionAge: s.cfg.GRPCServer.MaxConnectionAge * time.Minute, 88 | Time: s.cfg.GRPCServer.Timeout * time.Minute, 89 | }, 90 | ), 91 | grpc.ChainUnaryInterceptor( 92 | grpcCtxTags.UnaryServerInterceptor(), 93 | grpcOpenTracing.UnaryServerInterceptor(), 94 | grpcPrometheus.UnaryServerInterceptor, 95 | grpcRecovery.UnaryServerInterceptor(), 96 | ), 97 | ) 98 | 99 | server := NewServer(&service, s.logger, validate) 100 | RegisterHotelsServiceServer(grpcServer, &server) 101 | grpcPrometheus.Register(grpcServer) 102 | 103 | go func() { 104 | s.logger.Infof("GRPC Application is listening on port: %v", s.cfg.GRPCServer.Port) 105 | s.logger.Fatal(grpcServer.Serve(listener)) 106 | }() 107 | 108 | if s.cfg.ProductionMode() { 109 | reflection.Register(grpcServer) 110 | } 111 | 112 | quit := make(chan os.Signal, 1) 113 | signal.Notify(quit, os.Interrupt, syscall.SIGTERM) 114 | 115 | select { 116 | case v := <-quit: 117 | s.logger.Errorf("signal.Notify: %v", v) 118 | case done := <-ctx.Done(): 119 | s.logger.Errorf("ctx.Done: %v", done) 120 | } 121 | 122 | s.logger.Info("Application Exited Properly") 123 | 124 | if err := s.echo.Server.Shutdown(ctx); err != nil { 125 | return errors.Join(err, errors.New("on server shutdown")) 126 | } 127 | 128 | grpcServer.GracefulStop() 129 | s.logger.Info("Application Exited Properly") 130 | 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /app/hotels/domain.go: -------------------------------------------------------------------------------- 1 | package hotels 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | "google.golang.org/protobuf/types/known/timestamppb" 8 | ) 9 | 10 | // HotelDO model 11 | type HotelDO struct { 12 | Image *string `json:"image,omitempty"` 13 | UpdatedAt *time.Time `json:"updated_at"` 14 | CreatedAt *time.Time `json:"created_at"` 15 | Longitude *float64 `json:"longitude,omitempty"` 16 | Latitude *float64 `json:"latitude,omitempty"` 17 | Location string `json:"location" validate:"required,min=10,max=250"` 18 | Description string `json:"description,omitempty" validate:"required,min=10,max=250"` 19 | City string `json:"city,omitempty" validate:"required,min=3,max=25"` 20 | Country string `json:"country,omitempty" validate:"required,min=3,max=25"` 21 | Email string `json:"email,omitempty" validate:"required,email"` 22 | Name string `json:"name" validate:"required,min=3,max=25"` 23 | Photos []string `json:"photos,omitempty"` 24 | Rating float64 `json:"rating" validate:"required,min=0,max=10"` 25 | CommentsCount int `json:"comments_count,omitempty"` 26 | HotelID uuid.UUID `json:"hotel_id"` 27 | } 28 | 29 | func (h *HotelDO) GetImage() string { 30 | var image string 31 | if h.Image != nil { 32 | image = *h.Image 33 | } 34 | return image 35 | } 36 | 37 | func (h *HotelDO) GetLatitude() float64 { 38 | var lat float64 39 | if h.Latitude != nil { 40 | lat = *h.Latitude 41 | } 42 | return lat 43 | } 44 | 45 | func (h *HotelDO) GetLongitude() float64 { 46 | var lon float64 47 | if h.Longitude != nil { 48 | lon = *h.Longitude 49 | } 50 | return lon 51 | } 52 | 53 | func (h *HotelDO) ToProto() *Hotel { 54 | return &Hotel{ 55 | HotelID: h.HotelID.String(), 56 | Name: h.Name, 57 | Email: h.Email, 58 | Country: h.Country, 59 | City: h.City, 60 | Description: h.Description, 61 | Image: h.GetImage(), 62 | Photos: h.Photos, 63 | CommentsCount: int64(h.CommentsCount), 64 | Latitude: h.GetLatitude(), 65 | Longitude: h.GetLongitude(), 66 | Location: h.Location, 67 | CreatedAt: timestamppb.New(*h.CreatedAt), 68 | UpdatedAt: timestamppb.New(*h.UpdatedAt), 69 | } 70 | } 71 | 72 | // All Hotels response with pagination 73 | type List struct { 74 | Hotels []*HotelDO `json:"comments"` 75 | TotalCount int `json:"totalCount"` 76 | TotalPages int `json:"totalPages"` 77 | Page int `json:"page"` 78 | Size int `json:"size"` 79 | HasMore bool `json:"hasMore"` 80 | } 81 | 82 | func (h *List) ToProto() []*Hotel { 83 | hotelsList := make([]*Hotel, 0, len(h.Hotels)) 84 | for _, hotel := range h.Hotels { 85 | hotelsList = append(hotelsList, hotel.ToProto()) 86 | } 87 | return hotelsList 88 | } 89 | 90 | type UpdateHotelImageMsg struct { 91 | Image string `json:"image,omitempty"` 92 | HotelID uuid.UUID `json:"hotel_id"` 93 | } 94 | 95 | // UpdateHotelImageMsg 96 | type UploadHotelImageMsg struct { 97 | ContentType string `json:"content_type"` 98 | Data []byte `json:"date"` 99 | HotelID uuid.UUID `json:"hotel_id"` 100 | } 101 | -------------------------------------------------------------------------------- /app/hotels/hotels.proto: -------------------------------------------------------------------------------- 1 | // protoc --go_out=plugins=grpc:. *.proto 2 | 3 | syntax = "proto3"; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | package hotels; 8 | 9 | option go_package = "github.com/badu/microservices-demo/app/hotels;hotels"; 10 | 11 | message Hotel { 12 | google.protobuf.Timestamp CreatedAt = 14; 13 | google.protobuf.Timestamp UpdatedAt = 15; 14 | string HotelID = 1; 15 | string Name = 2; 16 | string Email = 3; 17 | string Country = 4; 18 | string City = 5; 19 | string Description = 6; 20 | string Location = 7; 21 | string Image = 9; 22 | repeated string Photos = 10; 23 | int64 CommentsCount = 11; 24 | double Rating = 8; 25 | double Latitude = 12; 26 | double Longitude = 13; 27 | } 28 | 29 | message GetByIDRequest { 30 | string HotelID = 1; 31 | } 32 | 33 | message GetByIDResponse { 34 | Hotel Hotel = 1; 35 | } 36 | 37 | message GetHotelsRequest { 38 | int64 page = 1; 39 | int64 size = 2; 40 | } 41 | 42 | message GetHotelsResponse { 43 | int64 TotalCount = 1; 44 | int64 TotalPages = 2; 45 | int64 Page = 3; 46 | int64 Size = 4; 47 | bool HasMore = 5; 48 | repeated Hotel Hotels = 6; 49 | } 50 | 51 | message CreateHotelRequest { 52 | string Name = 1; 53 | string Email = 2; 54 | string Country = 3; 55 | string City = 4; 56 | string Description = 5; 57 | string Location = 6; 58 | string Image = 8; 59 | repeated string Photos = 9; 60 | int64 CommentsCount = 10; 61 | double Rating = 7; 62 | double Latitude = 11; 63 | double Longitude = 12; 64 | } 65 | 66 | message CreateHotelResponse { 67 | Hotel Hotel = 1; 68 | } 69 | 70 | message UpdateHotelRequest { 71 | string HotelID = 1; 72 | string Name = 2; 73 | string Email = 3; 74 | string Country = 4; 75 | string City = 5; 76 | string Description = 6; 77 | string Location = 7; 78 | string Image = 9; 79 | repeated string Photos = 10; 80 | int64 CommentsCount = 11; 81 | double Rating = 8; 82 | double Latitude = 12; 83 | double Longitude = 13; 84 | } 85 | 86 | message UpdateHotelResponse { 87 | Hotel Hotel = 1; 88 | } 89 | 90 | message UploadImageRequest { 91 | string HotelID = 1; 92 | string ContentType = 3; 93 | bytes Data = 2; 94 | } 95 | 96 | message UploadImageResponse { 97 | string HotelID = 1; 98 | } 99 | 100 | service HotelsService { 101 | rpc CreateHotel(CreateHotelRequest) returns (CreateHotelResponse) {} 102 | rpc UpdateHotel(UpdateHotelRequest) returns (UpdateHotelResponse) {} 103 | rpc GetHotelByID(GetByIDRequest) returns (GetByIDResponse) {} 104 | rpc GetHotels(GetHotelsRequest) returns (GetHotelsResponse) {} 105 | rpc UploadImage(UploadImageRequest) returns (UploadImageResponse) {} 106 | } 107 | -------------------------------------------------------------------------------- /app/hotels/rabbit_conf.go: -------------------------------------------------------------------------------- 1 | package hotels 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/streadway/amqp" 7 | ) 8 | 9 | const ( 10 | exchangeKind = "direct" 11 | exchangeDurable = true 12 | exchangeAutoDelete = false 13 | exchangeInternal = false 14 | exchangeNoWait = false 15 | 16 | queueDurable = true 17 | queueAutoDelete = false 18 | queueExclusive = false 19 | queueNoWait = false 20 | 21 | publishMandatory = false 22 | publishImmediate = false 23 | 24 | prefetchCount = 1 25 | prefetchSize = 0 26 | prefetchGlobal = false 27 | 28 | consumeAutoAck = false 29 | consumeExclusive = false 30 | consumeNoLocal = false 31 | consumeNoWait = false 32 | 33 | ExchangeName = "hotels" 34 | 35 | UpdateImageQueue = "update_hotel_image" 36 | UpdateImageBindingKey = "update_hotel_image_key" 37 | UpdateImageWorkers = 5 38 | UpdateImageConsumerTag = "update_hotel_image_consumer" 39 | ) 40 | 41 | func (c *ConsumerImpl) Initialize() error { 42 | if err := c.Dial(); err != nil { 43 | return errors.Join(err, errors.New("while dialing rabbitmq")) 44 | } 45 | 46 | updateImageChan, err := c.CreateExchangeAndQueue(ExchangeName, UpdateImageQueue, UpdateImageBindingKey) 47 | if err != nil { 48 | return errors.Join(err, errors.New("while creating rabbit mq exchange an queue")) 49 | } 50 | 51 | c.channels = append(c.channels, updateImageChan) 52 | 53 | return nil 54 | } 55 | 56 | // CloseChannels close active channels 57 | func (c *ConsumerImpl) CloseChannels() { 58 | for _, channel := range c.channels { 59 | go func(ch *amqp.Channel) { 60 | if err := ch.Close(); err != nil { 61 | c.logger.Errorf("CloseChannels ch.Close error: %v", err) 62 | } 63 | }(channel) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/hotels/rabbit_publisher.go: -------------------------------------------------------------------------------- 1 | package hotels 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/opentracing/opentracing-go" 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/prometheus/client_golang/prometheus/promauto" 11 | uuid "github.com/satori/go.uuid" 12 | "github.com/streadway/amqp" 13 | 14 | "github.com/badu/microservices-demo/pkg/config" 15 | "github.com/badu/microservices-demo/pkg/logger" 16 | "github.com/badu/microservices-demo/pkg/rabbitmq" 17 | ) 18 | 19 | var ( 20 | successPublisherMessages = promauto.NewCounter(prometheus.CounterOpts{ 21 | Name: "rabbitmq_hotels_success_publish_messages_total", 22 | Help: "The total number of success RabbitMQ published messages", 23 | }) 24 | errorPublisherMessages = promauto.NewCounter(prometheus.CounterOpts{ 25 | Name: "rabbitmq_hotels_error_publish_messages_total", 26 | Help: "The total number of error RabbitMQ published messages", 27 | }) 28 | ) 29 | 30 | type PublisherImpl struct { 31 | amqpConn *amqp.Connection 32 | cfg *config.Config 33 | logger logger.Logger 34 | } 35 | 36 | func NewHotelsPublisher(cfg *config.Config, logger logger.Logger) (PublisherImpl, error) { 37 | amqpConn, err := rabbitmq.NewRabbitMQConn(cfg.RabbitMQ) 38 | if err != nil { 39 | return PublisherImpl{}, err 40 | } 41 | return PublisherImpl{cfg: cfg, logger: logger, amqpConn: amqpConn}, nil 42 | } 43 | 44 | func (p *PublisherImpl) CreateExchangeAndQueue(exchange, queueName, bindingKey string) (*amqp.Channel, error) { 45 | amqpChan, err := p.amqpConn.Channel() 46 | if err != nil { 47 | return nil, errors.Join(err, errors.New("asking concurrent server channel")) 48 | } 49 | 50 | p.logger.Infof("Declaring exchange: %s", exchange) 51 | if err := amqpChan.ExchangeDeclare( 52 | exchange, 53 | exchangeKind, 54 | exchangeDurable, 55 | exchangeAutoDelete, 56 | exchangeInternal, 57 | exchangeNoWait, 58 | nil, 59 | ); err != nil { 60 | return nil, errors.Join(err, errors.New("while exchangeDeclare")) 61 | } 62 | 63 | queue, err := amqpChan.QueueDeclare( 64 | queueName, 65 | queueDurable, 66 | queueAutoDelete, 67 | queueExclusive, 68 | queueNoWait, 69 | nil, 70 | ) 71 | if err != nil { 72 | return nil, errors.Join(err, errors.New("while QueueDeclare")) 73 | } 74 | 75 | p.logger.Infof("Declared queue, binding it to exchange: Queue: %v, messageCount: %v, "+ 76 | "consumerCount: %v, exchange: %v, exchange: %v, bindingKey: %v", 77 | queue.Name, 78 | queue.Messages, 79 | queue.Consumers, 80 | exchange, 81 | bindingKey, 82 | ) 83 | 84 | err = amqpChan.QueueBind( 85 | queue.Name, 86 | bindingKey, 87 | exchange, 88 | queueNoWait, 89 | nil, 90 | ) 91 | if err != nil { 92 | return nil, errors.Join(err, errors.New("while QueueBind")) 93 | } 94 | 95 | return amqpChan, nil 96 | } 97 | 98 | func (p *PublisherImpl) Publish(ctx context.Context, exchange, routingKey, contentType string, headers amqp.Table, body []byte) error { 99 | span, ctx := opentracing.StartSpanFromContext(ctx, "rabbit_publisher_hotels.Publish") 100 | defer span.Finish() 101 | 102 | amqpChan, err := p.amqpConn.Channel() 103 | if err != nil { 104 | return errors.Join(err, errors.New("asking concurrent channel")) 105 | } 106 | defer amqpChan.Close() 107 | 108 | p.logger.Infof("Publishing message Exchange: %s, RoutingKey: %s", exchange, routingKey) 109 | 110 | if err := amqpChan.Publish( 111 | exchange, 112 | routingKey, 113 | publishMandatory, 114 | publishImmediate, 115 | amqp.Publishing{ 116 | Headers: headers, 117 | ContentType: contentType, 118 | DeliveryMode: amqp.Persistent, 119 | MessageId: uuid.NewV4().String(), 120 | Timestamp: time.Now().UTC(), 121 | Body: body, 122 | }, 123 | ); err != nil { 124 | errorPublisherMessages.Inc() 125 | return errors.Join(err, errors.New("while publishing")) 126 | } 127 | 128 | successPublisherMessages.Inc() 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /app/hotels/service.go: -------------------------------------------------------------------------------- 1 | package hotels 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | 8 | "github.com/opentracing/opentracing-go" 9 | uuid "github.com/satori/go.uuid" 10 | "github.com/streadway/amqp" 11 | 12 | "github.com/badu/microservices-demo/pkg/pagination" 13 | 14 | "github.com/badu/microservices-demo/pkg/logger" 15 | ) 16 | 17 | var ( 18 | ErrInvalidUUID = errors.New("invalid uuid") 19 | ErrInvalidDeliveryHeaders = errors.New("invalid delivery headers") 20 | ErrInternalServerError = errors.New("internal server error") 21 | ErrInvalidImageFormat = errors.New("invalid file format") 22 | ErrHotelNotFound = errors.New("hotel not found") 23 | ) 24 | 25 | const ( 26 | hotelIDHeader = "hotel_uuid" 27 | 28 | imagesExchange = "images" 29 | uploadHotelImageRoutingKey = "upload_hotel_image" 30 | ) 31 | 32 | type Repository interface { 33 | CreateHotel(ctx context.Context, hotel *HotelDO) (*HotelDO, error) 34 | UpdateHotel(ctx context.Context, hotel *HotelDO) (*HotelDO, error) 35 | UpdateHotelImage(ctx context.Context, hotelID uuid.UUID, imageURL string) error 36 | GetHotelByID(ctx context.Context, hotelID uuid.UUID) (*HotelDO, error) 37 | GetHotels(ctx context.Context, query *pagination.Pagination) (*List, error) 38 | } 39 | 40 | type Publisher interface { 41 | CreateExchangeAndQueue(exchange, queueName, bindingKey string) (*amqp.Channel, error) 42 | Publish(ctx context.Context, exchange, routingKey, contentType string, headers amqp.Table, body []byte) error 43 | } 44 | 45 | type ServiceImpl struct { 46 | repository Repository 47 | logger logger.Logger 48 | publisher Publisher 49 | } 50 | 51 | func NewService(repository Repository, logger logger.Logger, publisher Publisher) ServiceImpl { 52 | return ServiceImpl{repository: repository, logger: logger, publisher: publisher} 53 | } 54 | 55 | func (h *ServiceImpl) CreateHotel(ctx context.Context, hotel *HotelDO) (*HotelDO, error) { 56 | span, ctx := opentracing.StartSpanFromContext(ctx, "hotels_service.CreateHotel") 57 | defer span.Finish() 58 | 59 | return h.repository.CreateHotel(ctx, hotel) 60 | } 61 | 62 | func (h *ServiceImpl) UpdateHotel(ctx context.Context, hotel *HotelDO) (*HotelDO, error) { 63 | span, ctx := opentracing.StartSpanFromContext(ctx, "hotels_service.UpdateHotel") 64 | defer span.Finish() 65 | 66 | return h.repository.UpdateHotel(ctx, hotel) 67 | } 68 | 69 | func (h *ServiceImpl) GetHotelByID(ctx context.Context, hotelID uuid.UUID) (*HotelDO, error) { 70 | span, ctx := opentracing.StartSpanFromContext(ctx, "hotels_service.GetHotelByID") 71 | defer span.Finish() 72 | 73 | return h.repository.GetHotelByID(ctx, hotelID) 74 | } 75 | 76 | func (h *ServiceImpl) GetHotels(ctx context.Context, query *pagination.Pagination) (*List, error) { 77 | span, ctx := opentracing.StartSpanFromContext(ctx, "hotels_service.CreateHotel") 78 | defer span.Finish() 79 | 80 | return h.repository.GetHotels(ctx, query) 81 | } 82 | 83 | func (h *ServiceImpl) UploadImage(ctx context.Context, msg *UploadHotelImageMsg) error { 84 | span, ctx := opentracing.StartSpanFromContext(ctx, "hotels_service.UploadImage") 85 | defer span.Finish() 86 | 87 | headers := make(amqp.Table, 1) 88 | headers[hotelIDHeader] = msg.HotelID.String() 89 | if err := h.publisher.Publish( 90 | ctx, 91 | imagesExchange, 92 | uploadHotelImageRoutingKey, 93 | msg.ContentType, 94 | headers, 95 | msg.Data, 96 | ); err != nil { 97 | return errors.Join(err, errors.New("while publishing image")) 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (h *ServiceImpl) UpdateHotelImage(ctx context.Context, delivery amqp.Delivery) error { 104 | span, ctx := opentracing.StartSpanFromContext(ctx, "hotels_service.UpdateHotelImage") 105 | defer span.Finish() 106 | 107 | var msg UpdateHotelImageMsg 108 | if err := json.Unmarshal(delivery.Body, &msg); err != nil { 109 | return errors.Join(err, errors.New("while unmarshalling json")) 110 | } 111 | 112 | if err := h.repository.UpdateHotelImage(ctx, msg.HotelID, msg.Image); err != nil { 113 | return err 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (h *ServiceImpl) validateDeliveryHeaders(delivery amqp.Delivery) (*uuid.UUID, error) { 120 | h.logger.Infof("amqp.Delivery header: %-v", delivery.Headers) 121 | 122 | hotelUUID, ok := delivery.Headers[hotelIDHeader] 123 | if !ok { 124 | return nil, ErrInvalidDeliveryHeaders 125 | } 126 | hotelID, ok := hotelUUID.(string) 127 | if !ok { 128 | return nil, ErrInvalidUUID 129 | } 130 | 131 | parsedUUID, err := uuid.FromString(hotelID) 132 | if err != nil { 133 | return nil, errors.Join(err, errors.New("while reading uuid")) 134 | } 135 | 136 | return &parsedUUID, nil 137 | } 138 | -------------------------------------------------------------------------------- /app/images/aws_storage_client.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "log" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/credentials" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | "github.com/aws/aws-sdk-go/service/s3" 14 | "github.com/opentracing/opentracing-go" 15 | uuid "github.com/satori/go.uuid" 16 | 17 | "github.com/badu/microservices-demo/pkg/config" 18 | ) 19 | 20 | const ( 21 | imagesBucket = "images" 22 | ) 23 | 24 | // Init new AWS S3 session 25 | func NewS3Session(cfg *config.Config) *s3.S3 { 26 | // s3Config := &aws.Config{ 27 | // Credentials: credentials.NewStaticCredentials("minio", "minio", ""), 28 | // Endpoint: aws.String("http://localhost:9000"), 29 | // Region: aws.String("us-east-1"), 30 | // DisableSSL: aws.Bool(true), 31 | // S3ForcePathStyle: aws.Bool(true), 32 | // } 33 | // 34 | // newSession, err := session.NewSession(s3Config) 35 | // if err != nil { 36 | // log.Fatal(err) 37 | // } 38 | // 39 | // s3Client := s3Client.New(newSession) 40 | return s3.New(session.Must(session.NewSession(&aws.Config{ 41 | Credentials: credentials.NewStaticCredentials("minio123", "minio123", ""), 42 | Region: aws.String(cfg.AWS.S3Region), 43 | Endpoint: aws.String(cfg.AWS.S3EndPoint), 44 | DisableSSL: aws.Bool(cfg.AWS.DisableSSL), 45 | S3ForcePathStyle: aws.Bool(cfg.AWS.S3ForcePathStyle), 46 | }))) 47 | 48 | } 49 | 50 | type AWSS3Client struct { 51 | cfg *config.Config 52 | s3Client *s3.S3 53 | } 54 | 55 | func NewAWSStorage(cfg *config.Config, s3 *s3.S3) AWSS3Client { 56 | return AWSS3Client{cfg: cfg, s3Client: s3} 57 | } 58 | 59 | func (i *AWSS3Client) PutObject(ctx context.Context, data []byte, fileType string) (string, error) { 60 | span, ctx := opentracing.StartSpanFromContext(ctx, "aws_s3_client.PutObject") 61 | defer span.Finish() 62 | 63 | newFilename := uuid.NewV4().String() 64 | key := i.getFileKey(newFilename, fileType) 65 | 66 | object, err := i.s3Client.PutObjectWithContext(ctx, &s3.PutObjectInput{ 67 | Body: bytes.NewReader(data), 68 | Bucket: aws.String(imagesBucket), 69 | Key: aws.String(key), 70 | ACL: aws.String(s3.BucketCannedACLPublicRead), 71 | }) 72 | if err != nil { 73 | return "", errors.Join(err, errors.New("aws s3 client saving object to bucket")) 74 | } 75 | 76 | log.Printf("object : %-v", object) 77 | 78 | return i.getFilePublicURL(key), err 79 | } 80 | 81 | func (i *AWSS3Client) GetObject(ctx context.Context, key string) (*s3.GetObjectOutput, error) { 82 | span, ctx := opentracing.StartSpanFromContext(ctx, "aws_s3_client.GetObject") 83 | defer span.Finish() 84 | 85 | obj, err := i.s3Client.GetObjectWithContext(ctx, &s3.GetObjectInput{ 86 | Bucket: aws.String(imagesBucket), 87 | Key: aws.String(key), 88 | }) 89 | if err != nil { 90 | return nil, errors.Join(err, errors.New("aws s3 client getting object from bucket")) 91 | } 92 | 93 | return obj, nil 94 | } 95 | 96 | func (i *AWSS3Client) DeleteObject(ctx context.Context, key string) error { 97 | span, ctx := opentracing.StartSpanFromContext(ctx, "aws_s3_client.DeleteObject") 98 | defer span.Finish() 99 | 100 | _, err := i.s3Client.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{ 101 | Bucket: aws.String(imagesBucket), 102 | Key: aws.String(key), 103 | }) 104 | if err != nil { 105 | return errors.Join(err, errors.New("aws s3 client deleting object from bucket")) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func (i *AWSS3Client) getFileKey(fileID string, fileType string) string { 112 | return fmt.Sprintf("%s.%s", fileID, fileType) 113 | } 114 | 115 | func (i *AWSS3Client) getFilePublicURL(key string) string { 116 | return i.cfg.AWS.S3EndPointMinio + "/" + imagesBucket + "/" + key 117 | } 118 | -------------------------------------------------------------------------------- /app/images/domain.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | "google.golang.org/protobuf/types/known/timestamppb" 8 | ) 9 | 10 | // ImageDO model 11 | type ImageDO struct { 12 | CreatedAt time.Time `json:"created_at"` 13 | UpdatedAt time.Time `json:"updated_at"` 14 | ImageURL string `json:"image_url"` 15 | ImageID uuid.UUID `json:"image_id"` 16 | IsUploaded bool `json:"is_uploaded"` 17 | } 18 | 19 | // Event message for upload image 20 | type UploadImageMsg struct { 21 | ImageURL string `json:"image_url"` 22 | ImageID uuid.UUID `json:"image_id"` 23 | UserID uuid.UUID `json:"user_id"` 24 | IsUploaded bool `json:"is_uploaded"` 25 | } 26 | 27 | // Event message for create image 28 | type CreateImageMsg struct { 29 | ImageURL string `json:"image_url"` 30 | IsUploaded bool `json:"is_uploaded"` 31 | } 32 | 33 | func (i *ImageDO) ToProto() *Image { 34 | return &Image{ 35 | ImageID: i.ImageID.String(), 36 | ImageURL: i.ImageURL, 37 | IsUploaded: i.IsUploaded, 38 | CreatedAt: timestamppb.New(i.CreatedAt), 39 | } 40 | } 41 | 42 | type UpdateHotelImageMsg struct { 43 | Image string `json:"image,omitempty"` 44 | HotelID uuid.UUID `json:"hotel_id"` 45 | } 46 | 47 | // Event message for uploaded images 48 | type UploadedImageMsg struct { 49 | CreatedAt time.Time `json:"created_at"` 50 | ImageURL string `json:"image_url"` 51 | ImageID uuid.UUID `json:"image_id"` 52 | UserID uuid.UUID `json:"user_id"` 53 | IsUploaded bool `json:"is_uploaded"` 54 | } 55 | 56 | // Event message for upload user avatar 57 | type UpdateAvatarMsg struct { 58 | ContentType string `json:"content_type"` 59 | Body []byte 60 | UserID uuid.UUID `json:"user_id"` 61 | } 62 | -------------------------------------------------------------------------------- /app/images/grpc_server.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | uuid "github.com/satori/go.uuid" 8 | "github.com/streadway/amqp" 9 | "google.golang.org/grpc/status" 10 | 11 | "github.com/badu/microservices-demo/pkg/config" 12 | grpcErrors "github.com/badu/microservices-demo/pkg/grpc_errors" 13 | "github.com/badu/microservices-demo/pkg/logger" 14 | ) 15 | 16 | type Service interface { 17 | ResizeImage(ctx context.Context, delivery amqp.Delivery) error 18 | ProcessHotelImage(ctx context.Context, delivery amqp.Delivery) error 19 | Create(ctx context.Context, delivery amqp.Delivery) error 20 | GetImageByID(ctx context.Context, imageID uuid.UUID) (*ImageDO, error) 21 | } 22 | 23 | type ImageServer struct { 24 | cfg *config.Config 25 | logger logger.Logger 26 | service Service 27 | } 28 | 29 | func NewImageServer(cfg *config.Config, logger logger.Logger, service Service) ImageServer { 30 | return ImageServer{cfg: cfg, logger: logger, service: service} 31 | } 32 | 33 | func (i *ImageServer) GetImageByID(ctx context.Context, req *GetByIDRequest) (*GetByIDResponse, error) { 34 | span, ctx := opentracing.StartSpanFromContext(ctx, "images_grpc_server.GetImageByID") 35 | defer span.Finish() 36 | 37 | imageUUID, err := uuid.FromString(req.GetImageID()) 38 | if err != nil { 39 | i.logger.Errorf("uuid.FromString: %v", err) 40 | return nil, status.Errorf(grpcErrors.ParseGRPCErrStatusCode(err), "uuid.FromString: %v", err) 41 | } 42 | 43 | imageByID, err := i.service.GetImageByID(ctx, imageUUID) 44 | if err != nil { 45 | i.logger.Errorf("uuid.FromString: %v", err) 46 | return nil, status.Errorf(grpcErrors.ParseGRPCErrStatusCode(err), "imageServer.GetImageByID: %v", err) 47 | } 48 | 49 | return &GetByIDResponse{Image: imageByID.ToProto()}, nil 50 | } 51 | -------------------------------------------------------------------------------- /app/images/image.proto: -------------------------------------------------------------------------------- 1 | // protoc --go_out=plugins=grpc:. *.proto 2 | syntax = "proto3"; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | 6 | package images; 7 | 8 | option go_package = "github.com/badu/microservices-demo/app/images;images"; 9 | 10 | message Image { 11 | google.protobuf.Timestamp CreatedAt = 4; 12 | string ImageID = 1; 13 | string ImageURL = 2; 14 | bool IsUploaded = 3; 15 | } 16 | 17 | message GetByIDResponse { 18 | Image Image = 1; 19 | } 20 | 21 | message GetByIDRequest { 22 | string ImageID = 1; 23 | } 24 | 25 | service ImageService { 26 | rpc GetImageByID(GetByIDRequest) returns (GetByIDResponse) {} 27 | } 28 | -------------------------------------------------------------------------------- /app/images/image_procecess.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/disintegration/gift" 7 | ) 8 | 9 | type ImgResizer struct { 10 | Gift *gift.GIFT 11 | Buffer *bytes.Buffer 12 | } 13 | 14 | func NewImgResizer(filters ...gift.Filter) *ImgResizer { 15 | g := gift.New( 16 | filters..., 17 | // gift.Resize(1024, 0, gift.LanczosResampling), 18 | // gift.Resize(width, height, resampling), 19 | // gift.Contrast(20), 20 | // gift.Brightness(7), 21 | // gift.Gamma(0.5), 22 | // gift.CropToSize(1024, 1024, gift.CenterAnchor), 23 | ) 24 | b := &bytes.Buffer{} 25 | return &ImgResizer{Gift: g, Buffer: b} 26 | } 27 | -------------------------------------------------------------------------------- /app/images/rabbit_conf.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/streadway/amqp" 7 | ) 8 | 9 | const ( 10 | exchangeKind = "direct" 11 | exchangeDurable = true 12 | exchangeAutoDelete = false 13 | exchangeInternal = false 14 | exchangeNoWait = false 15 | 16 | queueDurable = true 17 | queueAutoDelete = false 18 | queueExclusive = false 19 | queueNoWait = false 20 | 21 | publishMandatory = false 22 | publishImmediate = false 23 | 24 | prefetchCount = 1 25 | prefetchSize = 0 26 | prefetchGlobal = false 27 | 28 | consumeAutoAck = false 29 | consumeExclusive = false 30 | consumeNoLocal = false 31 | consumeNoWait = false 32 | 33 | ExchangeName = "images" 34 | 35 | ResizeQueueName = "resize_queue" 36 | ResizeConsumerTag = "resize_consumer" 37 | ResizeWorkers = 10 38 | ResizeBindingKey = "resize_image_key" 39 | 40 | CreateQueueName = "create_queue" 41 | CreateConsumerTag = "create_consumer" 42 | CreateWorkers = 5 43 | CreateBindingKey = "create_image_key" 44 | 45 | UploadHotelImageQueue = "upload_hotel_image_queue" 46 | UploadHotelImageConsumerTag = "upload_hotel_image_consumer_tag" 47 | UploadHotelImageWorkers = 10 48 | UploadHotelImageBindingKey = "upload_hotel_image_binding_key" 49 | ) 50 | 51 | // Initialize consumers 52 | func (c *ConsumerImpl) Initialize() error { 53 | if err := c.Dial(); err != nil { 54 | return errors.Join(err, errors.New("images.ConsumerDial")) 55 | } 56 | 57 | updateImageChan, err := c.CreateExchangeAndQueue(ExchangeName, UploadHotelImageQueue, UploadHotelImageBindingKey) 58 | if err != nil { 59 | return errors.Join(err, errors.New("images.CreateExchangeAndQueue")) 60 | } 61 | c.channels = append(c.channels, updateImageChan) 62 | 63 | resizeChan, err := c.CreateExchangeAndQueue(ExchangeName, ResizeQueueName, ResizeBindingKey) 64 | if err != nil { 65 | return errors.Join(err, errors.New("images.CreateExchangeAndQueue")) 66 | } 67 | c.channels = append(c.channels, resizeChan) 68 | 69 | createImgChan, err := c.CreateExchangeAndQueue(ExchangeName, CreateQueueName, CreateBindingKey) 70 | if err != nil { 71 | return errors.Join(err, errors.New("images.CreateExchangeAndQueue")) 72 | } 73 | c.channels = append(c.channels, createImgChan) 74 | 75 | return nil 76 | } 77 | 78 | // CloseChannels close active channels 79 | func (c *ConsumerImpl) CloseChannels() { 80 | for _, channel := range c.channels { 81 | go func(ch *amqp.Channel) { 82 | if err := ch.Close(); err != nil { 83 | c.logger.Errorf("CloseChannels ch.Close error: %v", err) 84 | } 85 | }(channel) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/images/rabbit_publisher.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/opentracing/opentracing-go" 9 | "github.com/prometheus/client_golang/prometheus" 10 | uuid "github.com/satori/go.uuid" 11 | "github.com/streadway/amqp" 12 | 13 | "github.com/badu/microservices-demo/pkg/config" 14 | "github.com/badu/microservices-demo/pkg/logger" 15 | "github.com/badu/microservices-demo/pkg/rabbitmq" 16 | ) 17 | 18 | type PublisherImpl struct { 19 | amqpConn *amqp.Connection 20 | cfg *config.Config 21 | logger logger.Logger 22 | incomingMessages prometheus.Counter 23 | successMessages prometheus.Counter 24 | errorMessages prometheus.Counter 25 | } 26 | 27 | func NewPublisher( 28 | cfg *config.Config, 29 | logger logger.Logger, 30 | incomingMessages prometheus.Counter, 31 | successMessages prometheus.Counter, 32 | errorMessages prometheus.Counter, 33 | ) (PublisherImpl, error) { 34 | amqpConn, err := rabbitmq.NewRabbitMQConn(cfg.RabbitMQ) 35 | if err != nil { 36 | return PublisherImpl{}, err 37 | } 38 | return PublisherImpl{ 39 | cfg: cfg, 40 | logger: logger, 41 | amqpConn: amqpConn, 42 | incomingMessages: incomingMessages, 43 | successMessages: successMessages, 44 | errorMessages: errorMessages, 45 | }, nil 46 | } 47 | 48 | func (p *PublisherImpl) CreateExchangeAndQueue(exchange, queueName, bindingKey string) (*amqp.Channel, error) { 49 | amqpChan, err := p.amqpConn.Channel() 50 | if err != nil { 51 | return nil, errors.Join(err, errors.New("p.amqpConn.Channel")) 52 | } 53 | 54 | p.logger.Infof("Declaring exchange: %s", exchange) 55 | if err := amqpChan.ExchangeDeclare( 56 | exchange, 57 | exchangeKind, 58 | exchangeDurable, 59 | exchangeAutoDelete, 60 | exchangeInternal, 61 | exchangeNoWait, 62 | nil, 63 | ); err != nil { 64 | return nil, errors.Join(err, errors.New("error ch.ExchangeDeclare")) 65 | } 66 | 67 | queue, err := amqpChan.QueueDeclare( 68 | queueName, 69 | queueDurable, 70 | queueAutoDelete, 71 | queueExclusive, 72 | queueNoWait, 73 | nil, 74 | ) 75 | if err != nil { 76 | return nil, errors.Join(err, errors.New("error ch.QueueDeclare")) 77 | } 78 | 79 | p.logger.Infof("Declared queue, binding it to exchange: Queue: %v, messageCount: %v, "+ 80 | "consumerCount: %v, exchange: %v, exchange: %v, bindingKey: %v", 81 | queue.Name, 82 | queue.Messages, 83 | queue.Consumers, 84 | exchange, 85 | bindingKey, 86 | ) 87 | 88 | err = amqpChan.QueueBind( 89 | queue.Name, 90 | bindingKey, 91 | exchange, 92 | queueNoWait, 93 | nil, 94 | ) 95 | if err != nil { 96 | return nil, errors.Join(err, errors.New("error ch.QueueBind")) 97 | } 98 | 99 | return amqpChan, nil 100 | } 101 | 102 | // Publish message 103 | func (p *PublisherImpl) Publish(ctx context.Context, exchange, routingKey, contentType string, headers amqp.Table, body []byte) error { 104 | span, ctx := opentracing.StartSpanFromContext(ctx, "rabbit_publisher_images.Publish") 105 | defer span.Finish() 106 | 107 | amqpChan, err := p.amqpConn.Channel() 108 | if err != nil { 109 | return errors.Join(err, errors.New("p.amqpConn.Channel")) 110 | } 111 | defer amqpChan.Close() 112 | 113 | p.logger.Infof("Publishing message Exchange: %s, RoutingKey: %s", exchange, routingKey) 114 | 115 | if err := amqpChan.Publish( 116 | exchange, 117 | routingKey, 118 | publishMandatory, 119 | publishImmediate, 120 | amqp.Publishing{ 121 | Headers: headers, 122 | ContentType: contentType, 123 | DeliveryMode: amqp.Persistent, 124 | MessageId: uuid.NewV4().String(), 125 | Timestamp: time.Now(), 126 | Body: body, 127 | }, 128 | ); err != nil { 129 | p.errorMessages.Inc() 130 | return errors.Join(err, errors.New("ch.Publish")) 131 | } 132 | 133 | p.successMessages.Inc() 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /app/images/repository.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/jackc/pgx/v4/pgxpool" 8 | "github.com/opentracing/opentracing-go" 9 | uuid "github.com/satori/go.uuid" 10 | ) 11 | 12 | const ( 13 | createImageQuery = `INSERT INTO images (image_url, is_uploaded) VALUES ($1, $2) RETURNING image_id, image_url, is_uploaded, created_at` 14 | 15 | getImageByIDQuery = `SELECT image_id, image_url, is_uploaded, created_at, updated_at FROM images WHERE image_id = $1` 16 | ) 17 | 18 | type RepositoryImpl struct { 19 | pgxPool *pgxpool.Pool 20 | } 21 | 22 | func NewRepository(pgxPool *pgxpool.Pool) RepositoryImpl { 23 | return RepositoryImpl{pgxPool: pgxPool} 24 | } 25 | 26 | func (r *RepositoryImpl) Create(ctx context.Context, msg *ImageDO) (*ImageDO, error) { 27 | span, ctx := opentracing.StartSpanFromContext(ctx, "images_repository.Create") 28 | defer span.Finish() 29 | 30 | var res ImageDO 31 | if err := r.pgxPool.QueryRow( 32 | ctx, 33 | createImageQuery, 34 | msg.ImageURL, 35 | msg.IsUploaded, 36 | ).Scan(&res.ImageID, &res.ImageURL, &res.IsUploaded, &res.CreatedAt); err != nil { 37 | return nil, errors.Join(err, errors.New("while scanning results")) 38 | } 39 | 40 | return &res, nil 41 | } 42 | func (r *RepositoryImpl) GetImageByID(ctx context.Context, imageID uuid.UUID) (*ImageDO, error) { 43 | span, ctx := opentracing.StartSpanFromContext(ctx, "images_repository.GetImageByID") 44 | defer span.Finish() 45 | 46 | var img ImageDO 47 | if err := r.pgxPool.QueryRow(ctx, getImageByIDQuery, imageID).Scan( 48 | &img.ImageID, 49 | &img.ImageURL, 50 | &img.IsUploaded, 51 | &img.CreatedAt, 52 | &img.UpdatedAt, 53 | ); err != nil { 54 | return nil, errors.Join(err, errors.New("while scanning results")) 55 | } 56 | 57 | return &img, nil 58 | } 59 | -------------------------------------------------------------------------------- /app/sessions/application.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/go-redis/redis/v8" 12 | "github.com/labstack/echo/v4" 13 | "github.com/opentracing/opentracing-go" 14 | "github.com/prometheus/client_golang/prometheus/promhttp" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/keepalive" 17 | "google.golang.org/grpc/reflection" 18 | 19 | "github.com/badu/microservices-demo/pkg/grpc_client" 20 | 21 | grpcRecovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" 22 | grpcCtxTags "github.com/grpc-ecosystem/go-grpc-middleware/tags" 23 | grpcPrometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 24 | traceUtils "github.com/opentracing-contrib/go-grpc" 25 | 26 | "github.com/badu/microservices-demo/pkg/config" 27 | "github.com/badu/microservices-demo/pkg/logger" 28 | ) 29 | 30 | type Application struct { 31 | logger logger.Logger 32 | cfg *config.Config 33 | redisConn *redis.Client 34 | tracer opentracing.Tracer 35 | } 36 | 37 | func NewApplication(logger logger.Logger, cfg *config.Config, redisConn *redis.Client, tracer opentracing.Tracer) Application { 38 | return Application{logger: logger, cfg: cfg, redisConn: redisConn, tracer: tracer} 39 | } 40 | 41 | func (s *Application) Run() error { 42 | ctx, cancel := context.WithCancel(context.Background()) 43 | 44 | repository := NewRepository(s.redisConn, s.cfg.GRPCServer.SessionPrefix, time.Duration(s.cfg.GRPCServer.SessionExpire)*time.Minute) 45 | service := NewService(&repository) 46 | csrfRepository := NewCSRFRepository(s.redisConn, s.cfg.GRPCServer.CSRFPrefix, time.Duration(s.cfg.GRPCServer.CsrfExpire)*time.Minute) 47 | csrfService := NewCSRFService(s.cfg.GRPCServer.CsrfSalt, &csrfRepository) 48 | 49 | router := echo.New() 50 | router.GET("/metrics", echo.WrapHandler(promhttp.Handler())) 51 | 52 | go func() { 53 | if err := router.Start(s.cfg.Metrics.Port); err != nil { 54 | s.logger.Errorf("router.Start metrics: %v", err) 55 | cancel() 56 | } 57 | s.logger.Infof("Prometheus Metrics available on: %v", s.cfg.Metrics.Port) 58 | }() 59 | 60 | l, err := net.Listen("tcp", s.cfg.GRPCServer.Port) 61 | if err != nil { 62 | return err 63 | } 64 | defer l.Close() 65 | 66 | im := grpc_client.NewClientMiddleware(s.logger, s.cfg, s.tracer) 67 | grpcServer := grpc.NewServer( 68 | grpc.KeepaliveParams( 69 | keepalive.ServerParameters{ 70 | MaxConnectionIdle: s.cfg.GRPCServer.MaxConnectionIdle * time.Minute, 71 | Timeout: s.cfg.GRPCServer.Timeout * time.Second, 72 | MaxConnectionAge: s.cfg.GRPCServer.MaxConnectionAge * time.Minute, 73 | Time: s.cfg.GRPCServer.Timeout * time.Minute, 74 | }, 75 | ), 76 | grpc.ChainUnaryInterceptor( 77 | grpcCtxTags.UnaryServerInterceptor(), 78 | grpcPrometheus.UnaryServerInterceptor, 79 | grpcRecovery.UnaryServerInterceptor(), 80 | traceUtils.OpenTracingServerInterceptor(s.tracer), 81 | im.Logger, 82 | ), 83 | ) 84 | 85 | server := NewServer(s.logger, &service, &csrfService) 86 | RegisterAuthorizationServiceServer(grpcServer, &server) 87 | grpcPrometheus.Register(grpcServer) 88 | 89 | go func() { 90 | s.logger.Infof("GRPC Server is listening on port: %v", s.cfg.GRPCServer.Port) 91 | s.logger.Fatal(grpcServer.Serve(l)) 92 | }() 93 | 94 | if s.cfg.ProductionMode() { 95 | reflection.Register(grpcServer) 96 | } 97 | 98 | quit := make(chan os.Signal, 1) 99 | signal.Notify(quit, os.Interrupt, syscall.SIGTERM) 100 | 101 | select { 102 | case v := <-quit: 103 | s.logger.Errorf("got signal.Notify: %v", v) 104 | case done := <-ctx.Done(): 105 | s.logger.Errorf("got ctx.Done: %v", done) 106 | } 107 | 108 | if err := router.Shutdown(ctx); err != nil { 109 | s.logger.Errorf("server shutdown error: %v", err) 110 | } 111 | 112 | grpcServer.GracefulStop() 113 | s.logger.Info("sessions server exited properly") 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /app/sessions/csrf_repository.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/go-redis/redis/v8" 10 | "github.com/opentracing/opentracing-go" 11 | ) 12 | 13 | type CsrfRepositoryImpl struct { 14 | redis *redis.Client 15 | prefix string 16 | duration time.Duration 17 | } 18 | 19 | func NewCSRFRepository(redis *redis.Client, prefix string, duration time.Duration) CsrfRepositoryImpl { 20 | return CsrfRepositoryImpl{redis: redis, prefix: prefix, duration: duration} 21 | } 22 | 23 | // Create csrf token 24 | func (r *CsrfRepositoryImpl) Create(ctx context.Context, token string) error { 25 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_repository.Create") 26 | defer span.Finish() 27 | 28 | if err := r.redis.SetEX(ctx, r.createKey(token), token, r.duration).Err(); err != nil { 29 | return errors.Join(err, errors.New("CsrfRepositoryImpl.Create.redis.SetEX")) 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // Check csrf token 36 | func (r *CsrfRepositoryImpl) GetToken(ctx context.Context, token string) (string, error) { 37 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_repository.Check") 38 | defer span.Finish() 39 | 40 | token, err := r.redis.Get(ctx, r.createKey(token)).Result() 41 | if err != nil { 42 | return "", err 43 | } 44 | 45 | return token, nil 46 | } 47 | 48 | func (r *CsrfRepositoryImpl) createKey(token string) string { 49 | return fmt.Sprintf("%s: %s", r.prefix, token) 50 | } 51 | -------------------------------------------------------------------------------- /app/sessions/csrf_service.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "errors" 8 | "io" 9 | 10 | "github.com/opentracing/opentracing-go" 11 | ) 12 | 13 | type CSRFService interface { 14 | GetCSRFToken(ctx context.Context, sesID string) (string, error) 15 | ValidateCSRFToken(ctx context.Context, sesID string, token string) (bool, error) 16 | } 17 | 18 | type CSRFRepository interface { 19 | Create(ctx context.Context, token string) error 20 | GetToken(ctx context.Context, token string) (string, error) 21 | } 22 | 23 | type CsrfServiceImpl struct { 24 | repository CSRFRepository 25 | salt string 26 | } 27 | 28 | func NewCSRFService(salt string, csrfRepo CSRFRepository) CsrfServiceImpl { 29 | return CsrfServiceImpl{repository: csrfRepo, salt: salt} 30 | } 31 | 32 | func (c *CsrfServiceImpl) GetCSRFToken(ctx context.Context, sesID string) (string, error) { 33 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_service.CreateToken") 34 | defer span.Finish() 35 | 36 | token, err := c.makeToken(sesID) 37 | if err != nil { 38 | return "", errors.Join(err, errors.New("CsrfServiceImpl.CreateToken.c.makeToken")) 39 | } 40 | 41 | if err := c.repository.Create(ctx, token); err != nil { 42 | return "", errors.Join(err, errors.New("CsrfServiceImpl.CreateToken.repository.Create")) 43 | } 44 | 45 | return token, nil 46 | } 47 | 48 | func (c *CsrfServiceImpl) ValidateCSRFToken(ctx context.Context, sesID string, token string) (bool, error) { 49 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_service.CheckToken") 50 | defer span.Finish() 51 | 52 | existsToken, err := c.repository.GetToken(ctx, token) 53 | if err != nil { 54 | return false, err 55 | } 56 | 57 | return c.validateToken(existsToken, sesID), nil 58 | } 59 | 60 | func (c *CsrfServiceImpl) makeToken(sessionID string) (string, error) { 61 | 62 | hash := sha256.New() 63 | _, err := io.WriteString(hash, c.salt+sessionID) 64 | if err != nil { 65 | return "", err 66 | } 67 | 68 | token := base64.RawStdEncoding.EncodeToString(hash.Sum(nil)) 69 | return token, nil 70 | } 71 | 72 | func (c *CsrfServiceImpl) validateToken(token string, sessionID string) bool { 73 | trueToken, err := c.makeToken(sessionID) 74 | if err != nil { 75 | return false 76 | } 77 | 78 | return token == trueToken 79 | } 80 | -------------------------------------------------------------------------------- /app/sessions/domain.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | // SessionDO model 6 | type SessionDO struct { 7 | SessionID string `json:"session_id"` 8 | UserID uuid.UUID `json:"user_id"` 9 | } 10 | 11 | func (s *SessionDO) FromProto(session *Session) (*SessionDO, error) { 12 | userUUID, err := uuid.FromString(session.GetUserID()) 13 | if err != nil { 14 | return nil, err 15 | } 16 | s.UserID = userUUID 17 | s.SessionID = session.GetSessionID() 18 | return s, nil 19 | } 20 | -------------------------------------------------------------------------------- /app/sessions/grpc_server.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | uuid "github.com/satori/go.uuid" 8 | "google.golang.org/grpc/status" 9 | 10 | grpcErrors "github.com/badu/microservices-demo/pkg/grpc_errors" 11 | "github.com/badu/microservices-demo/pkg/logger" 12 | ) 13 | 14 | type Service interface { 15 | CreateSession(ctx context.Context, userID uuid.UUID) (*SessionDO, error) 16 | GetSessionByID(ctx context.Context, sessID string) (*SessionDO, error) 17 | DeleteSession(ctx context.Context, sessID string) error 18 | } 19 | 20 | type ServerImpl struct { 21 | logger logger.Logger 22 | service Service 23 | csrfService CSRFService 24 | } 25 | 26 | func NewServer(logger logger.Logger, service Service, csrfService CSRFService) ServerImpl { 27 | return ServerImpl{logger: logger, service: service, csrfService: csrfService} 28 | } 29 | 30 | func (s *ServerImpl) CreateSession(ctx context.Context, r *CreateSessionRequest) (*CreateSessionResponse, error) { 31 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_grpc_server.CreateSession") 32 | defer span.Finish() 33 | 34 | userUUID, err := uuid.FromString(r.UserID) 35 | if err != nil { 36 | s.logger.Errorf("uuid.FromString: %v", err) 37 | return nil, status.Errorf(grpcErrors.ParseGRPCErrStatusCode(err), "uuid.FromString: %v", err) 38 | } 39 | sess, err := s.service.CreateSession(ctx, userUUID) 40 | if err != nil { 41 | s.logger.Errorf("service.CreateSession: %v", err) 42 | return nil, status.Errorf(grpcErrors.ParseGRPCErrStatusCode(err), "service.CreateSession: %v", err) 43 | } 44 | 45 | return &CreateSessionResponse{Session: s.sessionJSONToProto(sess)}, nil 46 | } 47 | 48 | func (s *ServerImpl) GetSessionByID(ctx context.Context, r *GetSessionByIDRequest) (*GetSessionByIDResponse, error) { 49 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_grpc_server.GetSessionByID") 50 | defer span.Finish() 51 | 52 | sess, err := s.service.GetSessionByID(ctx, r.SessionID) 53 | if err != nil { 54 | s.logger.Errorf("service.GetSessionByID: %v", err) 55 | return nil, status.Errorf(grpcErrors.ParseGRPCErrStatusCode(err), "service.GetSessionByID: %v", err) 56 | } 57 | 58 | return &GetSessionByIDResponse{Session: s.sessionJSONToProto(sess)}, nil 59 | } 60 | 61 | func (s *ServerImpl) DeleteSession(ctx context.Context, r *DeleteSessionRequest) (*DeleteSessionResponse, error) { 62 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_grpc_server.DeleteSession") 63 | defer span.Finish() 64 | 65 | if err := s.service.DeleteSession(ctx, r.SessionID); err != nil { 66 | return nil, status.Errorf(grpcErrors.ParseGRPCErrStatusCode(err), "service.DeleteSession: %v", err) 67 | } 68 | 69 | return &DeleteSessionResponse{SessionID: r.SessionID}, nil 70 | } 71 | 72 | func (s *ServerImpl) CreateCsrfToken(ctx context.Context, r *CreateCsrfTokenRequest) (*CreateCsrfTokenResponse, error) { 73 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_grpc_server.CreateCsrfToken") 74 | defer span.Finish() 75 | 76 | token, err := s.csrfService.GetCSRFToken(ctx, r.GetCsrfTokenInput().GetSessionID()) 77 | if err != nil { 78 | return nil, status.Errorf(grpcErrors.ParseGRPCErrStatusCode(err), "csrfService.CreateCsrfToken: %v", err) 79 | } 80 | 81 | return &CreateCsrfTokenResponse{CsrfToken: &CsrfToken{Token: token}}, nil 82 | } 83 | 84 | func (s *ServerImpl) CheckCsrfToken(ctx context.Context, r *CheckCsrfTokenRequest) (*CheckCsrfTokenResponse, error) { 85 | span, ctx := opentracing.StartSpanFromContext(ctx, "csrf_grpc_server.CheckCsrfToken") 86 | defer span.Finish() 87 | 88 | isValid, err := s.csrfService.ValidateCSRFToken(ctx, r.GetCsrfTokenCheck().GetSessionID(), r.GetCsrfTokenCheck().GetToken()) 89 | if err != nil { 90 | return nil, status.Errorf(grpcErrors.ParseGRPCErrStatusCode(err), "csrfService.CheckToken: %v", err) 91 | } 92 | 93 | return &CheckCsrfTokenResponse{CheckResult: &CheckResult{Result: isValid}}, nil 94 | } 95 | 96 | func (s *ServerImpl) sessionJSONToProto(sess *SessionDO) *Session { 97 | return &Session{ 98 | UserID: sess.UserID.String(), 99 | SessionID: sess.SessionID, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/sessions/repository.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "github.com/opentracing/opentracing-go" 12 | uuid "github.com/satori/go.uuid" 13 | ) 14 | 15 | type RepositoryImpl struct { 16 | redis *redis.Client 17 | prefix string 18 | expiration time.Duration 19 | } 20 | 21 | func NewRepository(redis *redis.Client, prefix string, expiration time.Duration) RepositoryImpl { 22 | return RepositoryImpl{redis: redis, prefix: prefix, expiration: expiration} 23 | } 24 | 25 | func (s *RepositoryImpl) CreateSession(ctx context.Context, userID uuid.UUID) (*SessionDO, error) { 26 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessions_repository.CreateSession") 27 | defer span.Finish() 28 | 29 | sess := &SessionDO{ 30 | SessionID: uuid.NewV4().String(), 31 | UserID: userID, 32 | } 33 | 34 | sessBytes, err := json.Marshal(&sess) 35 | if err != nil { 36 | return nil, errors.Join(err, errors.New("while marshalling json")) 37 | } 38 | 39 | if err := s.redis.SetEX(ctx, s.createKey(sess.SessionID), string(sessBytes), s.expiration).Err(); err != nil { 40 | return nil, errors.Join(err, errors.New("while saving to redis")) 41 | } 42 | 43 | return sess, nil 44 | } 45 | 46 | func (s *RepositoryImpl) GetSessionByID(ctx context.Context, sessID string) (*SessionDO, error) { 47 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessions_repository.GetSessionByID") 48 | defer span.Finish() 49 | 50 | result, err := s.redis.Get(ctx, s.createKey(sessID)).Result() 51 | if err != nil { 52 | return nil, errors.Join(err, errors.New("while reading from redis")) 53 | } 54 | 55 | var sess SessionDO 56 | if err := json.Unmarshal([]byte(result), &sess); err != nil { 57 | return nil, errors.Join(err, errors.New("while unmarshalling json")) 58 | } 59 | return &sess, nil 60 | } 61 | 62 | func (s *RepositoryImpl) DeleteSession(ctx context.Context, sessID string) error { 63 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessions_repository.DeleteSession") 64 | defer span.Finish() 65 | 66 | if err := s.redis.Del(ctx, s.createKey(sessID)).Err(); err != nil { 67 | return errors.Join(err, errors.New("while deleting from redis")) 68 | } 69 | return nil 70 | } 71 | 72 | func (s *RepositoryImpl) createKey(sessionID string) string { 73 | return fmt.Sprintf("%s: %s", s.prefix, sessionID) 74 | } 75 | -------------------------------------------------------------------------------- /app/sessions/service.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | uuid "github.com/satori/go.uuid" 8 | ) 9 | 10 | type Repository interface { 11 | CreateSession(ctx context.Context, userID uuid.UUID) (*SessionDO, error) 12 | GetSessionByID(ctx context.Context, sessID string) (*SessionDO, error) 13 | DeleteSession(ctx context.Context, sessID string) error 14 | } 15 | 16 | type ServiceImpl struct { 17 | repo Repository 18 | } 19 | 20 | func NewService(sessRepo Repository) ServiceImpl { 21 | return ServiceImpl{repo: sessRepo} 22 | } 23 | 24 | func (s *ServiceImpl) CreateSession(ctx context.Context, userID uuid.UUID) (*SessionDO, error) { 25 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessions_service.CreateSession") 26 | defer span.Finish() 27 | return s.repo.CreateSession(ctx, userID) 28 | } 29 | 30 | func (s *ServiceImpl) GetSessionByID(ctx context.Context, sessID string) (*SessionDO, error) { 31 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessions_service.GetSessionByID") 32 | defer span.Finish() 33 | return s.repo.GetSessionByID(ctx, sessID) 34 | } 35 | 36 | func (s *ServiceImpl) DeleteSession(ctx context.Context, sessID string) error { 37 | span, ctx := opentracing.StartSpanFromContext(ctx, "sessions_service.GetSessionByID") 38 | defer span.Finish() 39 | return s.repo.DeleteSession(ctx, sessID) 40 | } 41 | -------------------------------------------------------------------------------- /app/sessions/session.proto: -------------------------------------------------------------------------------- 1 | //protoc --go_out=plugins=grpc:. *.proto 2 | 3 | syntax = "proto3"; 4 | 5 | package sessions; 6 | 7 | option go_package = "github.com/badu/microservices-demo/app/sessions;sessions"; 8 | 9 | message Session { 10 | string UserID = 1; 11 | string SessionID = 2; 12 | } 13 | 14 | message CsrfTokenInput { 15 | string SessionID = 1; 16 | } 17 | 18 | message CsrfToken { 19 | string Token = 1; 20 | } 21 | 22 | message CsrfTokenCheck { 23 | string SessionID = 1; 24 | string Token = 2; 25 | } 26 | 27 | message CheckResult { 28 | bool Result = 1; 29 | } 30 | 31 | message Empty {} 32 | 33 | message CreateSessionRequest { 34 | string UserID = 1; 35 | } 36 | 37 | message CreateSessionResponse { 38 | Session Session = 1; 39 | } 40 | 41 | message GetSessionByIDRequest { 42 | string SessionID = 1; 43 | } 44 | 45 | message GetSessionByIDResponse { 46 | Session Session = 1; 47 | } 48 | 49 | message DeleteSessionRequest { 50 | string SessionID = 1; 51 | } 52 | 53 | message DeleteSessionResponse { 54 | string SessionID = 1; 55 | } 56 | 57 | message CreateCsrfTokenRequest { 58 | CsrfTokenInput CsrfTokenInput = 1; 59 | } 60 | 61 | message CreateCsrfTokenResponse { 62 | CsrfToken CsrfToken = 1; 63 | } 64 | 65 | message CheckCsrfTokenRequest { 66 | CsrfTokenCheck CsrfTokenCheck = 1; 67 | } 68 | 69 | message CheckCsrfTokenResponse { 70 | CheckResult CheckResult = 1; 71 | } 72 | 73 | service AuthorizationService { 74 | rpc CreateSession(CreateSessionRequest) returns (CreateSessionResponse) {} 75 | rpc GetSessionByID(GetSessionByIDRequest) returns (GetSessionByIDResponse) {} 76 | rpc DeleteSession(DeleteSessionRequest) returns (DeleteSessionResponse) {} 77 | 78 | rpc CreateCsrfToken(CreateCsrfTokenRequest) returns (CreateCsrfTokenResponse) {} 79 | rpc CheckCsrfToken(CheckCsrfTokenRequest) returns (CheckCsrfTokenResponse) {} 80 | } 81 | -------------------------------------------------------------------------------- /app/users/domain.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | uuid "github.com/satori/go.uuid" 9 | 10 | "golang.org/x/crypto/bcrypt" 11 | "google.golang.org/protobuf/types/known/timestamppb" 12 | 13 | "github.com/badu/microservices-demo/pkg/postgres" 14 | ) 15 | 16 | type UserDO struct { 17 | Role *Role `json:"role"` 18 | CreatedAt *time.Time `json:"created_at"` 19 | UpdatedAt *time.Time `json:"updated_at"` 20 | FirstName string `json:"first_name" validate:"required,min=3,max=25"` 21 | LastName string `json:"last_name" validate:"required,min=3,max=25"` 22 | Email string `json:"email" validate:"required,email"` 23 | Password string `json:"password" validate:"required,min=6,max=250"` 24 | Avatar postgres.NullString `json:"avatar" validate:"max=250" swaggertype:"string"` 25 | UserID uuid.UUID `json:"user_id"` 26 | } 27 | 28 | type UserResponse struct { 29 | Role *Role `json:"role"` 30 | CreatedAt *time.Time `json:"created_at"` 31 | UpdatedAt *time.Time `json:"updated_at"` 32 | FirstName string `json:"first_name" validate:"required,min=3,max=25"` 33 | LastName string `json:"last_name" validate:"required,min=3,max=25"` 34 | Email string `json:"email" validate:"required,email"` 35 | Avatar postgres.NullString `json:"avatar" validate:"max=250" swaggertype:"string"` 36 | UserID uuid.UUID `json:"user_id"` 37 | } 38 | 39 | type UserUpdate struct { 40 | Role *Role `json:"role"` 41 | FirstName string `json:"first_name" validate:"omitempty,min=3,max=25" swaggertype:"string"` 42 | LastName string `json:"last_name" validate:"omitempty,min=3,max=25" swaggertype:"string"` 43 | Email string `json:"email" validate:"omitempty,email" swaggertype:"string"` 44 | Avatar string `json:"avatar" validate:"max=250" swaggertype:"string"` 45 | UserID uuid.UUID `json:"user_id"` 46 | } 47 | 48 | type Login struct { 49 | Email string `json:"email" validate:"required,email"` 50 | Password string `json:"password" validate:"required,min=6,max=250"` 51 | } 52 | 53 | type Role string 54 | 55 | const ( 56 | RoleGuest Role = "guest" 57 | RoleMember Role = "member" 58 | RoleAdmin Role = "admin" 59 | ) 60 | 61 | func (e *Role) ToString() string { 62 | return string(*e) 63 | } 64 | 65 | func (e *Role) Scan(src interface{}) error { 66 | switch s := src.(type) { 67 | case []byte: 68 | *e = Role(s) 69 | case string: 70 | *e = Role(s) 71 | default: 72 | return fmt.Errorf("unsupported scan type for Role: %T", src) 73 | } 74 | return nil 75 | } 76 | 77 | // Hash user password with bcrypt 78 | func (u *UserDO) HashPassword() error { 79 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) 80 | if err != nil { 81 | return err 82 | } 83 | u.Password = string(hashedPassword) 84 | return nil 85 | } 86 | 87 | // Compare user password and payload 88 | func (u *UserDO) ComparePasswords(password string) error { 89 | if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)); err != nil { 90 | return err 91 | } 92 | return nil 93 | } 94 | 95 | // Sanitize user password 96 | func (u *UserDO) SanitizePassword() { 97 | u.Password = "" 98 | } 99 | 100 | // Prepare user for register 101 | func (u *UserDO) PrepareCreate() error { 102 | u.Email = strings.ToLower(strings.TrimSpace(u.Email)) 103 | u.Password = strings.TrimSpace(u.Password) 104 | 105 | if err := u.HashPassword(); err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | func (r *UserResponse) ToProto() *User { 112 | return &User{ 113 | UserID: r.UserID.String(), 114 | FirstName: r.FirstName, 115 | LastName: r.LastName, 116 | Email: r.Email, 117 | Avatar: r.Avatar.String, 118 | Role: r.Role.ToString(), 119 | CreatedAt: timestamppb.New(*r.CreatedAt), 120 | UpdatedAt: timestamppb.New(*r.UpdatedAt), 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/users/grpc_server.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | uuid "github.com/satori/go.uuid" 8 | "github.com/streadway/amqp" 9 | 10 | "github.com/badu/microservices-demo/app/images" 11 | "github.com/badu/microservices-demo/app/sessions" 12 | grpcErrors "github.com/badu/microservices-demo/pkg/grpc_errors" 13 | "github.com/badu/microservices-demo/pkg/logger" 14 | ) 15 | 16 | type Service interface { 17 | Register(ctx context.Context, user *UserDO) (*UserResponse, error) 18 | Login(ctx context.Context, login Login) (*UserDO, error) 19 | GetByID(ctx context.Context, userID uuid.UUID) (*UserResponse, error) 20 | CreateSession(ctx context.Context, userID uuid.UUID) (string, error) 21 | GetSessionByID(ctx context.Context, sessionID string) (*sessions.SessionDO, error) 22 | GetCSRFToken(ctx context.Context, sessionID string) (string, error) 23 | DeleteSession(ctx context.Context, sessionID string) error 24 | Update(ctx context.Context, user *UserUpdate) (*UserResponse, error) 25 | UpdateUploadedAvatar(ctx context.Context, delivery amqp.Delivery) error 26 | UpdateAvatar(ctx context.Context, data *images.UpdateAvatarMsg) error 27 | GetUsersByIDs(ctx context.Context, userIDs []string) ([]*UserResponse, error) 28 | } 29 | 30 | type ServerImpl struct { 31 | service Service 32 | logger logger.Logger 33 | } 34 | 35 | func NewServer(service Service, logger logger.Logger) ServerImpl { 36 | return ServerImpl{service: service, logger: logger} 37 | } 38 | 39 | func (e *ServerImpl) GetUserByID(ctx context.Context, r *GetByIDRequest) (*GetByIDResponse, error) { 40 | span, ctx := opentracing.StartSpanFromContext(ctx, "users_grpc_server.GetUserByID") 41 | defer span.Finish() 42 | 43 | userUUID, err := uuid.FromString(r.GetUserID()) 44 | if err != nil { 45 | e.logger.Errorf("uuid.FromString: %v", err) 46 | return nil, grpcErrors.ErrorResponse(err, "uuid.FromString") 47 | } 48 | 49 | foundUser, err := e.service.GetByID(ctx, userUUID) 50 | if err != nil { 51 | e.logger.Errorf("uuid.FromString: %v", err) 52 | return nil, grpcErrors.ErrorResponse(err, "service.GetByID") 53 | } 54 | 55 | return &GetByIDResponse{User: foundUser.ToProto()}, nil 56 | } 57 | 58 | func (e *ServerImpl) GetUsersByIDs(ctx context.Context, req *GetUsersByIDsRequest) (*GetUsersByIDsResponse, error) { 59 | span, ctx := opentracing.StartSpanFromContext(ctx, "users_grpc_server.GetUserByID") 60 | defer span.Finish() 61 | 62 | usersByIDs, err := e.service.GetUsersByIDs(ctx, req.GetUsersIDs()) 63 | if err != nil { 64 | e.logger.Errorf("service.GetUsersByIDs: %v", err) 65 | return nil, grpcErrors.ErrorResponse(err, "service.GetUsersByIDs") 66 | } 67 | 68 | response := idsToUUID(usersByIDs) 69 | e.logger.Infof("USERS LIST RESPONSE: %v", response) 70 | 71 | return &GetUsersByIDsResponse{Users: response}, nil 72 | } 73 | 74 | func idsToUUID(users []*UserResponse) []*User { 75 | usersList := make([]*User, 0, len(users)) 76 | for _, val := range users { 77 | usersList = append(usersList, val.ToProto()) 78 | } 79 | 80 | return usersList 81 | } 82 | -------------------------------------------------------------------------------- /app/users/middleware.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/opentracing/opentracing-go" 10 | 11 | "github.com/badu/microservices-demo/pkg/config" 12 | httpErrors "github.com/badu/microservices-demo/pkg/http_errors" 13 | "github.com/badu/microservices-demo/pkg/logger" 14 | ) 15 | 16 | type SessionManager struct { 17 | logger logger.Logger 18 | cfg *config.Config 19 | service Service 20 | } 21 | 22 | func NewSessionManager(logger logger.Logger, cfg *config.Config, service Service) SessionManager { 23 | return SessionManager{logger: logger, cfg: cfg, service: service} 24 | } 25 | 26 | type RequestCtxUser struct{} 27 | 28 | type RequestCtxSession struct{} 29 | 30 | func (m *SessionManager) SessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 31 | return func(c echo.Context) error { 32 | span, ctx := opentracing.StartSpanFromContext(c.Request().Context(), "users_middlewares.SessionMiddleware") 33 | defer span.Finish() 34 | 35 | cookie, err := c.Cookie(m.cfg.HttpServer.SessionCookieName) 36 | if err != nil { 37 | if errors.Is(err, http.ErrNoCookie) { 38 | m.logger.Errorf("SessionMiddleware.ErrNoCookie: %v", err) 39 | return httpErrors.ErrorCtxResponse(c, err) 40 | } 41 | m.logger.Errorf("SessionMiddleware.c.Cookie: %v", err) 42 | return httpErrors.ErrorCtxResponse(c, err) 43 | } 44 | 45 | sessionByID, err := m.service.GetSessionByID(ctx, cookie.Value) 46 | if err != nil { 47 | m.logger.Errorf("SessionMiddleware.GetSessionByID: %v", err) 48 | return httpErrors.ErrorCtxResponse(c, httpErrors.Unauthorized) 49 | } 50 | 51 | userResponse, err := m.service.GetByID(ctx, sessionByID.UserID) 52 | if err != nil { 53 | m.logger.Errorf("SessionMiddleware.service.GetByID: %v", err) 54 | return httpErrors.ErrorCtxResponse(c, err) 55 | } 56 | 57 | ctx = context.WithValue(c.Request().Context(), RequestCtxUser{}, userResponse) 58 | ctx = context.WithValue(ctx, RequestCtxSession{}, sessionByID) 59 | c.SetRequest(c.Request().WithContext(ctx)) 60 | 61 | return next(c) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/users/rabbit_config.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | const ( 4 | exchangeKind = "direct" 5 | exchangeDurable = true 6 | exchangeAutoDelete = false 7 | exchangeInternal = false 8 | exchangeNoWait = false 9 | 10 | queueDurable = true 11 | queueAutoDelete = false 12 | queueExclusive = false 13 | queueNoWait = false 14 | 15 | publishMandatory = false 16 | publishImmediate = false 17 | 18 | prefetchCount = 1 19 | prefetchSize = 0 20 | prefetchGlobal = false 21 | 22 | consumeAutoAck = false 23 | consumeExclusive = false 24 | consumeNoLocal = false 25 | consumeNoWait = false 26 | 27 | UserExchange = "users" 28 | 29 | AvatarsQueueName = "avatars_queue" 30 | AvatarsConsumerTag = "user_avatar_consumer" 31 | AvatarsWorkers = 5 32 | AvatarsBindingKey = "update_avatar_key" 33 | ) 34 | -------------------------------------------------------------------------------- /app/users/rabbit_publisher.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/opentracing/opentracing-go" 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/prometheus/client_golang/prometheus/promauto" 11 | uuid "github.com/satori/go.uuid" 12 | "github.com/streadway/amqp" 13 | 14 | "github.com/badu/microservices-demo/pkg/config" 15 | "github.com/badu/microservices-demo/pkg/logger" 16 | "github.com/badu/microservices-demo/pkg/rabbitmq" 17 | ) 18 | 19 | var ( 20 | successPublisherMessages = promauto.NewCounter(prometheus.CounterOpts{ 21 | Name: "rabbitmq_images_success_publish_messages_total", 22 | Help: "The total number of success RabbitMQ published messages", 23 | }) 24 | errorPublisherMessages = promauto.NewCounter(prometheus.CounterOpts{ 25 | Name: "rabbitmq_images_error_publish_messages_total", 26 | Help: "The total number of error RabbitMQ published messages", 27 | }) 28 | ) 29 | 30 | type PublisherImpl struct { 31 | amqpConn *amqp.Connection 32 | cfg *config.Config 33 | logger logger.Logger 34 | } 35 | 36 | func NewUserPublisher(cfg *config.Config, logger logger.Logger) (PublisherImpl, error) { 37 | amqpConn, err := rabbitmq.NewRabbitMQConn(cfg.RabbitMQ) 38 | if err != nil { 39 | return PublisherImpl{}, err 40 | } 41 | return PublisherImpl{cfg: cfg, logger: logger, amqpConn: amqpConn}, nil 42 | } 43 | 44 | func (p *PublisherImpl) CreateExchangeAndQueue(exchange, queueName, bindingKey string) (*amqp.Channel, error) { 45 | amqpChan, err := p.amqpConn.Channel() 46 | if err != nil { 47 | return nil, errors.Join(err, errors.New("p.amqpConn.Channel")) 48 | } 49 | 50 | p.logger.Infof("Declaring exchange: %s", exchange) 51 | if err := amqpChan.ExchangeDeclare( 52 | exchange, 53 | exchangeKind, 54 | exchangeDurable, 55 | exchangeAutoDelete, 56 | exchangeInternal, 57 | exchangeNoWait, 58 | nil, 59 | ); err != nil { 60 | return nil, errors.Join(err, errors.New("error ch.ExchangeDeclare")) 61 | } 62 | 63 | queue, err := amqpChan.QueueDeclare( 64 | queueName, 65 | queueDurable, 66 | queueAutoDelete, 67 | queueExclusive, 68 | queueNoWait, 69 | nil, 70 | ) 71 | if err != nil { 72 | return nil, errors.Join(err, errors.New("error ch.QueueDeclare")) 73 | } 74 | 75 | p.logger.Infof("Declared queue, binding it to exchange: Queue: %v, messageCount: %v, "+ 76 | "consumerCount: %v, exchange: %v, exchange: %v, bindingKey: %v", 77 | queue.Name, 78 | queue.Messages, 79 | queue.Consumers, 80 | exchange, 81 | bindingKey, 82 | ) 83 | 84 | err = amqpChan.QueueBind( 85 | queue.Name, 86 | bindingKey, 87 | exchange, 88 | queueNoWait, 89 | nil, 90 | ) 91 | if err != nil { 92 | return nil, errors.Join(err, errors.New("error ch.QueueBind")) 93 | } 94 | 95 | return amqpChan, nil 96 | } 97 | 98 | // Publish message 99 | func (p *PublisherImpl) Publish(ctx context.Context, exchange, routingKey, contentType string, headers amqp.Table, body []byte) error { 100 | span, ctx := opentracing.StartSpanFromContext(ctx, "rabbit_publisher_users.Publish") 101 | defer span.Finish() 102 | 103 | amqpChan, err := p.amqpConn.Channel() 104 | if err != nil { 105 | return errors.Join(err, errors.New("p.amqpConn.Channel")) 106 | } 107 | defer amqpChan.Close() 108 | 109 | p.logger.Infof("Publishing message Exchange: %s, RoutingKey: %s", exchange, routingKey) 110 | 111 | if err := amqpChan.Publish( 112 | exchange, 113 | routingKey, 114 | publishMandatory, 115 | publishImmediate, 116 | amqp.Publishing{ 117 | Headers: headers, 118 | ContentType: contentType, 119 | DeliveryMode: amqp.Persistent, 120 | MessageId: uuid.NewV4().String(), 121 | Timestamp: time.Now().UTC(), 122 | Body: body, 123 | }, 124 | ); err != nil { 125 | errorPublisherMessages.Inc() 126 | return errors.Join(err, errors.New("ch.Publish")) 127 | } 128 | 129 | successPublisherMessages.Inc() 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /app/users/redis_repository.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "github.com/opentracing/opentracing-go" 12 | uuid "github.com/satori/go.uuid" 13 | ) 14 | 15 | type RedisRepositoryImpl struct { 16 | client *redis.Client 17 | prefix string 18 | expiration time.Duration 19 | } 20 | 21 | func NewRedisRepository(redisConn *redis.Client, prefix string, expiration time.Duration) RedisRepositoryImpl { 22 | return RedisRepositoryImpl{client: redisConn, prefix: prefix, expiration: expiration} 23 | } 24 | 25 | func (u *RedisRepositoryImpl) SaveUser(ctx context.Context, user *UserResponse) error { 26 | span, ctx := opentracing.StartSpanFromContext(ctx, "users_Redis_repository.SaveUser") 27 | defer span.Finish() 28 | 29 | userBytes, err := json.Marshal(user) 30 | if err != nil { 31 | return errors.Join(err, errors.New("userRedisRepository.SaveUser.json.Marshal")) 32 | } 33 | 34 | if err := u.client.SetEX(ctx, u.createKey(user.UserID), string(userBytes), u.expiration).Err(); err != nil { 35 | return errors.Join(err, errors.New("userRedisRepository.SaveUser.client.SetEX")) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (u *RedisRepositoryImpl) GetUserByID(ctx context.Context, userID uuid.UUID) (*UserResponse, error) { 42 | span, ctx := opentracing.StartSpanFromContext(ctx, "users_Redis_repository.GetUserByID") 43 | defer span.Finish() 44 | 45 | result, err := u.client.Get(ctx, u.createKey(userID)).Bytes() 46 | if err != nil { 47 | return nil, errors.Join(err, errors.New("userRedisRepository.GetUserByID.client.Get")) 48 | } 49 | 50 | var res UserResponse 51 | if err := json.Unmarshal(result, &res); err != nil { 52 | return nil, errors.Join(err, errors.New("userRedisRepository.GetUserByID.json.Unmarshal")) 53 | } 54 | return &res, nil 55 | } 56 | 57 | func (u *RedisRepositoryImpl) DeleteUser(ctx context.Context, userID uuid.UUID) error { 58 | span, ctx := opentracing.StartSpanFromContext(ctx, "users_Redis_repository.DeleteUser") 59 | defer span.Finish() 60 | 61 | if err := u.client.Del(ctx, u.createKey(userID)).Err(); err != nil { 62 | return errors.Join(err, errors.New("userRedisRepository.GetUserByID.client.Del")) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (u *RedisRepositoryImpl) createKey(userID uuid.UUID) string { 69 | return fmt.Sprintf("%s: %s", u.prefix, userID) 70 | } 71 | -------------------------------------------------------------------------------- /app/users/ssl/instructions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Inspired from: https://github.com/grpc/grpc-java/tree/master/examples#generating-self-signed-certificates-for-use-with-grpc 3 | 4 | # Output files 5 | # ca.key: Certificate Authority private key file (this shouldn't be shared in real-life) 6 | # ca.crt: Certificate Authority trust certificate (this should be shared with users in real-life) 7 | # server.key: Server private key, password protected (this shouldn't be shared) 8 | # server.csr: Server certificate signing request (this should be shared with the CA owner) 9 | # server.crt: Server certificate signed by the CA (this would be sent back by the CA owner) - keep on server 10 | # server.pem: Conversion of server.key into a format grpc likes (this shouldn't be shared) 11 | 12 | # Summary 13 | # Private files: ca.key, server.key, server.pem, server.crt 14 | # "Share" files: ca.crt (needed by the client), server.csr (needed by the CA) 15 | 16 | # Changes these CN's to match your hosts in your environment if needed. 17 | SERVER_CN=localhost 18 | 19 | # Step 1: Generate Certificate Authority + Trust Certificate (ca.crt) 20 | openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 21 | openssl req -passin pass:1111 -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=${SERVER_CN}" 22 | 23 | # Step 2: Generate the Server Private Key (server.key) 24 | openssl genrsa -passout pass:1111 -des3 -out server.key 4096 25 | 26 | # Step 3: Get a certificate signing request from the CA (server.csr) 27 | openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/CN=${SERVER_CN}" 28 | 29 | # Step 4: Sign the certificate with the CA we created (it's called self signing) - server.crt 30 | openssl x509 -req -passin pass:1111 -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt 31 | 32 | # Step 5: Convert the server certificate to .pem format (server.pem) - usable by grpc 33 | openssl pkcs8 -topk8 -nocrypt -passin pass:1111 -in server.key -out server.pem -------------------------------------------------------------------------------- /app/users/user.proto: -------------------------------------------------------------------------------- 1 | // protoc --go_out=plugins=grpc:. *.proto 2 | 3 | syntax = "proto3"; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | package users; 8 | 9 | option go_package = "github.com/badu/microservices-demo/app/users;users"; 10 | 11 | message User { 12 | google.protobuf.Timestamp CreatedAt = 7; 13 | google.protobuf.Timestamp UpdatedAt = 8; 14 | string UserID = 1; 15 | string FirstName = 2; 16 | string LastName = 3; 17 | string Email = 4; 18 | string Avatar = 5; 19 | string Role = 6; 20 | } 21 | 22 | message GetByIDResponse { 23 | User User = 1; 24 | } 25 | 26 | message GetByIDRequest { 27 | string UserID = 1; 28 | } 29 | 30 | 31 | message GetUsersByIDsResponse { 32 | repeated User Users = 1; 33 | } 34 | 35 | message GetUsersByIDsRequest { 36 | repeated string UsersIDs = 1; 37 | } 38 | 39 | service UserService { 40 | rpc GetUserByID(GetByIDRequest) returns (GetByIDResponse) {} 41 | rpc GetUsersByIDs(GetUsersByIDsRequest) returns (GetUsersByIDsResponse) {} 42 | } 43 | -------------------------------------------------------------------------------- /cmd/comments/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | MAINTAINER Badu 4 | 5 | RUN apk update \ 6 | && apk add --no-cache ca-certificates git make 7 | 8 | WORKDIR /comments_workdir 9 | 10 | COPY . . 11 | 12 | RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o ./build/service github.com/badu/microservices-demo/cmd/comments 13 | 14 | FROM alpine:3.17 15 | 16 | RUN apk update \ 17 | && apk add --no-cache ca-certificates tzdata && \ 18 | addgroup -S badu && adduser -S badu -G badu 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=builder /comments_workdir/build/service /app 23 | 24 | USER badu 25 | 26 | CMD /app/service 27 | -------------------------------------------------------------------------------- /cmd/comments/Makefile: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # Go migrate postgresql User service 3 | # ============================================================================== 4 | 5 | comments_dbname = comments_db 6 | comments_port = 5436 7 | comments_SSL_MODE = disable 8 | 9 | force_comments_db: 10 | migrate -database postgres://postgres:postgres@localhost:$(comments_port)/$(comments_dbname)?sslmode=$(comments_SSL_MODE) -path migrations force 1 11 | 12 | version_comments_db: 13 | migrate -database postgres://postgres:postgres@localhost:$(comments_port)/$(comments_dbname)?sslmode=$(comments_SSL_MODE) -path migrations version 14 | 15 | migrate_comments_db_up: 16 | migrate -database postgres://postgres:postgres@localhost:$(comments_port)/$(comments_dbname)?sslmode=$(comments_SSL_MODE) -path migrations up 1 17 | 18 | migrate_comments_db_down: 19 | migrate -database postgres://postgres:postgres@localhost:$(comments_port)/$(comments_dbname)?sslmode=$(comments_SSL_MODE) -path migrations down 1 20 | -------------------------------------------------------------------------------- /cmd/comments/config/config-docker.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5004" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | UserGrpcServicePort: ":5001" 18 | 19 | Rabbitmq: 20 | Host: localhost 21 | Port: 5672 22 | User: guest 23 | Password: guest 24 | 25 | HttpServer: 26 | Port: ":8015" 27 | PprofPort: ":8115" 28 | Timeout: 15 29 | ReadTimeout: 5 30 | WriteTimeout: 5 31 | CookieLifeTime: 44640 32 | SessionCookieName: "session_token" 33 | 34 | postgres: 35 | PostgresqlHost: localhost 36 | PostgresqlPort: 5436 37 | PostgresqlUser: postgres 38 | PostgresqlPassword: postgres 39 | PostgresqlDbname: comments_db 40 | PostgresqlSslmode: "disable" 41 | PgDriver: pgx 42 | 43 | logger: 44 | Development: true 45 | DisableCaller: false 46 | DisableStacktrace: false 47 | Encoding: json 48 | Level: info 49 | PrintConfig: false 50 | 51 | redis: 52 | RedisAddr: localhost:6379 53 | RedisPassword: 54 | RedisDb: 0 55 | RedisDefaultDB: 0 56 | MinIdleConn: 200 57 | PoolSize: 12000 58 | PoolTimeout: 240 59 | Password: "" 60 | DB: 0 61 | 62 | metrics: 63 | Port: ":7074" 64 | Url: 0.0.0.0:7074 65 | ServiceName: comments_microservice 66 | 67 | jaeger: 68 | Host: localhost:6831 69 | ServiceName: CommentsService_GRPC 70 | LogSpans: false 71 | 72 | -------------------------------------------------------------------------------- /cmd/comments/config/config-local.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5056" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | UserGrpcServicePort: ":5001" 18 | 19 | Rabbitmq: 20 | Host: localhost 21 | Port: 5672 22 | User: guest 23 | Password: guest 24 | 25 | HttpServer: 26 | Port: ":8015" 27 | PprofPort: ":8115" 28 | Timeout: 15 29 | ReadTimeout: 5 30 | WriteTimeout: 5 31 | CookieLifeTime: 44640 32 | SessionCookieName: "session_token" 33 | 34 | postgres: 35 | PostgresqlHost: localhost 36 | PostgresqlPort: 5436 37 | PostgresqlUser: postgres 38 | PostgresqlPassword: postgres 39 | PostgresqlDbname: comments_db 40 | PostgresqlSslmode: "disable" 41 | PgDriver: pgx 42 | 43 | logger: 44 | Development: true 45 | DisableCaller: false 46 | DisableStacktrace: false 47 | Encoding: json 48 | Level: info 49 | PrintConfig: false 50 | 51 | redis: 52 | RedisAddr: localhost:6379 53 | RedisPassword: 54 | RedisDb: 0 55 | RedisDefaultDB: 0 56 | MinIdleConn: 200 57 | PoolSize: 12000 58 | PoolTimeout: 240 59 | Password: "" 60 | DB: 0 61 | 62 | metrics: 63 | Port: ":7078" 64 | Url: 0.0.0.0:7078 65 | ServiceName: comments_microservice 66 | 67 | jaeger: 68 | Host: localhost:6831 69 | ServiceName: CommentsService_GRPC 70 | LogSpans: false 71 | 72 | -------------------------------------------------------------------------------- /cmd/comments/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/badu/microservices-demo/app/comments" 8 | 9 | "github.com/opentracing/opentracing-go" 10 | 11 | "github.com/badu/microservices-demo/pkg/config" 12 | "github.com/badu/microservices-demo/pkg/jaeger" 13 | "github.com/badu/microservices-demo/pkg/logger" 14 | "github.com/badu/microservices-demo/pkg/postgres" 15 | ) 16 | 17 | func main() { 18 | configPath := config.GetConfigPath(os.Getenv("config")) 19 | cfg, err := config.GetConfig(configPath) 20 | if err != nil { 21 | log.Fatalf("Loading config: %v", err) 22 | } 23 | 24 | appLogger := logger.NewApiLogger(cfg.Logger) 25 | appLogger.InitLogger() 26 | appLogger.Info("Starting Comments service") 27 | appLogger.Infof( 28 | "AppVersion: %s, LogLevel: %s, Mode: %s", 29 | cfg.GRPCServer.AppVersion, 30 | cfg.Logger.Level, 31 | cfg.GRPCServer.Mode, 32 | ) 33 | appLogger.Infof("Success parsed config: %#v", cfg.GRPCServer.AppVersion) 34 | 35 | pgxConn, err := postgres.NewPgxConn(cfg.Postgres) 36 | if err != nil { 37 | appLogger.Fatal("cannot connect to postgres", err) 38 | } 39 | defer pgxConn.Close() 40 | 41 | appLogger.Infof("%-v", pgxConn.Stat()) 42 | 43 | tracer, closer, err := jaeger.InitJaeger(cfg.Jaeger) 44 | if err != nil { 45 | appLogger.Fatal("cannot create tracer", err) 46 | } 47 | 48 | appLogger.Info("connected to Jaeger") 49 | 50 | opentracing.SetGlobalTracer(tracer) 51 | defer closer.Close() 52 | 53 | appLogger.Info("connected to Opentracing") 54 | 55 | app := comments.NewApplication(&appLogger, cfg, pgxConn, tracer) 56 | 57 | appLogger.Fatal(app.Run()) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/comments/migrations/01_create_initial_tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS comments CASCADE; 2 | DROP TRIGGER IF EXISTS comment_updated_at_trigger ON comments; 3 | DROP FUNCTION IF EXISTS comment_updated; 4 | DROP EXTENSION IF EXISTS citext CASCADE; 5 | DROP EXTENSION IF EXISTS POSTGIS CASCADE; 6 | DROP EXTENSION IF EXISTS pg_trgm CASCADE; 7 | DROP EXTENSION IF EXISTS btree_gist CASCADE; 8 | DROP EXTENSION IF EXISTS "uuid-ossp" CASCADE; 9 | DROP INDEX IF EXISTS hotel_id_idx; -------------------------------------------------------------------------------- /cmd/comments/migrations/01_create_initial_tables.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS citext; 2 | CREATE EXTENSION IF NOT EXISTS POSTGIS; 3 | CREATE EXTENSION IF NOT EXISTS pg_trgm; 4 | CREATE EXTENSION IF NOT EXISTS btree_gist; 5 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 6 | DROP TABLE IF EXISTS comments CASCADE; 7 | 8 | create table comments 9 | ( 10 | comment_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 11 | hotel_id UUID NOT NULL, 12 | user_id UUID NOT NULL, 13 | message citext NOT NULL CHECK ( message <> '' ), 14 | photos text[], 15 | rating float DEFAULT 0 CHECK (rating >= 0 AND rating <= 10), 16 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, 17 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 18 | ); 19 | 20 | CREATE INDEX IF NOT EXISTS hotel_id_idx ON comments (hotel_id); 21 | 22 | CREATE OR REPLACE FUNCTION comment_updated() RETURNS TRIGGER AS 23 | $$ 24 | BEGIN 25 | NEW.updated_at = CURRENT_TIMESTAMP; 26 | RETURN NEW; 27 | END; 28 | $$ LANGUAGE plpgsql; 29 | 30 | CREATE TRIGGER comment_updated_at_trigger 31 | BEFORE INSERT OR UPDATE 32 | ON comments 33 | FOR EACH ROW 34 | EXECUTE PROCEDURE comment_updated(); -------------------------------------------------------------------------------- /cmd/gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | MAINTAINER Badu 4 | 5 | RUN apk update \ 6 | && apk add --no-cache ca-certificates git make 7 | 8 | WORKDIR /gateway_workdir 9 | 10 | COPY . . 11 | 12 | RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o ./build/service github.com/badu/microservices-demo/cmd/gateway 13 | 14 | FROM alpine:3.17 15 | 16 | RUN apk update \ 17 | && apk add --no-cache ca-certificates tzdata && \ 18 | addgroup -S badu && adduser -S badu -G badu 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=builder /gateway_workdir/build/service /app 23 | 24 | USER badu 25 | 26 | CMD /app/service 27 | -------------------------------------------------------------------------------- /cmd/gateway/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: 2 | 3 | 4 | # ============================================================================== 5 | # Make local SSL Certificate 6 | # ============================================================================== 7 | 8 | make_cert: 9 | echo "Generating SSL certificates" 10 | sh ./gateway/ssl/instructions.sh 11 | -------------------------------------------------------------------------------- /cmd/gateway/config/config-docker.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5001" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | 18 | GRCP: 19 | HotelsServicePort: ":7075" 20 | 21 | HttpServer: 22 | AppVersion: 1.0.0 23 | Port: ":8081" 24 | PprofPort: ":8100" 25 | Timeout: 15 26 | ReadTimeout: 5 27 | WriteTimeout: 5 28 | CookieLifeTime: 44640 29 | SessionCookieName: "session_token" 30 | 31 | rabbitmq: 32 | Host: localhost 33 | Port: 5672 34 | User: guest 35 | Password: guest 36 | Exchange: emails-exchange 37 | Queue: emails-queue 38 | RoutingKey: emails-routing-key 39 | ConsumerTag: emails-consumer 40 | WorkerPoolSize: 24 41 | 42 | logger: 43 | Development: true 44 | DisableCaller: false 45 | DisableStacktrace: false 46 | Encoding: json 47 | Level: info 48 | PrintConfig: false 49 | 50 | postgres: 51 | PostgresqlHost: localhost 52 | PostgresqlPort: 5433 53 | PostgresqlUser: postgres 54 | PostgresqlPassword: postgres 55 | PostgresqlDbname: users_db 56 | PostgresqlSslmode: "disable" 57 | PgDriver: pgx 58 | 59 | redis: 60 | RedisAddr: localhost:6379 61 | RedisPassword: 62 | RedisDb: 0 63 | RedisDefaultDB: 0 64 | MinIdleConn: 200 65 | PoolSize: 12000 66 | PoolTimeout: 240 67 | Password: "" 68 | DB: 0 69 | 70 | metrics: 71 | Url: 0.0.0.0:7071 72 | ServiceName: user_microservice 73 | 74 | jaeger: 75 | Host: localhost:6831 76 | ServiceName: UserService_GRPC 77 | LogSpans: false 78 | -------------------------------------------------------------------------------- /cmd/gateway/config/config-local.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5001" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | 17 | GRCP: 18 | SessionServicePort: ":5000" 19 | HotelsServicePort: ":7075" 20 | UserServicePort: "" 21 | CommentsServicePort: "" 22 | ImagesServicePort: "" 23 | 24 | HttpServer: 25 | AppVersion: 1.0.0 26 | Port: ":8081" 27 | PprofPort: ":8100" 28 | Timeout: 15 29 | ReadTimeout: 5 30 | WriteTimeout: 5 31 | CookieLifeTime: 44640 32 | SessionCookieName: "session_token" 33 | 34 | RabbitMQ: 35 | Host: localhost 36 | Port: 5672 37 | User: guest 38 | Password: guest 39 | Exchange: emails-exchange 40 | Queue: emails-queue 41 | RoutingKey: emails-routing-key 42 | ConsumerTag: emails-consumer 43 | WorkerPoolSize: 24 44 | 45 | Logger: 46 | Development: true 47 | DisableCaller: false 48 | DisableStacktrace: false 49 | Encoding: json 50 | Level: info 51 | 52 | Postgres: 53 | PostgresqlHost: localhost 54 | PostgresqlPort: 5433 55 | PostgresqlUser: postgres 56 | PostgresqlPassword: postgres 57 | PostgresqlDbname: users 58 | PostgresqlSslmode: "disable" 59 | PgDriver: pgx 60 | 61 | Redis: 62 | RedisAddr: localhost:6379 63 | RedisPassword: 64 | RedisDb: 0 65 | RedisDefaultDB: 0 66 | MinIdleConn: 200 67 | PoolSize: 12000 68 | PoolTimeout: 240 69 | Password: "" 70 | DB: 0 71 | 72 | Metrics: 73 | Url: 0.0.0.0:7071 74 | ServiceName: Gateway 75 | 76 | Jaeger: 77 | Host: localhost:6831 78 | ServiceName: Gateway 79 | LogSpans: false 80 | -------------------------------------------------------------------------------- /cmd/gateway/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | 9 | "github.com/badu/microservices-demo/app/gateway" 10 | "github.com/badu/microservices-demo/pkg/config" 11 | "github.com/badu/microservices-demo/pkg/jaeger" 12 | "github.com/badu/microservices-demo/pkg/logger" 13 | "github.com/badu/microservices-demo/pkg/redis" 14 | ) 15 | 16 | func main() { 17 | configPath := config.GetConfigPath(os.Getenv("config")) 18 | cfg, err := config.GetConfig(configPath) 19 | if err != nil { 20 | log.Fatalf("Loading config: %v", err) 21 | } 22 | 23 | appLogger := logger.NewApiLogger(cfg.Logger) 24 | appLogger.InitLogger() 25 | appLogger.Info("Starting API Gateway") 26 | appLogger.Infof( 27 | "AppVersion: %s, LogLevel: %s, Mode: %t", 28 | cfg.HttpServer.AppVersion, 29 | cfg.Logger.Level, 30 | cfg.Logger.Development, 31 | ) 32 | appLogger.Infof("Success parsed config - app version : %#v", cfg.HttpServer.AppVersion) 33 | appLogger.Infof("HotelsServicePort : %q", cfg.GRPC.HotelsServicePort) 34 | 35 | tracer, closer, err := jaeger.InitJaeger(cfg.Jaeger) 36 | if err != nil { 37 | appLogger.Fatal("cannot create tracer", err) 38 | } 39 | appLogger.Info("connected to Jaeger") 40 | 41 | opentracing.SetGlobalTracer(tracer) 42 | defer closer.Close() 43 | 44 | appLogger.Info("connected to Opentracing") 45 | 46 | redisClient := redis.NewRedisClient(cfg.Redis) 47 | appLogger.Infof("connected to Redis : %#v", redisClient.PoolStats()) 48 | 49 | app := gateway.NewApplication(&appLogger, cfg, redisClient, tracer) 50 | appLogger.Fatal(app.Run()) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/gateway/ssl/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFCTCCAvGgAwIBAgIUXurcQIRzoL3gpcE71Gjmk2DBtFAwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMDMyNDA5Mjk0M1oXDTMxMDMy 4 | MjA5Mjk0M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF 5 | AAOCAg8AMIICCgKCAgEAotEyqeK5SIMD0i7g53vwueWiu/moBcDCsba94scVmAzG 6 | ICjLW44Ru98Sk28GS1YLf+ooYCGWgFcO/P2Zm1WCj28HL2eas7iDQAF0c5l2T7sF 7 | pQzJFwn4JGwQ32bLnuV58WseSflCzdswnIt0KqsJOpCdr6pXwZjJnQT+6HVJxAoV 8 | NK1IjawCQRH7RluzaiCIJO8CcEJseO14mLv8eE7TKl7GekfMKd+yrbFX4nc/Rmyf 9 | OciNiYbDjy6HZqRRI3jjFgGpwughYhb/LgTEKBl2D9seR3z7yxvZ1WhRqMTWog+U 10 | byGOG5/zsdohDqsZe05o/zdAlurTAlRuctNu8Dodzn/83XZZMjO/qjfwOYyt5FJO 11 | molfnJoW4LMA83pwxgzu/7RbxMqbwc2KjxZLLSl9hz5L8QdShy485xGMVQe0fIGb 12 | SH62dauIN4681v4Me+Op+DwDckNm1o9HC29QudQIny76RpbDUuzr+0zWU565jzVm 13 | 9LPjx2jrBRuf/wlW41ZI3sZ6UdsRoZYiEZYZB18Zq/9hVfaZgWPz6fyqoFw6H6gm 14 | 29+mTZDJvHWxDD6EiohYTmlwEaJdYat2osp4YKA/Oo85d6//GQR2XoETq3cC+ukt 15 | 4XeJZ/Qse78IYrdl1UBGVsvgNfImw5bEN8nFb9eANvUo/0Td4fT2pRFgE84fgAEC 16 | AwEAAaNTMFEwHQYDVR0OBBYEFFjk3hqzNiU1wEwuUX89/5WzydyHMB8GA1UdIwQY 17 | MBaAFFjk3hqzNiU1wEwuUX89/5WzydyHMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI 18 | hvcNAQELBQADggIBAGcPB324pJ36mJr1JIpw8pnuR3dbucRnnL5JCgggSUDI0JKg 19 | 67xj5+RD3Nks8OVaeewyq2S9j922theO74aWPvh2nT2bL2v9+gLthJhxnCxtJYn9 20 | uxYhCg35w/CXVvxXuWiIJIbdg98f/26h20RG1I6/XO5EW0jyEYbAAYUam4vfASA1 21 | oHGSA3KekFBTwIcOH5NO+JQO9Nvle5TI7EWGOPBQXUHbmsR5ofPN9hM0eknEVKz/ 22 | D76BE6PdUFJTpwi2UJoxpU+HjFuL/Dmwzzjtkfv+MKXu/HonuNIdNv4PgO+bD6jF 23 | 51ONF/e5Kp0EtoNladp+mV0t4K+On/ih2j8MruCdQCJwmDG0tMgoDcEhQBYL1dP/ 24 | FGKF2qHmgf7XhnDJoD5WkTV57E1MvkMZl9diwNDQuaiRkeicUkOo6K6SChqqBhaQ 25 | r2iFus6oUhvCYtNbjwmGrFac4vWhUWcDDA5yvHiVgc+VX5060v2QnIWdoAWRccPH 26 | zDMA+9m030n8jX4hh4OKWJMqRtW/Z2dCmJyfvF0z8xj3lT4IbCSnVBoxw6IzMoJs 27 | GPNEKLPThliPZ54dcvSV0PNm1V6GB2K5un8kocf2W3LgNJB98BSUW+dtLD3Xe2mp 28 | plKjwIvpzKDhiQd5R5tFCYL0APDkt8xI8eBDuLf0HhfcMUMZjv5NNe0JeBg2 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /cmd/gateway/ssl/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,4E47BBFDA239DC2E 4 | 5 | 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 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /cmd/gateway/ssl/instructions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Inspired from: https://github.com/grpc/grpc-java/tree/master/examples#generating-self-signed-certificates-for-use-with-grpc 4 | 5 | # Output files 6 | # ca.key: Certificate Authority private key file (this shouldn't be shared in real-life) 7 | # ca.crt: Certificate Authority trust certificate (this should be shared with users in real-life) 8 | # server.key: Server private key, password protected (this shouldn't be shared) 9 | # server.csr: Server certificate signing request (this should be shared with the CA owner) 10 | # server.crt: Server certificate signed by the CA (this would be sent back by the CA owner) - keep on server 11 | # server.pem: Conversion of server.key into a format grpc likes (this shouldn't be shared) 12 | 13 | # Summary 14 | # Private files: ca.key, server.key, server.pem, server.crt 15 | # "Share" files: ca.crt (needed by the client), server.csr (needed by the CA) 16 | 17 | # Changes these CN's to match your hosts in your environment if needed. 18 | SERVER_CN=localhost 19 | 20 | # Step 1: Generate Certificate Authority + Trust Certificate (ca.crt) 21 | openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 22 | openssl req -passin pass:1111 -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=${SERVER_CN}" 23 | 24 | # Step 2: Generate the Server Private Key (server.key) 25 | openssl genrsa -passout pass:1111 -des3 -out server.key 4096 26 | 27 | # Step 3: Get a certificate signing request from the CA (server.csr) 28 | openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/CN=${SERVER_CN}" 29 | 30 | # Step 4: Sign the certificate with the CA we created (it's called self signing) - server.crt 31 | openssl x509 -req -passin pass:1111 -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt 32 | 33 | # Step 5: Convert the server certificate to .pem format (server.pem) - usable by grpc 34 | openssl pkcs8 -topk8 -nocrypt -passin pass:1111 -in server.key -out server.pem 35 | -------------------------------------------------------------------------------- /cmd/gateway/ssl/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0 3 | MB4XDTIxMDMyNDA5Mjk0NFoXDTMxMDMyMjA5Mjk0NFowFDESMBAGA1UEAwwJbG9j 4 | YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArv8qeXm2pwG/ 5 | v1IpFAQFjVu9xIq0Vf+z98bzD4qWvsO+LccYSKuFfRyK0ZfmrdjxbTeI5leHb8pW 6 | DfO9jp4BDTRzi5s8VsDysKPYjkSmy3/1r680RKZRozi9ZVNnH6I+eVzcLIExfFRI 7 | 6Uj4OjQfIfnU0EKQxmCOFrvbb32HYdes7mx55uOD98l5BV+wQwNt0gEn7HTDlhH8 8 | M13tV5cAHkSkLFMxC98qsbjGwnqatMuHSZmTYaUT+GrvLe/FdWJHs6v4abKrNlrp 9 | vwNPFhUrHTxsoxiD4SY1Fu6rQTqV+JscExKVhShhFJ7CWC0VPwaqS+bIXWy9s6uz 10 | Cmrj0E9cS3dTxScFrm5SRmvyT1oOK0RpyZTz/xW7ezL3GpUDj2mcSDoF+EEFivCX 11 | t6rHorFOq36BXN58M1ih8C+RzxF2sDwqZp7AUe/jmN6Kc0tRvOKT1O08I0idaCw3 12 | 4BnPfYy2n2z7IDo8/odYm3Li5ZN4OKdX4izJzjyyLYG3lMTunFVUMD2QmA2051eu 13 | tW+cekVTXFkIUfw34JMgz9hinTWCTB+vCE1rckThnNWWZb0unEas3tBlJuXHqu57 14 | LejccGBMV6pwgl0Ur2Hiv5X+tl6W87GIfbP1UQUGQFn0G90Te/86YwpDTtHRvNoF 15 | /TzH8BebViY9zNOYTWH2+W6Fz3Jbo+cCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEA 16 | kjPUsFpg2/P/uzUfF+I+X73R1XQS19d0d1tH8m4oQLXMYeM0TzuXyea+m1e0+uCB 17 | 5ZNo37MDBPPghh7E2eIsDh06ZS/ifGHNOkL3CNHsxa1/wtmzZ/LkuoGKVJcBnEqF 18 | lCPxgtK0XGYdUWfAq9vjkjQFBJwe+/XlPfjkmyNvtNCXyx4RiLFVhUSNI+rUkM5x 19 | tnhO53dsIjW5+RjVgsqAN6vwloZ8WQc+Q4OcYWfIGrxq/qNELmzsQ7Nf4udK7m3z 20 | ffbK3H0zeKDxqu52C4nOH/yrkCSvqbFGPkty1eUdtmC8n7AkiRFc8RTLg9te4Ldp 21 | lR13F/r0IcUcZtsHg0jF6TgbTjYM2ChhgggTknhMsUmlul59Qyk5459+tXttK3iE 22 | ZBue3ER+yPV0avjEYyWXrAqjOPmD6FnANV0aRJnapoxn02JodAYUcBTeQEyAvpH3 23 | ov9yvpFlrQMXCsyG9ScFIuE0s1aG6srxDqM+6hg4fsnV2StLDNSBQ+GK3lxvEAHU 24 | kuPExYdly1Hl9suL+RV5P+a8x5l1lQTPsCZc+xDoGv17fvSObWlopF036zmiLLdu 25 | HW8ENtlRR8lFaYPj0Gzl4VObc9nZo3WXOQQGBC7JaIzNeLQvjhokU/yuO2bK7DoO 26 | TrWXqkmZc/xtqvjFXs3rv9xKZuFhWpouRpda6QOsf5s= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /cmd/gateway/ssl/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEWTCCAkECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0B 3 | AQEFAAOCAg8AMIICCgKCAgEArv8qeXm2pwG/v1IpFAQFjVu9xIq0Vf+z98bzD4qW 4 | vsO+LccYSKuFfRyK0ZfmrdjxbTeI5leHb8pWDfO9jp4BDTRzi5s8VsDysKPYjkSm 5 | y3/1r680RKZRozi9ZVNnH6I+eVzcLIExfFRI6Uj4OjQfIfnU0EKQxmCOFrvbb32H 6 | Ydes7mx55uOD98l5BV+wQwNt0gEn7HTDlhH8M13tV5cAHkSkLFMxC98qsbjGwnqa 7 | tMuHSZmTYaUT+GrvLe/FdWJHs6v4abKrNlrpvwNPFhUrHTxsoxiD4SY1Fu6rQTqV 8 | +JscExKVhShhFJ7CWC0VPwaqS+bIXWy9s6uzCmrj0E9cS3dTxScFrm5SRmvyT1oO 9 | K0RpyZTz/xW7ezL3GpUDj2mcSDoF+EEFivCXt6rHorFOq36BXN58M1ih8C+RzxF2 10 | sDwqZp7AUe/jmN6Kc0tRvOKT1O08I0idaCw34BnPfYy2n2z7IDo8/odYm3Li5ZN4 11 | OKdX4izJzjyyLYG3lMTunFVUMD2QmA2051eutW+cekVTXFkIUfw34JMgz9hinTWC 12 | TB+vCE1rckThnNWWZb0unEas3tBlJuXHqu57LejccGBMV6pwgl0Ur2Hiv5X+tl6W 13 | 87GIfbP1UQUGQFn0G90Te/86YwpDTtHRvNoF/TzH8BebViY9zNOYTWH2+W6Fz3Jb 14 | o+cCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQCQ2MHW8swvu8yPwnGoRgyP+sF8 15 | qeRtIOwBEIu7Hn13+TGH0VuGZwtibXj8Zdh4aw/600LXJhPLgqe4SvGDWR8xZr3K 16 | g5BlbFyQdgC7S1kMzDcok/sirz47AsdT2RVQNkd+PrOmAUD/Izcvf9VjorQwZ7Ma 17 | f9kI1LmtBI1csOyyo/79Z2YcT5InUK8i7bpRpwLB2Kyw4s4NCWV0hbsce90LpXL/ 18 | jfYxxJ6+Ctipi3BkqLWhoSbCX9/2uYj7C+FWW4ouEBUVODnKgSDt0Ppb3qLKRqjV 19 | lQ2xr5zZ95uUsE8G6Eh3DERRc3c+U2MpBM0U0nIrrFid4WYHNRvk1lgY8OlwV+XW 20 | fsduAvSsUqfxslKSuwYQBGsFLeFGaX39kVxu8kKxwXL/3THrISpEURiS5oGo5Pwb 21 | F5ZseBs99KiK2EozUB9mYWeqXibrq9FnQaU4bXYgGzESjGIOqUhMl7TUpaYGFYAc 22 | hLs4EHCr5ecsuEWT7+yZ5lIU2EY1eS1T0ezXaZ7nzGqHpxnR4E7Ze6hWmFgOFnQ7 23 | Dok5fUrGpfA9mGmeBp/gawGxcZUWqCyU26aI/2bKn2Q0BpLsyfdXlrWtCNTSZXqG 24 | U6B6LCv0lkczUZRgqcCh5KezVUdCDKCmPfW4CKFnRidCTeJB+tAg+zTJ3SzJhVxh 25 | +ZR7osIOUwQ9bV9bgA== 26 | -----END CERTIFICATE REQUEST----- 27 | -------------------------------------------------------------------------------- /cmd/gateway/ssl/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,87CA6E9761738737 4 | 5 | 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 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /cmd/gateway/ssl/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCu/yp5ebanAb+/ 3 | UikUBAWNW73EirRV/7P3xvMPipa+w74txxhIq4V9HIrRl+at2PFtN4jmV4dvylYN 4 | 872OngENNHOLmzxWwPKwo9iORKbLf/WvrzREplGjOL1lU2cfoj55XNwsgTF8VEjp 5 | SPg6NB8h+dTQQpDGYI4Wu9tvfYdh16zubHnm44P3yXkFX7BDA23SASfsdMOWEfwz 6 | Xe1XlwAeRKQsUzEL3yqxuMbCepq0y4dJmZNhpRP4au8t78V1Ykezq/hpsqs2Wum/ 7 | A08WFSsdPGyjGIPhJjUW7qtBOpX4mxwTEpWFKGEUnsJYLRU/BqpL5shdbL2zq7MK 8 | auPQT1xLd1PFJwWublJGa/JPWg4rRGnJlPP/Fbt7MvcalQOPaZxIOgX4QQWK8Je3 9 | qseisU6rfoFc3nwzWKHwL5HPEXawPCpmnsBR7+OY3opzS1G84pPU7TwjSJ1oLDfg 10 | Gc99jLafbPsgOjz+h1ibcuLlk3g4p1fiLMnOPLItgbeUxO6cVVQwPZCYDbTnV661 11 | b5x6RVNcWQhR/DfgkyDP2GKdNYJMH68ITWtyROGc1ZZlvS6cRqze0GUm5ceq7nst 12 | 6NxwYExXqnCCXRSvYeK/lf62XpbzsYh9s/VRBQZAWfQb3RN7/zpjCkNO0dG82gX9 13 | PMfwF5tWJj3M05hNYfb5boXPcluj5wIDAQABAoICAGxZPnIh4u46Tf9mHlrvNoTU 14 | VYWYH5nTjRkPjDSGKVmJlnl/eNCdqjPRdT3aIBBpbumhZdjAPvH/RBK1ZBsmRx5m 15 | 6Fd60Pwh0OF/QLitVtxOqM6S2vjaRVea7OEMp5/Vc3ru/L9tajdBdDdEX7Shytjd 16 | d19Op5a7952pyG6aOTViNQcWEM/c9Dt5+b1TBq4nNFq/Lnnfva1wC0e+kxIVQxR6 17 | iW+MPqTMEwIglqeZA6qHDYeLV84wSFIZ22cEuqw4Io7jHhZLcmhgF7OVl+TjTGN4 18 | owbESfsgDeTZ9xyW6n4Xg2Vnrz3Xx2vd9BRmLW5tcQk3pbz6c+YHm/4KOm0ihdCJ 19 | mX1PyQWFgsqCb5uwiA1DSlkmJAnRZr64MK6fPBpqTMvYZFuGX7ClVbUf0IUrUc8L 20 | lubVQVo0ctkEz5oAB5AWTaby3dgJh96Re7URVG8emiWqzl3kONp9H9f/as/u1dwX 21 | bOnVnz681gsv2Fd278ZeoG79Y3I3FJAS3VntwkSFaFWobqMqDzPyl3V0U5hl7eNF 22 | kShCzgGy9MjRJykWJHqvAkqKWE5vHY2B3lT2oiWWs2rfNf4H3ZmmIJ3yxQqtgpj1 23 | PeuVtImsbFkelJ7FgcZTBvzjk8ls73iLI/TwFy5q7mfbl68JuLuSMzhkYjwSwRIV 24 | rRz6wbI93zQ93N9Li56hAoIBAQDbPI2mZcyCD7mov4qZRhogy7FeZkmm3TvF101Q 25 | l4cI74Xh5ZhKulvnpiKw3KRm5hNErxxmBWtuHiszy268wB7Hy2MHN9veLtkrnrYW 26 | Y7tJ448xWbGkoO3PhR2ZTEHrY/kgN94GcAnqirfNfyPuABwvhTH7PEvNl6df7T4/ 27 | Bp9B/k/Mgv/W3+lU31R/Pp7kQp0kESgQ3PSI4EDxgzd2UGL/L5MDhq6hPMGmemAD 28 | eWEuUFoKBCjEmPMBV19w667rogr+GWQTs5Q7fjfLfpvHhTQVFJmBJ40dgsigWD2O 29 | XEztSKlQq1K+1x9XinaPOE/pE+KQwjG7rj5bRRKY6PzZj0XxAoIBAQDMV3l2EuS9 30 | WcKwuY81Lx6MxaFZPTzfK8WbOLRg+m3MZ+bifne60UTE7jXFLu28o6t0Amy1TobS 31 | Nk+elLNSXNdIibSUxWGmoWLKW2YCiv11H4LHsD7C2fg4aCe4ykBE/fbm2OP3xSFO 32 | DomPhpLlHN/7MKnCb6BV1mYCnW6PRHuizyXUUsAAJ+yEdq/CLzqakfDxskBvt1U+ 33 | I/4V60AjFOjIfnBes55ZtX7RVWdZ2s5I8MuYgDBrZCVlN53EV6iqCZ5+3AsnYR9i 34 | MG8EHAWn9H6nrkTft2S/YFroczGeECGTDxOFWHL6J48GUQmfpVAqNN/FqdJFztXc 35 | 86p50oR57c9XAoIBAAeLItiZ4hqLjfjXdl5lwhpkx9qWrGwYaqkZCmj+fb/59eJa 36 | 8w2HMghbeBAb13BO9FTFrbV49ERat2PV2R6XgbegMO0bp+4SMJjGoj9M+THGqbOq 37 | AWnHu8cLLoSK0i3WR8krAvG3wy+VzFM+z66txEqCS8D4Rydw02qCkES85pfvtzEz 38 | RLhRBgdMk+y46+CwxhVqnbVO1P8ssC9fM3ZXt8BOE8HoTXPw0jz3MvyLnaNpUl/L 39 | 2tdA6AOgzQDmqyMidTxfBUmEQVJ2ula/KX8I/0zGY63E/Z8JU/vobV3LoT385Q6I 40 | hpVYc5SgiAnPhzhe3cnbVLi/exfK3aPzSFf4DRECggEAOu+H0qxEWPCInJwIV9iN 41 | QSDHFov5AI0JY2mJbOcNwAS/rNe5BKLS6NGjpImSe/PgwYbpf6l0bTDI6f4NclXg 42 | qcU91iLPtVWavq55z4Xy8FL0T0/f6icf1nUD7+I1gzCUg6ROq+mpX3AF8OVpfLdF 43 | SpeQfsnfPqKlrV9mwd+IoT6JtKFhSyo4p0PsEV+50VcCXVOCDjacWV3cLMo7J2Mi 44 | eKEvG8FGjK0yNIIjWuEm2hbJQeTHElJCDt1pnXWC6ff4RLKGbQcwHAvIGTFT2EtG 45 | NGqo5rVoU2IhFiYTS3lvlZClmrIWB1xdff7lfawaYPH7EHGYehXbzOxiYNx85QXX 46 | 5QKCAQAtKVdVlN23c8Ku37rI/lbwlM1jYr5vjlCXJKjWRvABRfOZSmZPXnri0kxJ 47 | sbO9OdJaLPwDg2UbCKTHcuz54YyufxHWT7Gt0ytG0kdnGPpoMdKgVIXUItWnDH2F 48 | afVKpU/ZXkp+wwZnD3D+2cUKf+vph2fIdk2gfShDrv8N3dmfKHfk6Q5WQPuZy8Rw 49 | kLRexy2iA4D4BzAIGa9n/1lhbdbRvBgEMWTAanDrPk/3Lu1O7zIawY5gWZeYTCio 50 | gex6MWAXEQ51hgwg8bVyuMHVq3lIV9D5nw9uOJRetnq3joFwvtRkS3KvtAgUFv4A 51 | RAtdTFXd7PYGUT1NS3KZM80+xRVB 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /cmd/hotels/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | MAINTAINER Badu 4 | 5 | RUN apk update \ 6 | && apk add --no-cache ca-certificates git make 7 | 8 | WORKDIR /hotels_workdir 9 | 10 | COPY . . 11 | 12 | RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o ./build/service github.com/badu/microservices-demo/cmd/hotels 13 | 14 | FROM alpine:3.17 15 | 16 | RUN apk update \ 17 | && apk add --no-cache ca-certificates tzdata && \ 18 | addgroup -S badu && adduser -S badu -G badu 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=builder /hotels_workdir/build/service /app 23 | 24 | USER badu 25 | 26 | CMD /app/service 27 | -------------------------------------------------------------------------------- /cmd/hotels/Makefile: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # Go migrate postgresql Hotels service 3 | # ============================================================================== 4 | 5 | hotels_dbname = hotels_db 6 | hotels_port = 5435 7 | hotels_SSL_MODE = disable 8 | 9 | force_hotels_db: 10 | migrate -database postgres://postgres:postgres@localhost:$(hotels_port)/$(hotels_dbname)?sslmode=$(hotels_SSL_MODE) -path migrations force 1 11 | 12 | version_hotels_db: 13 | migrate -database postgres://postgres:postgres@localhost:$(hotels_port)/$(hotels_dbname)?sslmode=$(hotels_SSL_MODE) -path migrations version 14 | 15 | migrate_hotels_db_up: 16 | migrate -database postgres://postgres:postgres@localhost:$(hotels_port)/$(hotels_dbname)?sslmode=$(hotels_SSL_MODE) -path migrations up 1 17 | 18 | migrate_hotels_db_down: 19 | migrate -database postgres://postgres:postgres@localhost:$(hotels_port)/$(hotels_dbname)?sslmode=$(hotels_SSL_MODE) -path migrations down 1 20 | 21 | -------------------------------------------------------------------------------- /cmd/hotels/config/config-docker.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5003" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | 18 | Rabbitmq: 19 | Host: localhost 20 | Port: 5672 21 | User: guest 22 | Password: guest 23 | 24 | HttpServer: 25 | Port: ":8007" 26 | PprofPort: ":8100" 27 | Timeout: 15 28 | ReadTimeout: 5 29 | WriteTimeout: 5 30 | CookieLifeTime: 44640 31 | SessionCookieName: "session_token" 32 | 33 | postgres: 34 | PostgresqlHost: localhost 35 | PostgresqlPort: 5435 36 | PostgresqlUser: postgres 37 | PostgresqlPassword: postgres 38 | PostgresqlDbname: hotels_db 39 | PostgresqlSslmode: "disable" 40 | PgDriver: pgx 41 | 42 | logger: 43 | Development: true 44 | DisableCaller: false 45 | DisableStacktrace: false 46 | Encoding: json 47 | Level: info 48 | PrintConfig: false 49 | 50 | redis: 51 | RedisAddr: localhost:6379 52 | RedisPassword: 53 | RedisDb: 0 54 | RedisDefaultDB: 0 55 | MinIdleConn: 200 56 | PoolSize: 12000 57 | PoolTimeout: 240 58 | Password: "" 59 | DB: 0 60 | 61 | metrics: 62 | Port: ":7073" 63 | Url: 0.0.0.0:7073 64 | ServiceName: hotels_microservice 65 | 66 | jaeger: 67 | Host: localhost:6831 68 | ServiceName: HotelsService_GRPC 69 | LogSpans: false 70 | 71 | -------------------------------------------------------------------------------- /cmd/hotels/config/config-local.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5055" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | 18 | Rabbitmq: 19 | Host: localhost 20 | Port: 5672 21 | User: guest 22 | Password: guest 23 | 24 | HttpServer: 25 | Port: ":8007" 26 | PprofPort: ":8100" 27 | Timeout: 15 28 | ReadTimeout: 5 29 | WriteTimeout: 5 30 | CookieLifeTime: 44640 31 | SessionCookieName: "session_token" 32 | 33 | postgres: 34 | PostgresqlHost: localhost 35 | PostgresqlPort: 5435 36 | PostgresqlUser: postgres 37 | PostgresqlPassword: postgres 38 | PostgresqlDbname: hotels_db 39 | PostgresqlSslmode: "disable" 40 | PgDriver: pgx 41 | 42 | logger: 43 | Development: true 44 | DisableCaller: false 45 | DisableStacktrace: false 46 | Encoding: json 47 | Level: info 48 | PrintConfig: false 49 | 50 | redis: 51 | RedisAddr: localhost:6379 52 | RedisPassword: 53 | RedisDb: 0 54 | RedisDefaultDB: 0 55 | MinIdleConn: 200 56 | PoolSize: 12000 57 | PoolTimeout: 240 58 | Password: "" 59 | DB: 0 60 | 61 | metrics: 62 | Port: ":7075" 63 | Url: 0.0.0.0:7075 64 | ServiceName: hotels_microservice 65 | 66 | jaeger: 67 | Host: localhost:6831 68 | ServiceName: HotelsService_GRPC 69 | LogSpans: false 70 | 71 | -------------------------------------------------------------------------------- /cmd/hotels/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | 9 | "github.com/badu/microservices-demo/app/hotels" 10 | "github.com/badu/microservices-demo/pkg/config" 11 | "github.com/badu/microservices-demo/pkg/jaeger" 12 | "github.com/badu/microservices-demo/pkg/logger" 13 | "github.com/badu/microservices-demo/pkg/postgres" 14 | "github.com/badu/microservices-demo/pkg/redis" 15 | ) 16 | 17 | func main() { 18 | configPath := config.GetConfigPath(os.Getenv("config")) 19 | cfg, err := config.GetConfig(configPath) 20 | if err != nil { 21 | log.Fatalf("Loading config: %v", err) 22 | } 23 | 24 | appLogger := logger.NewApiLogger(cfg.Logger) 25 | appLogger.InitLogger() 26 | appLogger.Info("Starting hotels microservice") 27 | appLogger.Infof( 28 | "AppVersion: %s, LogLevel: %s, Mode: %s", 29 | cfg.GRPCServer.AppVersion, 30 | cfg.Logger.Level, 31 | cfg.GRPCServer.Mode, 32 | ) 33 | appLogger.Infof("Success parsed config: %#v", cfg.GRPCServer.AppVersion) 34 | 35 | pgxConn, err := postgres.NewPgxConn(cfg.Postgres) 36 | if err != nil { 37 | appLogger.Fatal("cannot connect to postgres", err) 38 | } 39 | defer pgxConn.Close() 40 | 41 | redisClient := redis.NewRedisClient(cfg.Redis) 42 | appLogger.Info("connected to Redis") 43 | 44 | tracer, closer, err := jaeger.InitJaeger(cfg.Jaeger) 45 | if err != nil { 46 | appLogger.Fatal("cannot create tracer", err) 47 | } 48 | appLogger.Info("connected to Jaeger") 49 | 50 | opentracing.SetGlobalTracer(tracer) 51 | defer closer.Close() 52 | appLogger.Info("connected to Opentracing") 53 | 54 | appLogger.Infof("%-v", pgxConn.Stat()) 55 | appLogger.Infof("%-v", redisClient.PoolStats()) 56 | 57 | app := hotels.NewApplication(&appLogger, cfg, redisClient, pgxConn, tracer) 58 | appLogger.Fatal(app.Run()) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/hotels/migrations/01_create_initial_tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS hotels CASCADE; 2 | DROP TRIGGER IF EXISTS hotel_updated_at_trigger ON hotels; 3 | DROP FUNCTION IF EXISTS hotel_updated; 4 | DROP EXTENSION IF EXISTS citext CASCADE; 5 | DROP EXTENSION IF EXISTS POSTGIS CASCADE; 6 | DROP EXTENSION IF EXISTS pg_trgm CASCADE; 7 | DROP EXTENSION IF EXISTS btree_gist CASCADE; 8 | DROP EXTENSION IF EXISTS "uuid-ossp" CASCADE; -------------------------------------------------------------------------------- /cmd/hotels/migrations/01_create_initial_tables.up.sql: -------------------------------------------------------------------------------- 1 | -- ALTER SYSTEM SET shared_buffers = '128MB'; 2 | CREATE EXTENSION IF NOT EXISTS citext; 3 | CREATE EXTENSION IF NOT EXISTS POSTGIS; 4 | CREATE EXTENSION IF NOT EXISTS pg_trgm; 5 | CREATE EXTENSION IF NOT EXISTS btree_gist; 6 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 7 | DROP TABLE IF EXISTS hotels CASCADE; 8 | 9 | create table hotels 10 | ( 11 | hotel_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 12 | name citext NOT NULL CHECK ( name <> '' ), 13 | email citext NOT NULL CHECK ( email <> '' ), 14 | location citext NOT NULL CHECK ( location <> '' ), 15 | country citext NOT NULL CHECK ( country <> '' ), 16 | city citext NOT NULL CHECK ( city <> '' ), 17 | coordinates geometry(POINT, 4326), 18 | description text, 19 | image text, 20 | photos text[], 21 | rating float DEFAULT 0 CHECK (rating >= 0 AND rating <= 10), 22 | comments_count int DEFAULT 0 CHECK (comments_count >= 0), 23 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, 24 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 25 | ); 26 | 27 | CREATE INDEX if not exists hotels_gist_idx ON hotels USING gist (coordinates); 28 | 29 | CREATE INDEX hotels_name_trgm_idx ON hotels 30 | USING gist (name); 31 | 32 | CREATE INDEX hotels_location_trgm_idx ON hotels 33 | USING gist (location); 34 | 35 | CREATE OR REPLACE FUNCTION hotel_updated() RETURNS TRIGGER AS 36 | $$ 37 | BEGIN 38 | NEW.updated_at = CURRENT_TIMESTAMP; 39 | RETURN NEW; 40 | END; 41 | $$ LANGUAGE plpgsql; 42 | 43 | CREATE TRIGGER hotel_updated_at_trigger 44 | BEFORE INSERT OR UPDATE 45 | ON hotels 46 | FOR EACH ROW 47 | EXECUTE PROCEDURE hotel_updated(); -------------------------------------------------------------------------------- /cmd/images/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | MAINTAINER Badu 4 | 5 | RUN apk update \ 6 | && apk add --no-cache ca-certificates git make 7 | 8 | WORKDIR /images_workdir 9 | 10 | COPY . . 11 | 12 | RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o ./build/service github.com/badu/microservices-demo/cmd/images 13 | 14 | FROM alpine:3.17 15 | 16 | RUN apk update \ 17 | && apk add --no-cache ca-certificates tzdata && \ 18 | addgroup -S badu && adduser -S badu -G badu 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=builder /images_workdir/build/service /app 23 | 24 | USER badu 25 | 26 | CMD /app/service 27 | -------------------------------------------------------------------------------- /cmd/images/Makefile: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # Go migrate postgresql Images service 3 | # ============================================================================== 4 | 5 | images_dbname = images_db 6 | images_port = 5434 7 | images_SSL_MODE = disable 8 | 9 | force_images_db: 10 | migrate -database postgres://postgres:postgres@localhost:$(images_port)/$(images_dbname)?sslmode=$(images_SSL_MODE) -path migrations force 1 11 | 12 | version_images_db: 13 | migrate -database postgres://postgres:postgres@localhost:$(images_port)/$(images_dbname)?sslmode=$(images_SSL_MODE) -path migrations version 14 | 15 | migrate_images_db_up: 16 | migrate -database postgres://postgres:postgres@localhost:$(images_port)/$(images_dbname)?sslmode=$(images_SSL_MODE) -path migrations up 1 17 | 18 | migrate_images_db_down: 19 | migrate -database postgres://postgres:postgres@localhost:$(images_port)/$(images_dbname)?sslmode=$(images_SSL_MODE) -path migrations down 1 20 | -------------------------------------------------------------------------------- /cmd/images/config/config-docker.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5002" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | 18 | rabbitmq: 19 | Host: localhost 20 | Port: 5672 21 | User: guest 22 | Password: guest 23 | Exchange: emails-exchange 24 | Queue: emails-queue 25 | RoutingKey: emails-routing-key 26 | ConsumerTag: emails-consumer 27 | WorkerPoolSize: 24 28 | 29 | AWS: 30 | S3Region: "us-east-1" 31 | S3EndPointMinio: "http://localhost:9000/minio" 32 | S3EndPoint: "http://localhost:9000" 33 | DisableSSL: true 34 | S3ForcePathStyle: true 35 | 36 | HttpServer: 37 | Port: ":8007" 38 | PprofPort: ":8100" 39 | Timeout: 15 40 | ReadTimeout: 5 41 | WriteTimeout: 5 42 | CookieLifeTime: 44640 43 | SessionCookieName: "session_token" 44 | 45 | postgres: 46 | PostgresqlHost: localhost 47 | PostgresqlPort: 5434 48 | PostgresqlUser: postgres 49 | PostgresqlPassword: postgres 50 | PostgresqlDbname: images_db 51 | PostgresqlSslmode: "disable" 52 | PgDriver: pgx 53 | 54 | logger: 55 | Development: true 56 | DisableCaller: false 57 | DisableStacktrace: false 58 | Encoding: json 59 | Level: info 60 | PrintConfig: false 61 | 62 | redis: 63 | RedisAddr: localhost:6379 64 | RedisPassword: 65 | RedisDb: 0 66 | RedisDefaultDB: 0 67 | MinIdleConn: 200 68 | PoolSize: 12000 69 | PoolTimeout: 240 70 | Password: "" 71 | DB: 0 72 | 73 | metrics: 74 | Url: 0.0.0.0:7072 75 | ServiceName: images_microservice 76 | 77 | jaeger: 78 | Host: localhost:6831 79 | ServiceName: ImagesService_GRPC 80 | LogSpans: false 81 | -------------------------------------------------------------------------------- /cmd/images/config/config-local.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5050" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | 18 | rabbitmq: 19 | Host: localhost 20 | Port: 5672 21 | User: guest 22 | Password: guest 23 | Exchange: emails-exchange 24 | Queue: emails-queue 25 | RoutingKey: emails-routing-key 26 | ConsumerTag: emails-consumer 27 | WorkerPoolSize: 24 28 | 29 | AWS: 30 | S3Region: "us-east-1" 31 | S3EndPointMinio: "http://localhost:9000/minio" 32 | S3EndPoint: "http://localhost:9000" 33 | DisableSSL: true 34 | S3ForcePathStyle: true 35 | 36 | HttpServer: 37 | Port: ":8007" 38 | PprofPort: ":8100" 39 | Timeout: 15 40 | ReadTimeout: 5 41 | WriteTimeout: 5 42 | CookieLifeTime: 44640 43 | SessionCookieName: "session_token" 44 | 45 | postgres: 46 | PostgresqlHost: localhost 47 | PostgresqlPort: 5434 48 | PostgresqlUser: postgres 49 | PostgresqlPassword: postgres 50 | PostgresqlDbname: images_db 51 | PostgresqlSslmode: "disable" 52 | PgDriver: pgx 53 | 54 | logger: 55 | Development: true 56 | DisableCaller: false 57 | DisableStacktrace: false 58 | Encoding: json 59 | Level: info 60 | PrintConfig: false 61 | 62 | redis: 63 | RedisAddr: localhost:6379 64 | RedisPassword: 65 | RedisDb: 0 66 | RedisDefaultDB: 0 67 | MinIdleConn: 200 68 | PoolSize: 12000 69 | PoolTimeout: 240 70 | Password: "" 71 | DB: 0 72 | 73 | metrics: 74 | Url: 0.0.0.0:7074 75 | ServiceName: images_microservice 76 | 77 | jaeger: 78 | Host: localhost:6831 79 | ServiceName: ImagesService_GRPC 80 | LogSpans: false 81 | -------------------------------------------------------------------------------- /cmd/images/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | 9 | "github.com/badu/microservices-demo/app/images" 10 | "github.com/badu/microservices-demo/pkg/config" 11 | "github.com/badu/microservices-demo/pkg/jaeger" 12 | "github.com/badu/microservices-demo/pkg/logger" 13 | "github.com/badu/microservices-demo/pkg/postgres" 14 | "github.com/badu/microservices-demo/pkg/rabbitmq" 15 | ) 16 | 17 | func main() { 18 | configPath := config.GetConfigPath(os.Getenv("config")) 19 | cfg, err := config.GetConfig(configPath) 20 | if err != nil { 21 | log.Fatalf("Loading config: %v", err) 22 | } 23 | 24 | appLogger := logger.NewApiLogger(cfg.Logger) 25 | appLogger.InitLogger() 26 | appLogger.Info("Starting images microservice") 27 | appLogger.Infof( 28 | "AppVersion: %s, LogLevel: %s, Mode: %s", 29 | cfg.GRPCServer.AppVersion, 30 | cfg.Logger.Level, 31 | cfg.GRPCServer.Mode, 32 | ) 33 | appLogger.Infof("Success parsed config: %#v", cfg.GRPCServer.AppVersion) 34 | 35 | pgxConn, err := postgres.NewPgxConn(cfg.Postgres) 36 | if err != nil { 37 | appLogger.Fatal("cannot connect to postgres", err) 38 | } 39 | defer pgxConn.Close() 40 | 41 | tracer, closer, err := jaeger.InitJaeger(cfg.Jaeger) 42 | if err != nil { 43 | appLogger.Fatal("cannot create tracer", err) 44 | } 45 | appLogger.Info("connected to Jaeger") 46 | 47 | opentracing.SetGlobalTracer(tracer) 48 | defer closer.Close() 49 | appLogger.Info("connected to Opentracing") 50 | 51 | amqpConn, err := rabbitmq.NewRabbitMQConn(cfg.RabbitMQ) 52 | if err != nil { 53 | appLogger.Fatal(err) 54 | } 55 | defer amqpConn.Close() 56 | 57 | s3 := images.NewS3Session(cfg) 58 | appLogger.Infof("connected to AWS S3 : %v", s3.Client.APIVersion) 59 | 60 | app := images.NewApplication(&appLogger, cfg, tracer, pgxConn, s3) 61 | appLogger.Fatal(app.Run()) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/images/migrations/01_create_initial_tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS images CASCADE; 2 | DROP TRIGGER IF EXISTS image_updated_at_trigger ON images; 3 | DROP FUNCTION IF EXISTS image_updated; -------------------------------------------------------------------------------- /cmd/images/migrations/01_create_initial_tables.up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS images CASCADE; 2 | 3 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 4 | CREATE EXTENSION IF NOT EXISTS CITEXT; 5 | 6 | CREATE TABLE images 7 | ( 8 | image_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 9 | image_url VARCHAR(250) NOT NULL CHECK ( image_url <> '' ), 10 | is_uploaded bool DEFAULT false, 11 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, 12 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 13 | ); 14 | 15 | CREATE OR REPLACE FUNCTION image_updated() RETURNS TRIGGER AS 16 | $$ 17 | BEGIN 18 | NEW.updated_at = CURRENT_TIMESTAMP; 19 | RETURN NEW; 20 | END; 21 | $$ LANGUAGE plpgsql; 22 | 23 | CREATE TRIGGER image_updated_at_trigger 24 | BEFORE INSERT OR UPDATE 25 | ON images 26 | FOR EACH ROW 27 | EXECUTE PROCEDURE image_updated(); -------------------------------------------------------------------------------- /cmd/sessions/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | MAINTAINER Badu 4 | 5 | RUN apk update \ 6 | && apk add --no-cache ca-certificates git make 7 | 8 | WORKDIR /sessions_workdir 9 | 10 | COPY . . 11 | 12 | RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o ./build/service github.com/badu/microservices-demo/cmd/sessions 13 | 14 | FROM alpine:3.17 15 | 16 | RUN apk update \ 17 | && apk add --no-cache ca-certificates tzdata && \ 18 | addgroup -S badu && adduser -S badu -G badu 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=builder /sessions_workdir/build/service /app 23 | 24 | USER badu 25 | 26 | CMD /app/service 27 | -------------------------------------------------------------------------------- /cmd/sessions/config/config-docker.yaml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5000" 4 | CookieLifeTime: 44640 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | CsrfSalt: "KbWaoi5xtDC3GEfBa9ovQdzOzXsuVU9I" 17 | 18 | logger: 19 | Development: true 20 | DisableCaller: false 21 | DisableStacktrace: false 22 | Encoding: json 23 | Level: info 24 | PrintConfig: false 25 | 26 | postgres: 27 | PostgresqlHost: localhost 28 | PostgresqlPort: 5433 29 | PostgresqlUser: postgres 30 | PostgresqlPassword: postgres 31 | PostgresqlDbname: users_db 32 | PostgresqlSslmode: "disable" 33 | PgDriver: pgx 34 | 35 | redis: 36 | RedisAddr: localhost:6379 37 | RedisPassword: 38 | RedisDb: 0 39 | RedisDefaultDB: 0 40 | MinIdleConn: 200 41 | PoolSize: 12000 42 | PoolTimeout: 240 43 | Password: "" 44 | DB: 0 45 | 46 | metrics: 47 | Port: ":7070" 48 | Url: 0.0.0.0:7070 49 | ServiceName: sessions_microservice 50 | 51 | jaeger: 52 | Host: localhost:6831 53 | ServiceName: SessionsSrvice_GRPC 54 | LogSpans: false 55 | -------------------------------------------------------------------------------- /cmd/sessions/config/config-local.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5000" 4 | CookieLifeTime: 44640 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | CsrfSalt: "KbWaoi5xtDC3GEfBa9ovQdzOzXsuVU9I" 17 | 18 | logger: 19 | Development: true 20 | DisableCaller: false 21 | DisableStacktrace: false 22 | Encoding: json 23 | Level: info 24 | PrintConfig: false 25 | 26 | postgres: 27 | PostgresqlHost: localhost 28 | PostgresqlPort: 5433 29 | PostgresqlUser: postgres 30 | PostgresqlPassword: postgres 31 | PostgresqlDbname: users_db 32 | PostgresqlSslmode: "disable" 33 | PgDriver: pgx 34 | 35 | redis: 36 | RedisAddr: localhost:6379 37 | RedisPassword: 38 | RedisDb: 0 39 | RedisDefaultDB: 0 40 | MinIdleConn: 200 41 | PoolSize: 12000 42 | PoolTimeout: 240 43 | Password: "" 44 | DB: 0 45 | 46 | metrics: 47 | Port: ":7070" 48 | Url: 0.0.0.0:7070 49 | ServiceName: sessions_microservice 50 | 51 | jaeger: 52 | Host: localhost:6831 53 | ServiceName: SessionsSrvice_GRPC 54 | LogSpans: false 55 | -------------------------------------------------------------------------------- /cmd/sessions/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | 9 | "github.com/badu/microservices-demo/app/sessions" 10 | "github.com/badu/microservices-demo/pkg/config" 11 | "github.com/badu/microservices-demo/pkg/jaeger" 12 | "github.com/badu/microservices-demo/pkg/logger" 13 | "github.com/badu/microservices-demo/pkg/redis" 14 | ) 15 | 16 | func main() { 17 | configPath := config.GetConfigPath(os.Getenv("config")) 18 | cfg, err := config.GetConfig(configPath) 19 | if err != nil { 20 | log.Fatalf("Loading config: %v", err) 21 | } 22 | 23 | appLogger := logger.NewApiLogger(cfg.Logger) 24 | appLogger.InitLogger() 25 | appLogger.Info("Starting sessions server") 26 | appLogger.Infof( 27 | "AppVersion: %s, LogLevel: %s, Mode: %s", 28 | cfg.GRPCServer.AppVersion, 29 | cfg.Logger.Level, 30 | cfg.GRPCServer.Mode, 31 | ) 32 | appLogger.Infof("Success parsed config: %#v", cfg.GRPCServer.AppVersion) 33 | 34 | redisClient := redis.NewRedisClient(cfg.Redis) 35 | appLogger.Info("connected to Redis") 36 | 37 | tracer, closer, err := jaeger.InitJaeger(cfg.Jaeger) 38 | if err != nil { 39 | appLogger.Fatal("cannot create tracer", err) 40 | } 41 | appLogger.Info("connected to Jaeger") 42 | 43 | opentracing.SetGlobalTracer(tracer) 44 | defer closer.Close() 45 | appLogger.Info("connected to Opentracing") 46 | 47 | appLogger.Infof("%-v", redisClient.PoolStats()) 48 | 49 | app := sessions.NewApplication(&appLogger, cfg, redisClient, tracer) 50 | appLogger.Fatal(app.Run()) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/users/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | MAINTAINER Badu 4 | 5 | RUN apk update \ 6 | && apk add --no-cache ca-certificates git make 7 | 8 | WORKDIR /users_workdir 9 | 10 | COPY . . 11 | 12 | RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o ./build/service github.com/badu/microservices-demo/cmd/users 13 | 14 | FROM alpine:3.17 15 | 16 | RUN apk update \ 17 | && apk add --no-cache ca-certificates tzdata && \ 18 | addgroup -S badu && adduser -S badu -G badu 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=builder /users_workdir/build/service /app 23 | 24 | USER badu 25 | 26 | CMD /app/service 27 | -------------------------------------------------------------------------------- /cmd/users/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: 2 | 3 | # ============================================================================== 4 | # Go migrate postgresql user service 5 | # ============================================================================== 6 | 7 | user_dbname = users_db 8 | user_port = 5433 9 | user_SSL_MODE = disable 10 | 11 | force_users_db: 12 | migrate -database postgres://postgres:postgres@localhost:$(user_port)/$(user_dbname)?sslmode=$(user_SSL_MODE) -path migrations force 1 13 | 14 | version_users_db: 15 | migrate -database postgres://postgres:postgres@localhost:$(user_port)/$(user_dbname)?sslmode=$(user_SSL_MODE) -path migrations version 16 | 17 | migrate_users_db_up: 18 | migrate -database postgres://postgres:postgres@localhost:$(user_port)/$(user_dbname)?sslmode=$(user_SSL_MODE) -path migrations up 1 19 | 20 | migrate_users_db_down: 21 | migrate -database postgres://postgres:postgres@localhost:$(user_port)/$(user_dbname)?sslmode=$(user_SSL_MODE) -path migrations down 1 22 | 23 | # ============================================================================== 24 | # Make local SSL Certificate 25 | # ============================================================================== 26 | 27 | make_cert: 28 | echo "Generating SSL certificates" 29 | sh ./user/ssl/instructions.sh 30 | 31 | -------------------------------------------------------------------------------- /cmd/users/config/config-docker.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5001" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | 18 | HttpServer: 19 | Port: ":8081" 20 | PprofPort: ":8100" 21 | Timeout: 15 22 | ReadTimeout: 5 23 | WriteTimeout: 5 24 | CookieLifeTime: 44640 25 | SessionCookieName: "session_token" 26 | 27 | rabbitmq: 28 | Host: localhost 29 | Port: 5672 30 | User: guest 31 | Password: guest 32 | Exchange: emails-exchange 33 | Queue: emails-queue 34 | RoutingKey: emails-routing-key 35 | ConsumerTag: emails-consumer 36 | WorkerPoolSize: 24 37 | 38 | logger: 39 | Development: true 40 | DisableCaller: false 41 | DisableStacktrace: false 42 | Encoding: json 43 | Level: info 44 | PrintConfig: false 45 | 46 | postgres: 47 | PostgresqlHost: localhost 48 | PostgresqlPort: 5433 49 | PostgresqlUser: postgres 50 | PostgresqlPassword: postgres 51 | PostgresqlDbname: users_db 52 | PostgresqlSslmode: "disable" 53 | PgDriver: pgx 54 | 55 | redis: 56 | RedisAddr: localhost:6379 57 | RedisPassword: 58 | RedisDb: 0 59 | RedisDefaultDB: 0 60 | MinIdleConn: 200 61 | PoolSize: 12000 62 | PoolTimeout: 240 63 | Password: "" 64 | DB: 0 65 | 66 | metrics: 67 | Url: 0.0.0.0:7071 68 | ServiceName: user_microservice 69 | 70 | jaeger: 71 | Host: localhost:6831 72 | ServiceName: UserService_GRPC 73 | LogSpans: false 74 | -------------------------------------------------------------------------------- /cmd/users/config/config-local.yml: -------------------------------------------------------------------------------- 1 | GRPCServer: 2 | AppVersion: 1.0.0 3 | Port: ":5001" 4 | CookieLifeTime: 360 5 | CsrfExpire: 15 6 | SessionExpire: 60 7 | SessionID: "SessionID" 8 | Mode: "Development" 9 | Timeout: 15 10 | ReadTimeout: 5 11 | WriteTimeout: 5 12 | MaxConnectionIdle: 5 13 | MaxConnectionAge: 5 14 | SessionPrefix: "session" 15 | CSRFPrefix: "csrf" 16 | SessionGrpcServicePort: ":5000" 17 | 18 | HttpServer: 19 | Port: ":8081" 20 | PprofPort: ":8100" 21 | Timeout: 15 22 | ReadTimeout: 5 23 | WriteTimeout: 5 24 | CookieLifeTime: 44640 25 | SessionCookieName: "session_token" 26 | 27 | rabbitmq: 28 | Host: localhost 29 | Port: 5672 30 | User: guest 31 | Password: guest 32 | Exchange: emails-exchange 33 | Queue: emails-queue 34 | RoutingKey: emails-routing-key 35 | ConsumerTag: emails-consumer 36 | WorkerPoolSize: 24 37 | 38 | logger: 39 | Development: true 40 | DisableCaller: false 41 | DisableStacktrace: false 42 | Encoding: json 43 | Level: info 44 | PrintConfig: false 45 | 46 | postgres: 47 | PostgresqlHost: localhost 48 | PostgresqlPort: 5433 49 | PostgresqlUser: postgres 50 | PostgresqlPassword: postgres 51 | PostgresqlDbname: users_db 52 | PostgresqlSslmode: "disable" 53 | PgDriver: pgx 54 | 55 | redis: 56 | RedisAddr: localhost:6379 57 | RedisPassword: 58 | RedisDb: 0 59 | RedisDefaultDB: 0 60 | MinIdleConn: 200 61 | PoolSize: 12000 62 | PoolTimeout: 240 63 | Password: "" 64 | DB: 0 65 | 66 | metrics: 67 | Url: 0.0.0.0:7071 68 | ServiceName: user_microservice 69 | 70 | jaeger: 71 | Host: localhost:6831 72 | ServiceName: UserService_GRPC 73 | LogSpans: false 74 | -------------------------------------------------------------------------------- /cmd/users/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | 9 | "github.com/badu/microservices-demo/app/users" 10 | "github.com/badu/microservices-demo/pkg/config" 11 | "github.com/badu/microservices-demo/pkg/jaeger" 12 | "github.com/badu/microservices-demo/pkg/logger" 13 | "github.com/badu/microservices-demo/pkg/postgres" 14 | "github.com/badu/microservices-demo/pkg/redis" 15 | ) 16 | 17 | func main() { 18 | configPath := config.GetConfigPath(os.Getenv("config")) 19 | 20 | cfg, err := config.GetConfig(configPath) 21 | if err != nil { 22 | log.Fatalf("failed loading config: %v", err) 23 | } 24 | 25 | appLogger := logger.NewApiLogger(cfg.Logger) 26 | appLogger.InitLogger() 27 | appLogger.Info("starting user server") 28 | appLogger.Infof("LogLevel: %s, Mode: %s", cfg.Logger.Level, cfg.GRPCServer.Mode) 29 | appLogger.Infof("succeeded parsing config (app version: %#v", cfg.GRPCServer.AppVersion) 30 | 31 | pgxConn, err := postgres.NewPgxConn(cfg.Postgres) 32 | if err != nil { 33 | appLogger.Fatal("cannot connect to postgres ", err) 34 | } 35 | defer pgxConn.Close() 36 | 37 | redisClient := redis.NewRedisClient(cfg.Redis) 38 | appLogger.Info("connected to Redis") 39 | 40 | tracer, closer, err := jaeger.InitJaeger(cfg.Jaeger) 41 | if err != nil { 42 | appLogger.Fatal("cannot create tracer", err) 43 | } 44 | defer closer.Close() 45 | 46 | appLogger.Info("connected to Jaeger") 47 | 48 | opentracing.SetGlobalTracer(tracer) 49 | appLogger.Info("connected to Opentracing") 50 | 51 | appLogger.Infof("%#v", pgxConn.Stat()) 52 | appLogger.Infof("%#v", redisClient.PoolStats()) 53 | 54 | app := users.NewApplication(&appLogger, cfg, redisClient, pgxConn, tracer) 55 | 56 | appLogger.Fatal(app.Run()) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/users/migrations/01_create_initial_tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users CASCADE; 2 | DROP TYPE IF EXISTS role; 3 | DROP TRIGGER IF EXISTS user_updated_at_trigger ON users; 4 | DROP FUNCTION IF EXISTS user_updated; -------------------------------------------------------------------------------- /cmd/users/migrations/01_create_initial_tables.up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users CASCADE; 2 | 3 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 4 | CREATE EXTENSION IF NOT EXISTS CITEXT; 5 | -- CREATE EXTENSION IF NOT EXISTS postgis; 6 | -- CREATE EXTENSION IF NOT EXISTS postgis_topology; 7 | 8 | 9 | CREATE TYPE role AS ENUM ('admin', 'user', 'owner'); 10 | 11 | CREATE TABLE users 12 | ( 13 | user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 14 | first_name VARCHAR(60) NOT NULL CHECK ( first_name <> '' ), 15 | last_name VARCHAR(60) NOT NULL CHECK ( last_name <> '' ), 16 | email VARCHAR(64) UNIQUE NOT NULL CHECK ( email <> '' ), 17 | password TEXT NOT NULL CHECK ( octet_length(password) <> 0 ), 18 | role role DEFAULT 'user', 19 | avatar TEXT, 20 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, 21 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 22 | ); 23 | 24 | CREATE OR REPLACE FUNCTION user_updated() RETURNS TRIGGER AS 25 | $$ 26 | BEGIN 27 | NEW.updated_at = CURRENT_TIMESTAMP; 28 | RETURN NEW; 29 | END; 30 | $$ LANGUAGE plpgsql; 31 | 32 | CREATE TRIGGER user_updated_at_trigger 33 | BEFORE INSERT OR UPDATE 34 | ON users 35 | FOR EACH ROW 36 | EXECUTE PROCEDURE user_updated(); -------------------------------------------------------------------------------- /cmd/users/ssl/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFCTCCAvGgAwIBAgIUUdsQWoxSTY8J8chZJaT5MVxXHLwwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMDMyNDA5MjkxM1oXDTMxMDMy 4 | MjA5MjkxM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF 5 | AAOCAg8AMIICCgKCAgEAsV1qdNkC3p76AWiXKSk8nNWHHFlVlrZWOzWJVu7QanHv 6 | bYj5MIqhMW6K4Gq4znQxLQ3sdD+fjiUKfej4NGcP0cpUdmjZf3rbrK8JUXDt8HQ8 7 | Ar5sfnlB5mZfPeGzaiWXbO6Ml8DbOFCklld/m8SiT+plcVp1pdhcRDcrn5LeioBD 8 | RVoduHA+n7KbRV1/XalEiV0Dg0qaCinTnUAMlgMk4bEtEZzJdYDhpHvTQT/8G8yB 9 | aTBEQTbibTg2UOdi+OsZb1VXq6RWZX1GgdmAOwQAQxADmIUKDCI8oc+7CfcMGBtk 10 | 9YW0DPpBTpNP2cBGUJjEZkAZWT0cIp4gAVFZE1lEAHPqdNxZgJ7pG3F5JxuFqSky 11 | Axu72GQzbb5DBnAc9WGRo60KcnnMRaSof4P7rgqpLvRd1kjON8Qx+Sn2gez5JssA 12 | wMxiRLU8G7x+1BYfGw9Rnn6ZguRldJwn9oJptHcjT4T0AKSzA/zE9Dch9S346BB8 13 | 310xG9UCfX0+e8eCEh7FFx2NNql4d7Po/QRytKv66rF1BAKGAVbP/1ACp4PzkVLT 14 | IFQtdkdSuOeCD1aOkj6rNnpf9Xu+nIrhl/GiF0gpYyDaZQjmU5zLhQAwn8ZP1ols 15 | C0wBAnLfcO5vvIu2H3f6v5V8i0GlINScK8qlMLMR30FDOSySCc7kt/b5aoxO5/cC 16 | AwEAAaNTMFEwHQYDVR0OBBYEFBbyy7rOF8AEnlqQP605n3gdK5alMB8GA1UdIwQY 17 | MBaAFBbyy7rOF8AEnlqQP605n3gdK5alMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI 18 | hvcNAQELBQADggIBAJnvnpi5N9VDHGoSQqwuYIv2iQy+wSlzmfl75AmnGGZctfdP 19 | MGlhLIbNV5q0KMf9vm2+9Qip1QLb0ZP4Dpjr15SZqdEx0pbOgaXEhFUfMS+1rFWu 20 | Dm999wWhrzMjqbDtJQcIO993dpo3tmHvtqkJRdsCRCwb19hdUhHslpLkCKDKUlA6 21 | u7MUCuTbpIJ1A7IHQmulSjLsihIS7S1Oq5XCyT2/jHQK1qD3NbuyTZS57SkQEDji 22 | EC0LhCrp+Awcb0ocf6sEgweqQTHqSJOLIBDboVU5c9/m6oeS/hcDHJOnqhf2k7TY 23 | cKyz38avpXV78SabqNBSUU8EfmMFPOLASpwUuTb242WmeazIx12BHqBBFH7XBLrl 24 | Y/AoeJD4yXLLkfRK93XNXdLQE26QJLF3AZJlUY/ZGDnLuWXPmK3HVREMJ6sD7Qjv 25 | bbbjZO1tGcMbyOOrfO7sj+e+AFkr7bp5d6Ueoelkh75PaJnPLYb8hcck7gVaUVOJ 26 | 40UadWeNUWwwWV8qn2nTHZdZXKaQgoNGkYPXfIVvu7+7RKF+suMxHcjCDO6kOted 27 | PfNv9Xi5pW1akLly2FPtrnNuTpDTWx4JeNUpnbnSvaIFk7kZwnNuk2SZPwjJ4+rM 28 | JOtwY+KL5Tt4U7XFOMiMhLfvZ32t4DFve8B7P+LFET3lP0Kh8gGuHZEq8HUf 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /cmd/users/ssl/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,D85580CD7390F6B7 4 | 5 | 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 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /cmd/users/ssl/instructions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Inspired from: https://github.com/grpc/grpc-java/tree/master/examples#generating-self-signed-certificates-for-use-with-grpc 4 | 5 | # Output files 6 | # ca.key: Certificate Authority private key file (this shouldn't be shared in real-life) 7 | # ca.crt: Certificate Authority trust certificate (this should be shared with users in real-life) 8 | # server.key: Server private key, password protected (this shouldn't be shared) 9 | # server.csr: Server certificate signing request (this should be shared with the CA owner) 10 | # server.crt: Server certificate signed by the CA (this would be sent back by the CA owner) - keep on server 11 | # server.pem: Conversion of server.key into a format grpc likes (this shouldn't be shared) 12 | 13 | # Summary 14 | # Private files: ca.key, server.key, server.pem, server.crt 15 | # "Share" files: ca.crt (needed by the client), server.csr (needed by the CA) 16 | 17 | # Changes these CN's to match your hosts in your environment if needed. 18 | SERVER_CN=localhost 19 | 20 | # Step 1: Generate Certificate Authority + Trust Certificate (ca.crt) 21 | openssl genrsa -passout pass:1111 -des3 -out ca.key 4096 22 | openssl req -passin pass:1111 -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=${SERVER_CN}" 23 | 24 | # Step 2: Generate the Server Private Key (server.key) 25 | openssl genrsa -passout pass:1111 -des3 -out server.key 4096 26 | 27 | # Step 3: Get a certificate signing request from the CA (server.csr) 28 | openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/CN=${SERVER_CN}" 29 | 30 | # Step 4: Sign the certificate with the CA we created (it's called self signing) - server.crt 31 | openssl x509 -req -passin pass:1111 -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt 32 | 33 | # Step 5: Convert the server certificate to .pem format (server.pem) - usable by grpc 34 | openssl pkcs8 -topk8 -nocrypt -passin pass:1111 -in server.key -out server.pem 35 | -------------------------------------------------------------------------------- /cmd/users/ssl/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0 3 | MB4XDTIxMDMyNDA5MjkxM1oXDTMxMDMyMjA5MjkxM1owFDESMBAGA1UEAwwJbG9j 4 | YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtqRgopIDpnLx 5 | lIbanoxEHckAlq+GOWPqebVCiQp2JQYLDUCPiKyYYEoyPJa1zTtoDSigIh/9nLkm 6 | M8WU+uor9aFqt+uPEPCA5NgxUMw0aQg5pqKMv/w44jjzqvnZzpSVEB7drWm5+xXx 7 | yZsKNLIrcKZSP3re5/QmPF95rZJMnkhq+muCXWG1F33HVo4Rvfb9jzwuoQ6l1Obv 8 | +VOt9YPwqkmlC7/8upmCnsmgv+6FOncBJ1tDSm+CylNxv7dlxAzGLpr9gY5Iy6QI 9 | oVCd3359xDDkvjFZGAYrXuMa5MMCFqV/bfwxCXF9pJdfSfAoAiyxYJNgwJfVCOPd 10 | CIHhAtxbGmb5+oE3MYiNHKllJJBsiN2ST5pA90OAQ3FP0EnHvfqcJF8zGdak7oxn 11 | j3+iafagNGR5c9Ipv2EzyjdtkCaJrLNgFWN16Cy/N5+NKzASQIUnQs1MjJiUyYrA 12 | P824hWpV6wZe34vhTFvGZc9HrFxaEI6HJ4c8UDvkj+7ir9cxfxRwZeIPmnnimqaN 13 | HHFD8ZQOLUUpXdHxDTeOELEOBKi3Eq9JTa/tdxF41AGwKatvtqktcYP9r1nNUl+/ 14 | XIup9PFR8/vrRvT2N9eYr8lGDvrmOCMp13AgPArgYGx90zxcVE5yXTcc/SkJx86P 15 | 9wtEMyYBoan4LkpXFx+b49d9qyRBxi8CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEA 16 | UG+W8jYfZ2DSpJe41SGwEMhNzwzIkRsCQLNx7Vf39bABOl/6F4b8LGhYJWKJHu7p 17 | rLSr/SYwMVV20zL4trFx8SFcjrxdWmwgDhT4EB+MFJStY2b0z2PurjJu3er6Jtw8 18 | E0O0BRvyksDFYarfKapcb40ph1LKxRMfCQK9krhNajloZTIKp/ed0XgdgcD73qRl 19 | VWTkErw6peV6OO9sCu17sUxOCmXlmAj5YKHLlXpuixJOrrcts0oG6IHzTmvvhfUp 20 | V7PHhYCtwl67dIJBGiLhWWsLaYXsYZN7gObo+8bjCkztyEUCl0+AjL09vAIMJZPs 21 | rnuDLk+rHvLSFwh/+VcXmjhUAPWvEpA2gk936sgyw/bOiF0nJEHWYSnfxo/WXbgZ 22 | 8KvAZzIEcf7pZe+94mxovDSmGO0gtDA9/Z4IpFDuUZ8iKPQDKmNoItvyE5LXVf+j 23 | 5zUPIHZxpAlAc3s1N4dTypZY1xYbSygEHZZ44TvuNN8HZeyoMWTmIIVudHXLFNW1 24 | 4uZSlnBXpvXJpotenbpYfEm0b3TRk1yDvDy6iTgbRvyvSQs5WeW0jdcbrQYUaeyr 25 | b1z8A49lzf1gLrM+o1Jfh9fXQipKSONARizhXCv/ul2Ulm5dYlVHoBu2kAgFsSHR 26 | 1GqtUo1nM+6H2M2gGyfNe2fNSkjKB7yVq1QofM0lMdg= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /cmd/users/ssl/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEWTCCAkECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0B 3 | AQEFAAOCAg8AMIICCgKCAgEAtqRgopIDpnLxlIbanoxEHckAlq+GOWPqebVCiQp2 4 | JQYLDUCPiKyYYEoyPJa1zTtoDSigIh/9nLkmM8WU+uor9aFqt+uPEPCA5NgxUMw0 5 | aQg5pqKMv/w44jjzqvnZzpSVEB7drWm5+xXxyZsKNLIrcKZSP3re5/QmPF95rZJM 6 | nkhq+muCXWG1F33HVo4Rvfb9jzwuoQ6l1Obv+VOt9YPwqkmlC7/8upmCnsmgv+6F 7 | OncBJ1tDSm+CylNxv7dlxAzGLpr9gY5Iy6QIoVCd3359xDDkvjFZGAYrXuMa5MMC 8 | FqV/bfwxCXF9pJdfSfAoAiyxYJNgwJfVCOPdCIHhAtxbGmb5+oE3MYiNHKllJJBs 9 | iN2ST5pA90OAQ3FP0EnHvfqcJF8zGdak7oxnj3+iafagNGR5c9Ipv2EzyjdtkCaJ 10 | rLNgFWN16Cy/N5+NKzASQIUnQs1MjJiUyYrAP824hWpV6wZe34vhTFvGZc9HrFxa 11 | EI6HJ4c8UDvkj+7ir9cxfxRwZeIPmnnimqaNHHFD8ZQOLUUpXdHxDTeOELEOBKi3 12 | Eq9JTa/tdxF41AGwKatvtqktcYP9r1nNUl+/XIup9PFR8/vrRvT2N9eYr8lGDvrm 13 | OCMp13AgPArgYGx90zxcVE5yXTcc/SkJx86P9wtEMyYBoan4LkpXFx+b49d9qyRB 14 | xi8CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQC0KyHNRQu7d6K5WfesNumVQY0H 15 | 9qBsFmxT9nUOjl1VWf6PT5DWAu0LgTF/6K3KkoddhIel/NG/rl3R76SbyRwLUr0b 16 | SHDC7OjN6lFeuERp+F+udBPMlLilGjxqYp/Cv+FsXH0Ang4Kv4CjA36Xj3Pz5g14 17 | noPdYMNCEIceYhO3PMyjHSJPgtbFQ0Vi1ojiPz5mvI9HklPCUCfpf3+WOILBKCCX 18 | 6PByO9kIyw+CRCqQPfFKwbJZxmz5k3FrUmFzyUlW6j0dGt1VxKxrj0yyDbqfWTdS 19 | O6JYZCHeKcm8wPQqqkfUelF1iXcrWy9k+Kh4rOCXzpJm/KaR+HSamCvrrAXud0Em 20 | VgZuKhpT9gGw9GhexrrJ4ZRPyxef4HV5FJ/9tXcdgMFyAj1srcsfBRjh9M+nGU8t 21 | 0QbPu+FfDh9tXt5h9sG/HHM+jx2r3qNHoGjkb/yD45LjjX0yc00VOlYi3lrCMhkN 22 | QPVSdnHCipb1ZrMvfONzEHOBM5P5GRxx7ByoHOS3D1cFgWXxr+1y81wohOUmNvdc 23 | eet5TZIoCpWZxffgwA/Nw4j2H3Ju4COnt9/h2NtRlA0BQx0qPOi8lmddMciHhS3Q 24 | U03ph7jGEol7BtaK2I9VI875WMPy6j8+a9WQoV6vmq08W9qDQaLwfUyoCFxUzTlp 25 | VvOWqIqxVMcmMFvlRA== 26 | -----END CERTIFICATE REQUEST----- 27 | -------------------------------------------------------------------------------- /cmd/users/ssl/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,C87A3E50D9F83279 4 | 5 | q7N8xsovATnUhNSZ0I/pbvzOd0GA9t1NyToaGsAMYa7aOVOPHa3aXShJmW33g6ba 6 | kqNa9UyKNUHz86/nLldbpqDrSCHYCoysRhwcWq6VQ7wHUlssXitjuAnXWo9K470H 7 | 6oN7NyCa2wPLpLM2/n5NqHFb/9o2iJ7iIbQC7VBPrlwC1iGyP6tcRkYelQaxDgaZ 8 | iHuiAPCG59JcV/XdlXBi0v+7iFSBaYuVn3Mzt8faJ/9H9sHgACMQEj23LAfb72sB 9 | hLJ1OFntCqKKYoyNOykmukREL7IvQSHm4fzZK2XEwvwMZ04nMjUsHCFvAotFh6v5 10 | TqWwunuq6eBrm6FV1qaVXY+1uXgOO3XmKYSOYtnUE4zxbmUsrtrxvoRPOtJFN4r7 11 | ydH/WoBK+qfBSRj/8k+8bzcsyVQySTiCXxQQBGn4hqeDrxkuJ2vcVUiLVZ9X4/HB 12 | y2IpmbKIj3LKcSvhXxWe0TAl+ZoHH8CXLxC7xD+iW6Bh0o5ZQkQG8rKSODR6P8Uw 13 | UDoeVJDIdHSH47+h0dViLp9qQZXndw8RBAxpdwjOX2SVo9SXaLakc0fpPPWRMy/d 14 | UStL2kR/Bnaosfk/AiegQaCqkzjboYd/Cd4Llpj86eTx0zfbYKh+A7TPd2MweB0z 15 | 49stMioolEQKsf3cZeCFWbxEhzc52voe7jGE4XnR40/g1EOlM46DmjzHVOHavb0I 16 | 8qNCjM+bOamb28aduTkV2oqAgGsttqGAIADJa4yZN80QulFZ2nsUPmM9WUqwCc+c 17 | phreW8zgAxacno8zrUKbstyLD8PGZN4Dl8WC3od48G6DD8MWfX7VLG15hDWkyKGV 18 | LME4T+Q6k6oiVlkY5n2uhc2mh1aZRQyfpk+hRu6eahOBBeJg37In8k++rpbz0D/2 19 | YafSkJPvT2GamQc9fon6oXTSXtUuQASQ8rV7eHnCG1zJJ8a5jGrnrPSlO0s4EAg8 20 | xPOwNWxYl5OD7uqUWZEQHAXE6vyEmbSPVHijbyLyNx9l/SZ+rr2QQK3fnV08R7qL 21 | s/42jCbh6m0Gt4sqJRmz1dqFyfX8IorcPXy5PN4XBw7n50JYGqGNNtcBSh/JebDT 22 | mb3OCSfU/anpUbkZt319LKgk61ZOlbrZpmo9RPMwS3CfwsNYoM94zVgFIOQf11CY 23 | a1UMhghnfERbNIuHyo7X058aYpfbLSnRyRXu2/EW+5KUGcjU+6OBfo6e6Ag/YcOp 24 | jp8K/Y9oMRvCDM9jOOUWzeB/yBPHFymramyZ2aL8qJXGpHeBKCW5M8HsufLzgi3i 25 | 5eZ54dZMiaKhAVgdqOzUKKCunWag3rVyt716sEsQluT3/pOQxgSbxHM4R+X2U6ac 26 | 7rw95PEqSy1NhSPIwuqGeKNEzKDaz9bWcZOyM57+U4umMB0qQ7taTFBcvJs4l+QL 27 | Ng0Guie6nDOuqymbwMF3K/PKb6rwQMtGIn8hDpyZjU/cdRmX8likWASv60APioBl 28 | t7MPXzj1EhnzMZeLKvu4T7G3SKHdHaAG4oBB64OGRd0OI1g7mDkfbGTcSBizdTRG 29 | CagvVjJ73DocYsmaObsurKegzwfQOfs9VOq3OzJYX4qmP40CBCy+D8TrESupsGWF 30 | SCVBDXgjpsXmtlQkDkW5TmzuYT0A/pDB9Cym70LwXLN/MaxuXvDWRbbUvHeYArtH 31 | 0Xmh2+qpuVAXVlPrmgujSIqOWFC0wg37nV+WfrJpAwtBSjcwoEQws3PlL0U3o/sW 32 | JIpijYmCgfJLW38HtmbLq20oCUGcYlxsUmslbENx6efGeVefoRrUpUum9umyTSVr 33 | xf9nGXOoYdu7uikJv/Qzsesrv2GOZ2WNepATKSXN3DwGLGVTMAnhMY7RbHjow58p 34 | 5PuxXqBMuowHHT5rtn3ihO0+RR5kviP/RgKTa52cO3cgYONBvliCmYIAUkmyUV13 35 | kgGcyz+ZX8qhXm2+b5IgojNfoNlHFzFcAgAhmMnSUwEaM24TZ64Yb7r9Eblyz2f0 36 | dzzmCR3qmUi/kDj8wX51MhD/P4hNzu1CYenOq7hmjWJ8OwLvk1c+PKzj5DAZWjwp 37 | UoCDt/FceHxkg0/8Tj8PCTxjuStoG51GqBMtiht97syGDYkr2ZWVzQYok2LWCW+e 38 | JK19ScJeYlEWpQQ9i8aTpfi82fJE3mPD9sOJR2LnLpRUZB1XlDoH5EoKAOq+bsit 39 | zZfifQQjfZhHhr/sbSmzcEEpnd879TyCYJEkbugNjxKKz6fiyj0Dnz0Q66CJDEC5 40 | T6kxkIsDsJG1vybmx1RWJfIkDTv1zAR/5XUfVis/a7YR2Mqi7VGBKn2qQ64v5Vfv 41 | P+yjjeVcElAEBJv2OzDMZTIO0QLEXoH9p8UqkJRuYm4SRvH1/CSVnTbqhHwkio9W 42 | BusQGuLYPuS76zKsEO+wALTG48lVenMQOVlE34MQrLs9MvWHo58KWThm6q5ZlcVC 43 | m88L+7tSagSt6WzSNFEM97STN3uxkYbDlAoIUQCUJGmrXO1VPJzxfs4i9ZlrDrGU 44 | x1g0D0yh4g+tDjy4NxBQsghaNZIvQXVhbRv0kQ3dVGPB1LQyBELMzmMJobpxuPZl 45 | ARJ8qU6Ym2kUKsUG0X5M0SoVy3nVTlvZ6TcYHqJp06HEu9ac5vsXp3qvcwxLp1bG 46 | zbk7o/faT/5LkWOkOOdaOVX4xC2wwFMeWbaTtX0h3ZkH3kP2bLvr+mC4luSC0DkN 47 | zSK2YqO2URIYeVOgrFvlSUDtlS3AKVTGc8SQPP0vPDjKnquMhSCx5nsyj+T0TaJO 48 | C2Xp298R4bjrYmEblHM4yPkRS9XQaDw5DG4aDoVU8VuU7YKqjCklEssoKDZHQEov 49 | WvbnoXVHWiQ+zooESfCP9YbeHUGrVKSRwVIFKq0t+dexktaWMwcuoALo1np299hg 50 | u4Vol3HNr0D7zuUp6uJ+JheSofKljcsel5J2/qlkUaBcjNuVGy3BXJ4+jUmur9Jl 51 | w1ctib2tm3HOripeegBreqtlJir+iScuZJFXQNgAZT1CexmICVKFWH1x6NyDh8ge 52 | UkMw3GkzmZVuz4ENUgW1ItE4/eLpvHdvp9aKHFqJn1sIAicIIL3zUQiI1wMcj6z6 53 | TmO7slvfVPpMphKChNB7H4NuhaDVFN2mE0Uwz2fmfZLskFaT+w8+UbcaTcDayxcX 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /cmd/users/ssl/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | GTEdA3IPQIhZI1SW3xk/7J06rdmyQaui 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /docker/ReadMe.Md: -------------------------------------------------------------------------------- 1 | # Docker Monitoring 2 | -------------------------------------------------------------------------------- /docker/alerts.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: default 3 | rules: 4 | - alert: InternalServerError 5 | expr: increase(hits{status="500"}[1m]) > 0 6 | for: 1s 7 | labels: 8 | severity: critical 9 | annotations: 10 | summary: "path {{ $labels.path }} returned status 500" 11 | description: "{{ $labels.path }} of job {{ $labels.job }} returned status {{ $labels.status }}" -------------------------------------------------------------------------------- /docker/prometheus-local.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: [ 'host.docker.internal:9090' ] 9 | 10 | - job_name: 'system' 11 | static_configs: 12 | - targets: [ 'host.docker.internal:9100' ] 13 | 14 | - job_name: 'sessions' 15 | static_configs: 16 | - targets: [ 'host.docker.internal:7070' ] 17 | 18 | - job_name: 'users' 19 | static_configs: 20 | - targets: [ 'host.docker.internal:7071' ] 21 | 22 | - job_name: 'images' 23 | static_configs: 24 | - targets: [ 'host.docker.internal:7074' ] 25 | 26 | - job_name: 'hotels' 27 | static_configs: 28 | - targets: [ 'host.docker.internal:7075' ] 29 | 30 | - job_name: 'comments' 31 | static_configs: 32 | - targets: [ 'host.docker.internal:7078' ] 33 | -------------------------------------------------------------------------------- /docker/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: [ 'node_exporter:9090' ] 9 | 10 | - job_name: 'system' 11 | static_configs: 12 | - targets: [ 'host.docker.internal:9101' ] 13 | 14 | - job_name: 'sessions' 15 | static_configs: 16 | - targets: [ 'host.docker.internal:7070' ] 17 | 18 | - job_name: 'users' 19 | static_configs: 20 | - targets: [ 'host.docker.internal:7071' ] 21 | 22 | - job_name: 'images' 23 | static_configs: 24 | - targets: [ 'host.docker.internal:7074' ] 25 | 26 | - job_name: 'hotels' 27 | static_configs: 28 | - targets: [ 'host.docker.internal:7075' ] 29 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/spf13/viper" 10 | 11 | "github.com/badu/microservices-demo/pkg/jaeger" 12 | "github.com/badu/microservices-demo/pkg/logger" 13 | "github.com/badu/microservices-demo/pkg/postgres" 14 | "github.com/badu/microservices-demo/pkg/rabbitmq" 15 | "github.com/badu/microservices-demo/pkg/redis" 16 | ) 17 | 18 | type Config struct { 19 | HttpServer HttpServer 20 | Postgres postgres.Config 21 | GRPC GRPC 22 | RabbitMQ rabbitmq.Config 23 | Metrics Metrics 24 | Logger logger.Config 25 | Redis redis.Config 26 | AWS AWS 27 | Jaeger jaeger.Config 28 | GRPCServer GRPCServer 29 | } 30 | 31 | func (c *Config) ProductionMode() bool { 32 | return c.GRPCServer.Mode == "Production" 33 | } 34 | 35 | type AWS struct { 36 | S3Region string 37 | S3EndPoint string 38 | S3EndPointMinio string 39 | DisableSSL bool 40 | S3ForcePathStyle bool 41 | } 42 | 43 | type GRPCServer struct { 44 | SessionPrefix string 45 | Port string 46 | SessionGrpcServicePort string 47 | AppVersion string 48 | SessionID string 49 | CSRFPrefix string 50 | Mode string 51 | CsrfSalt string 52 | CsrfExpire int 53 | SessionExpire int 54 | Timeout time.Duration 55 | ReadTimeout time.Duration 56 | WriteTimeout time.Duration 57 | MaxConnectionIdle time.Duration 58 | MaxConnectionAge time.Duration 59 | CookieLifeTime int 60 | } 61 | 62 | type HttpServer struct { 63 | AppVersion string 64 | Port string 65 | PprofPort string 66 | SessionCookieName string 67 | CSRFHeader string 68 | Timeout time.Duration 69 | ReadTimeout time.Duration 70 | WriteTimeout time.Duration 71 | CookieLifeTime int 72 | } 73 | 74 | type GRPC struct { 75 | SessionServicePort string 76 | UserServicePort string 77 | HotelsServicePort string 78 | CommentsServicePort string 79 | ImagesServicePort string 80 | } 81 | 82 | // Metrics config 83 | type Metrics struct { 84 | Port string 85 | URL string 86 | ServiceName string 87 | } 88 | 89 | // Load config file from given path 90 | func LoadConfig(filename string) (*viper.Viper, error) { 91 | v := viper.New() 92 | 93 | v.SetConfigName(filename) 94 | v.SetConfigType("yaml") 95 | v.AddConfigPath(".") 96 | v.AutomaticEnv() 97 | if err := v.ReadInConfig(); err != nil { 98 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 99 | return nil, errors.Join(err, errors.New("config file was not found")) 100 | } 101 | return nil, err 102 | } 103 | 104 | return v, nil 105 | } 106 | 107 | // Parse config file 108 | func ParseConfig(v *viper.Viper) (*Config, error) { 109 | c := &Config{} 110 | 111 | err := v.Unmarshal(c) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | // inherit the version from the grpc server 117 | if len(c.GRPCServer.AppVersion) > 0 { 118 | c.HttpServer.AppVersion = c.GRPCServer.AppVersion 119 | } 120 | 121 | if c.Logger.PrintConfig { 122 | // print out the config as json 123 | jsonConfig, _ := json.MarshalIndent(c, "", "\t") 124 | fmt.Printf("config : %s", jsonConfig) 125 | } 126 | 127 | if len(c.HttpServer.AppVersion) == 0 { 128 | return nil, errors.New("app version was not found in config") 129 | } 130 | 131 | return c, nil 132 | } 133 | 134 | // Get config 135 | func GetConfig(configPath string) (*Config, error) { 136 | cfgFile, err := LoadConfig(configPath) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | cfg, err := ParseConfig(cfgFile) 142 | if err != nil { 143 | return nil, err 144 | } 145 | return cfg, nil 146 | } 147 | 148 | func GetConfigPath(configPath string) string { 149 | switch configPath { 150 | case "docker": 151 | return "./config/config-docker" 152 | case "": 153 | return "./config/config-local" 154 | default: 155 | return configPath 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /pkg/grpc_client/grpc_client.go: -------------------------------------------------------------------------------- 1 | package grpc_client 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | grpcRetry "github.com/grpc-ecosystem/go-grpc-middleware/retry" 8 | traceUtils "github.com/opentracing-contrib/go-grpc" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/codes" 11 | ) 12 | 13 | const ( 14 | backoffLinear = 100 * time.Millisecond 15 | ) 16 | 17 | func NewGRPCClientServiceConn(ctx context.Context, commonMW *ClientMiddleware, target string) (*grpc.ClientConn, error) { 18 | opts := []grpcRetry.CallOption{ 19 | grpcRetry.WithBackoff(grpcRetry.BackoffLinear(backoffLinear)), 20 | grpcRetry.WithCodes(codes.NotFound, codes.Aborted), 21 | } 22 | 23 | clientGRPCConn, err := grpc.DialContext( 24 | ctx, 25 | target, 26 | grpc.WithUnaryInterceptor(traceUtils.OpenTracingClientInterceptor(commonMW.Tracer())), 27 | grpc.WithUnaryInterceptor(commonMW.GetInterceptor()), 28 | grpc.WithInsecure(), 29 | grpc.WithUnaryInterceptor(grpcRetry.UnaryClientInterceptor(opts...)), 30 | ) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return clientGRPCConn, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/grpc_client/interceptor.go: -------------------------------------------------------------------------------- 1 | package grpc_client 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/metadata" 10 | 11 | "github.com/badu/microservices-demo/pkg/config" 12 | "github.com/badu/microservices-demo/pkg/logger" 13 | ) 14 | 15 | type ClientMiddleware struct { 16 | logger logger.Logger 17 | cfg *config.Config 18 | tracer opentracing.Tracer 19 | } 20 | 21 | func NewClientMiddleware(logger logger.Logger, cfg *config.Config, tracer opentracing.Tracer) *ClientMiddleware { 22 | return &ClientMiddleware{logger: logger, cfg: cfg, tracer: tracer} 23 | } 24 | 25 | func (m *ClientMiddleware) Logger(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 26 | start := time.Now() 27 | meta, _ := metadata.FromIncomingContext(ctx) 28 | result, err := handler(ctx, req) 29 | m.logger.Infof("Method: %s, Time: %v, Metadata: %v, Err: %v", info.FullMethod, time.Since(start), meta, err) 30 | return result, err 31 | } 32 | 33 | func (m *ClientMiddleware) GetInterceptor() func( 34 | ctx context.Context, 35 | method string, 36 | req interface{}, 37 | reply interface{}, 38 | cc *grpc.ClientConn, 39 | invoker grpc.UnaryInvoker, 40 | opts ...grpc.CallOption, 41 | ) error { 42 | return func( 43 | ctx context.Context, 44 | method string, 45 | req interface{}, 46 | reply interface{}, 47 | cc *grpc.ClientConn, 48 | invoker grpc.UnaryInvoker, 49 | opts ...grpc.CallOption, 50 | ) error { 51 | start := time.Now() 52 | err := invoker(ctx, method, req, reply, cc, opts...) 53 | m.logger.Infof("call=%v req=%#v reply=%#v time=%v err=%v", 54 | method, req, reply, time.Since(start), err) 55 | return err 56 | } 57 | } 58 | 59 | func (m *ClientMiddleware) Tracer() opentracing.Tracer { 60 | return m.tracer 61 | } 62 | -------------------------------------------------------------------------------- /pkg/grpc_errors/grpc_errors.go: -------------------------------------------------------------------------------- 1 | package grpcErrors 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | type Error string 16 | 17 | func (e Error) Error() string { return string(e) } 18 | 19 | const ( 20 | ErrNotFound = Error("not found") 21 | ErrNoCtxMetaData = Error("no ctx metadata") 22 | ErrInvalidSessionId = Error("invalid session id") 23 | ErrEmailExists = Error("email already exists") 24 | ) 25 | 26 | // Parse error and get code 27 | func ParseGRPCErrStatusCode(err error) codes.Code { 28 | switch { 29 | case errors.Is(err, sql.ErrNoRows): 30 | return codes.NotFound 31 | case errors.Is(err, context.Canceled): 32 | return codes.Canceled 33 | case errors.Is(err, context.DeadlineExceeded): 34 | return codes.DeadlineExceeded 35 | case errors.Is(err, ErrEmailExists): 36 | return codes.AlreadyExists 37 | case errors.Is(err, ErrNoCtxMetaData): 38 | return codes.Unauthenticated 39 | case errors.Is(err, ErrInvalidSessionId): 40 | return codes.PermissionDenied 41 | case strings.Contains(err.Error(), "Validate"): 42 | return codes.InvalidArgument 43 | case strings.Contains(err.Error(), "redis"): 44 | return codes.NotFound 45 | case errors.Is(err, context.Canceled): 46 | return codes.Canceled 47 | case errors.Is(err, context.DeadlineExceeded): 48 | return codes.DeadlineExceeded 49 | } 50 | return codes.Internal 51 | } 52 | 53 | // Map GRPC errors codes to http status 54 | func MapGRPCErrCodeToHttpStatus(code codes.Code) int { 55 | switch code { 56 | case codes.Unauthenticated: 57 | return http.StatusUnauthorized 58 | case codes.AlreadyExists: 59 | return http.StatusBadRequest 60 | case codes.NotFound: 61 | return http.StatusNotFound 62 | case codes.Internal: 63 | return http.StatusInternalServerError 64 | case codes.PermissionDenied: 65 | return http.StatusForbidden 66 | case codes.Canceled: 67 | return http.StatusRequestTimeout 68 | case codes.DeadlineExceeded: 69 | return http.StatusGatewayTimeout 70 | case codes.InvalidArgument: 71 | return http.StatusBadRequest 72 | } 73 | return http.StatusInternalServerError 74 | } 75 | 76 | // GRPC Error response 77 | func ErrorResponse(err error, msg string) error { 78 | return status.Errorf(ParseGRPCErrStatusCode(err), fmt.Sprintf("%s: %v", msg, err)) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/jaeger/jaeger.go: -------------------------------------------------------------------------------- 1 | package jaeger 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | "github.com/uber/jaeger-client-go" 8 | jaegerCfg "github.com/uber/jaeger-client-go/config" 9 | jaegerLog "github.com/uber/jaeger-client-go/log" 10 | "github.com/uber/jaeger-lib/metrics" 11 | ) 12 | 13 | // Jaeger 14 | type Config struct { 15 | Host string 16 | ServiceName string 17 | LogSpans bool 18 | } 19 | 20 | // Init Jaeger 21 | func InitJaeger(cfg Config) (opentracing.Tracer, io.Closer, error) { 22 | jaegerCfgInstance := jaegerCfg.Configuration{ 23 | ServiceName: cfg.ServiceName, 24 | Sampler: &jaegerCfg.SamplerConfig{ 25 | Type: jaeger.SamplerTypeConst, 26 | Param: 1, 27 | }, 28 | Reporter: &jaegerCfg.ReporterConfig{ 29 | LogSpans: cfg.LogSpans, 30 | LocalAgentHostPort: cfg.Host, 31 | }, 32 | } 33 | 34 | return jaegerCfgInstance.NewTracer( 35 | jaegerCfg.Logger(jaegerLog.StdLogger), 36 | jaegerCfg.Metrics(metrics.NullFactory), 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | // Logger config 11 | type Config struct { 12 | Encoding string 13 | Level string 14 | Development bool 15 | DisableCaller bool 16 | DisableStacktrace bool 17 | PrintConfig bool 18 | } 19 | 20 | // Logger methods interface 21 | type Logger interface { 22 | InitLogger() 23 | Debug(args ...interface{}) 24 | Debugf(template string, args ...interface{}) 25 | Info(args ...interface{}) 26 | Infof(template string, args ...interface{}) 27 | Warn(args ...interface{}) 28 | Warnf(template string, args ...interface{}) 29 | Error(args ...interface{}) 30 | Errorf(template string, args ...interface{}) 31 | DPanic(args ...interface{}) 32 | DPanicf(template string, args ...interface{}) 33 | Fatal(args ...interface{}) 34 | Fatalf(template string, args ...interface{}) 35 | } 36 | 37 | // Logger 38 | type ApiLogger struct { 39 | sugarLogger *zap.SugaredLogger 40 | cfg Config 41 | } 42 | 43 | // App Logger constructor 44 | func NewApiLogger(cfg Config) ApiLogger { 45 | return ApiLogger{cfg: cfg} 46 | } 47 | 48 | // For mapping config logger to email_service logger levels 49 | var loggerLevelMap = map[string]zapcore.Level{ 50 | "debug": zapcore.DebugLevel, 51 | "info": zapcore.InfoLevel, 52 | "warn": zapcore.WarnLevel, 53 | "error": zapcore.ErrorLevel, 54 | "dpanic": zapcore.DPanicLevel, 55 | "panic": zapcore.PanicLevel, 56 | "fatal": zapcore.FatalLevel, 57 | } 58 | 59 | func (l *ApiLogger) getLoggerLevel(cfg Config) zapcore.Level { 60 | level, exist := loggerLevelMap[cfg.Level] 61 | if !exist { 62 | return zapcore.DebugLevel 63 | } 64 | 65 | return level 66 | } 67 | 68 | // Init logger 69 | func (l *ApiLogger) InitLogger() { 70 | logLevel := l.getLoggerLevel(l.cfg) 71 | 72 | logWriter := zapcore.AddSync(os.Stderr) 73 | 74 | var encoderCfg zapcore.EncoderConfig 75 | if l.cfg.Development { 76 | encoderCfg = zap.NewDevelopmentEncoderConfig() 77 | } else { 78 | encoderCfg = zap.NewProductionEncoderConfig() 79 | } 80 | 81 | var encoder zapcore.Encoder 82 | encoderCfg.LevelKey = "LEVEL" 83 | encoderCfg.CallerKey = "CALLER" 84 | encoderCfg.TimeKey = "TIME" 85 | encoderCfg.NameKey = "NAME" 86 | encoderCfg.MessageKey = "MESSAGE" 87 | 88 | if l.cfg.DisableCaller { 89 | encoderCfg.CallerKey = "" 90 | } 91 | 92 | if l.cfg.Encoding == "console" { 93 | encoder = zapcore.NewConsoleEncoder(encoderCfg) 94 | } else { 95 | encoder = zapcore.NewJSONEncoder(encoderCfg) 96 | } 97 | 98 | encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder 99 | core := zapcore.NewCore(encoder, logWriter, zap.NewAtomicLevelAt(logLevel)) 100 | logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) 101 | 102 | l.sugarLogger = logger.Sugar() 103 | if err := l.sugarLogger.Sync(); err != nil { 104 | l.sugarLogger.Error(err) 105 | } 106 | 107 | } 108 | 109 | // Logger methods 110 | 111 | func (l *ApiLogger) Debug(args ...interface{}) { 112 | l.sugarLogger.Debug(args...) 113 | } 114 | 115 | func (l *ApiLogger) Debugf(template string, args ...interface{}) { 116 | l.sugarLogger.Debugf(template, args...) 117 | } 118 | 119 | func (l *ApiLogger) Info(args ...interface{}) { 120 | l.sugarLogger.Info(args...) 121 | } 122 | 123 | func (l *ApiLogger) Infof(template string, args ...interface{}) { 124 | l.sugarLogger.Infof(template, args...) 125 | } 126 | 127 | func (l *ApiLogger) Warn(args ...interface{}) { 128 | l.sugarLogger.Warn(args...) 129 | } 130 | 131 | func (l *ApiLogger) Warnf(template string, args ...interface{}) { 132 | l.sugarLogger.Warnf(template, args...) 133 | } 134 | 135 | func (l *ApiLogger) Error(args ...interface{}) { 136 | l.sugarLogger.Error(args...) 137 | } 138 | 139 | func (l *ApiLogger) Errorf(template string, args ...interface{}) { 140 | l.sugarLogger.Errorf(template, args...) 141 | } 142 | 143 | func (l *ApiLogger) DPanic(args ...interface{}) { 144 | l.sugarLogger.DPanic(args...) 145 | } 146 | 147 | func (l *ApiLogger) DPanicf(template string, args ...interface{}) { 148 | l.sugarLogger.DPanicf(template, args...) 149 | } 150 | 151 | func (l *ApiLogger) Panic(args ...interface{}) { 152 | l.sugarLogger.Panic(args...) 153 | } 154 | 155 | func (l *ApiLogger) Panicf(template string, args ...interface{}) { 156 | l.sugarLogger.Panicf(template, args...) 157 | } 158 | 159 | func (l *ApiLogger) Fatal(args ...interface{}) { 160 | l.sugarLogger.Fatal(args...) 161 | } 162 | 163 | func (l *ApiLogger) Fatalf(template string, args ...interface{}) { 164 | l.sugarLogger.Fatalf(template, args...) 165 | } 166 | -------------------------------------------------------------------------------- /pkg/pagination/pagination.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | const ( 12 | defaultSize = 10 13 | ) 14 | 15 | // Pagination query params 16 | type Pagination struct { 17 | OrderBy string `json:"orderBy,omitempty"` 18 | Size int `json:"size,omitempty"` 19 | Page int `json:"page,omitempty"` 20 | } 21 | 22 | func NewPaginationQuery(size int, page int) *Pagination { 23 | return &Pagination{Size: size, Page: page} 24 | } 25 | 26 | // Set page size 27 | func (q *Pagination) SetSize(sizeQuery string) error { 28 | if sizeQuery == "" { 29 | q.Size = defaultSize 30 | return nil 31 | } 32 | n, err := strconv.Atoi(sizeQuery) 33 | if err != nil { 34 | return err 35 | } 36 | q.Size = n 37 | 38 | return nil 39 | } 40 | 41 | // Set page number 42 | func (q *Pagination) SetPage(pageQuery string) error { 43 | if pageQuery == "" { 44 | q.Size = 0 45 | return nil 46 | } 47 | n, err := strconv.Atoi(pageQuery) 48 | if err != nil { 49 | return err 50 | } 51 | q.Page = n 52 | 53 | return nil 54 | } 55 | 56 | // Set order by 57 | func (q *Pagination) SetOrderBy(orderByQuery string) { 58 | q.OrderBy = orderByQuery 59 | } 60 | 61 | // Get offset 62 | func (q *Pagination) GetOffset() int { 63 | if q.Page == 0 { 64 | return 0 65 | } 66 | return (q.Page - 1) * q.Size 67 | } 68 | 69 | // Get limit 70 | func (q *Pagination) GetLimit() int { 71 | return q.Size 72 | } 73 | 74 | // Get OrderBy 75 | func (q *Pagination) GetOrderBy() string { 76 | return q.OrderBy 77 | } 78 | 79 | // Get OrderBy 80 | func (q *Pagination) GetPage() int { 81 | return q.Page 82 | } 83 | 84 | // Get OrderBy 85 | func (q *Pagination) GetSize() int { 86 | return q.Size 87 | } 88 | 89 | func (q *Pagination) GetQueryString() string { 90 | return fmt.Sprintf("page=%v&size=%v&orderBy=%s", q.GetPage(), q.GetSize(), q.GetOrderBy()) 91 | } 92 | 93 | // Get pagination query struct from 94 | func GetPaginationFromCtx(c echo.Context) (*Pagination, error) { 95 | q := &Pagination{} 96 | if err := q.SetPage(c.QueryParam("page")); err != nil { 97 | return nil, err 98 | } 99 | if err := q.SetSize(c.QueryParam("size")); err != nil { 100 | return nil, err 101 | } 102 | q.SetOrderBy(c.QueryParam("orderBy")) 103 | 104 | return q, nil 105 | } 106 | 107 | // Get total pages int 108 | func (q *Pagination) GetTotalPages(totalCount int) int { 109 | // d := float64(totalCount) / float64(pageSize) 110 | d := float64(totalCount) / float64(q.GetSize()) 111 | return int(math.Ceil(d)) 112 | } 113 | 114 | // Get has more 115 | func (q *Pagination) GetHasMore(totalCount int) bool { 116 | return q.GetPage() < totalCount/q.GetSize() 117 | } 118 | -------------------------------------------------------------------------------- /pkg/postgres/postgres.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/jackc/pgx/v4/pgxpool" 10 | ) 11 | 12 | const ( 13 | maxConn = 50 14 | healthCheckPeriod = 3 * time.Minute 15 | maxConnIdleTime = 1 * time.Minute 16 | maxConnLifetime = 3 * time.Minute 17 | minConns = 10 18 | lazyConnect = false 19 | ) 20 | 21 | // Postgresql config 22 | type Config struct { 23 | PostgresqlHost string 24 | PostgresqlPort string 25 | PostgresqlUser string 26 | PostgresqlPassword string 27 | PostgresqlDbname string 28 | PostgresqlSSLMode string 29 | PgDriver string 30 | } 31 | 32 | func NewPgxConn(cfg Config) (*pgxpool.Pool, error) { 33 | ctx := context.Background() 34 | dataSourceName := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=%s password=%s", 35 | cfg.PostgresqlHost, 36 | cfg.PostgresqlPort, 37 | cfg.PostgresqlUser, 38 | cfg.PostgresqlDbname, 39 | cfg.PostgresqlSSLMode, 40 | cfg.PostgresqlPassword, 41 | ) 42 | 43 | poolCfg, err := pgxpool.ParseConfig(dataSourceName) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | poolCfg.MaxConns = maxConn 49 | poolCfg.HealthCheckPeriod = healthCheckPeriod 50 | poolCfg.MaxConnIdleTime = maxConnIdleTime 51 | poolCfg.MaxConnLifetime = maxConnLifetime 52 | poolCfg.MinConns = minConns 53 | poolCfg.LazyConnect = lazyConnect 54 | 55 | connPool, err := pgxpool.ConnectConfig(ctx, poolCfg) 56 | if err != nil { 57 | return nil, errors.Join(err, errors.New("configuring postgresql connection")) 58 | } 59 | 60 | return connPool, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/postgres/types.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | ) 7 | 8 | type NullString sql.NullString 9 | 10 | func (n *NullString) MarshalJSON() ([]byte, error) { 11 | if n.Valid { 12 | return json.Marshal(n.String) 13 | } else { 14 | return json.Marshal(nil) 15 | } 16 | } 17 | 18 | func (n *NullString) UnmarshalJSON(bytes []byte) error { 19 | var s *string 20 | if err := json.Unmarshal(bytes, &s); err != nil { 21 | return err 22 | } 23 | 24 | if s != nil { 25 | n.Valid = true 26 | n.String = *s 27 | } else { 28 | n.Valid = false 29 | } 30 | return nil 31 | } 32 | 33 | type NullFloat64 sql.NullFloat64 34 | 35 | func (n *NullFloat64) MarshalJSON() ([]byte, error) { 36 | if n.Valid { 37 | return json.Marshal(n.Float64) 38 | } else { 39 | return json.Marshal(nil) 40 | } 41 | } 42 | 43 | func (n *NullFloat64) UnmarshalJSON(bytes []byte) error { 44 | var v *float64 45 | if err := json.Unmarshal(bytes, &v); err != nil { 46 | return err 47 | } 48 | 49 | if v != nil { 50 | n.Valid = true 51 | n.Float64 = *v 52 | } else { 53 | n.Valid = false 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/rabbitmq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/streadway/amqp" 7 | ) 8 | 9 | // RabbitMQ 10 | type Config struct { 11 | Host string 12 | Port string 13 | User string 14 | Password string 15 | } 16 | 17 | // Initialize new RabbitMQ connection 18 | func NewRabbitMQConn(cfg Config) (*amqp.Connection, error) { 19 | connAddr := fmt.Sprintf( 20 | "amqp://%s:%s@%s:%s/", 21 | cfg.User, 22 | cfg.Password, 23 | cfg.Host, 24 | cfg.Port, 25 | ) 26 | return amqp.Dial(connAddr) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-redis/redis/v8" 7 | ) 8 | 9 | // Redis config 10 | type Config struct { 11 | Addr string 12 | RedisPassword string 13 | Database string 14 | DefaultDatabase string 15 | Password string 16 | MinIdleConn int 17 | PoolSize int 18 | PoolTimeout int 19 | DB int 20 | } 21 | 22 | // Returns new redis client 23 | func NewRedisClient(cfg Config) *redis.Client { 24 | redisHost := cfg.Addr 25 | 26 | if redisHost == "" { 27 | redisHost = ":6379" 28 | } 29 | 30 | client := redis.NewClient(&redis.Options{ 31 | Addr: redisHost, 32 | MinIdleConns: cfg.MinIdleConn, 33 | PoolSize: cfg.PoolSize, 34 | PoolTimeout: time.Duration(cfg.PoolTimeout) * time.Second, 35 | Password: cfg.Password, // no password set 36 | DB: cfg.DB, // use default DB 37 | }) 38 | 39 | return client 40 | } 41 | --------------------------------------------------------------------------------