Пишем блог на микросервисах – часть 3 «User»

от автора

Во второй части нашего цикла статей «Пишем блог на микросервисах» мы описали «API Gateway».

Здесь мы опишем реализацию микросервиса User.

Наш микросервис должен уметь:

— Логировать обращения к сервису и промежуточные состояния с указанием TraceId (тот самый, который был выдан api-gw, см. Часть 2 «API Gateway»)
— Реализовать функции Вход(SignIN) и Регистрация(SignUp)
— Реализовать функции CRUD (создание, чтение, редактирование, удаление записи в БД). В качестве БД использовать MongoDB.

Для начала опишем наш сервис в протофайле (./services/user/protobuf/user.proto).
Указываем используемый синтаксис — proto3. Указываем наименование пакета protobuf, в этом пакете будет реализован автосгенеренный код серверной и клиентской части.

Импортируем библиотеку аннотации google/api/annotations.proto, она понадобится для описания директив для генерации REST API.

syntax = "proto3"; package protobuf; import "google/api/annotations.proto"; 

Описание сервиса User, непосредственно интерфейсов (методов) которыми он должен обладать. Например интерфейс SignUp(регистрация): он принимает на вход сообщение SignUpRequest, которое содержит атрибуты Username, Password, FirstName и LastName и отвечает сообщением SignUpResponse, которое содержит атрибуты Slug (UserID), Username, Role. Также в описании интерфейса, в секции options указываем директиву post: «/api/v1/user/signup. На ее основе кодогенератор создаст REST интерфейс, который будет принимать POST запросы по адресу http:{{api_gw_host}}/api/v1/user/signup.

//-------------------------------------------------- // Описание сервиса User //-------------------------------------------------- service UserService {      //Регистрация пользовател   rpc SignUp (SignUpRequest) returns (SignUpResponse) {     option (google.api.http) = {       post: "/api/v1/user/signup"     };   }    … }   //--------------------------------------------------  //  SignUp //-------------------------------------------------- message SignUpRequest {   string Username = 1;   string Password = 2;   string FirstName = 3;   string LastName = 4; } message SignUpResponse {   string Slug = 1;   string Username = 2;   string Role = 3; } 

И будет ожидать в body запроса следующую структуру:

{     Username: 'username_value',     Password: 'password_value',     FirstName: 'firstname_value',     LastName: 'lastname_value', } 

И соответственно в случае успеха будет отдавать структуру:

{     Slug: 'user_id_value',     Username: 'username_value',     Role: 'role_value', } 

Либо ошибку. Подробнее про ошибки расскажем чуть ниже в разделе описания функций, реализующих описанные в протофайле интерфейсы.

Остальные интерфейсы (SignIn, Create, Update, Delete, Get, Find) объявляются аналогично.

Теперь, когда у нас есть готовый протофайл. Переходим в корневой каталог проекта и выполняем команду sh ./bin/protogen.sh. Этот скрипт сгенерит основной код.
Далее переходим с каталог ./services/user и в файле functions.go пишем реализацию объявленных интерфейсов.

Для начала реализуем middleware. При каждом запросе к сервису мы вытаскиваем из контекста запроса параметры TraceId, UserId, UserRole и пишем их в лог файл. Здесь же можно реализовать авторизацию запроса.

//-------------------------------------------------- // Midelware //-------------------------------------------------- func AccessLogInterceptor(ctx context.Context,req interface{},info *grpc.UnaryServerInfo,handler grpc.UnaryHandler,) (interface{}, error) {      start:=time.Now()     md,_:=metadata.FromIncomingContext(ctx)      // Calls the handler     reply, err := handler(ctx, req)      var traceId,userId,userRole string     if len(md["trace-id"])>0{         traceId=md["trace-id"][0]     }     if len(md["user-id"])>0{         userId=md["user-id"][0]     }     if len(md["user-role"])>0{         userRole=md["user-role"][0]     }          msg:=fmt.Sprintf("Call:%v, traceId: %v, userId: %v, userRole: %v, time: %v", info.FullMethod,traceId,userId,userRole,time.Since(start))     app.AccesLog(msg)      return reply, err } 

В методе SignUp определяем структуру ответа.

//Ответ, по умолчанию STATUS_FAIL out:=&SignUpResponse{} 

Далее проверяем параметры запроса.

//Проверка содержимого запроса перед выполнением     //Проверка Username     err:=checkUserName(in.Username)     if err!=nil{         log.Printf("[ERR] %s.SignUp, %v", app.SERVICE_NAME,err)         return out,err     }      //Проверка Username на дубль     err=o.checkUserNameExist(in.Username)     if err!=nil{         log.Printf("[ERR] %s.SignUp, %v", app.SERVICE_NAME,err)         return out,err     }          //Проверка Password     err=checkPassword(in.Password)     if err!=nil{         log.Printf("[ERR] %s.SignUp, %v", app.SERVICE_NAME,err)         return out,err     } 

И если все Ок, заполняем структуру User, пишем в БД и возвращаем ответ.

user:=&User{         Username:in.Username,         FirstName:in.FirstName,         LastName:in.LastName,         Password:getMD5(in.Password),     }      var slug string     collection:= o.DbClient.Database("blog").Collection("users")     insertResult, err := collection.InsertOne(context.TODO(), user)     if err != nil {         return out,err     }     if oid, ok := insertResult.InsertedID.(primitive.ObjectID); ok {         slug=fmt.Sprintf("%s",oid.Hex())     }else {         err:=app.ErrInsert         return out,err     }      out.Slug=slug     out.Username=in.Username     out.Role=app.ROLE_USER     return out,nil 

Отдельно обращаем внимание на возврат ошибки, например:

err:=app.ErrInsert 

Так как в конечном итоге эта ошибка вернется нашему api-wg (в его REST часть) и было бы круто, преобразовать ее в стандартный HTTP код-ответ. Чтобы не писать кучу дополнительного кода, следует использовать не стандартный go error, а status.error из пакета google.golang.org/grpc/status.

Все типовые ошибки микросервиса User и как они конвертируются в HTTP коды ответов описаны в файле./services/user/app/errors.go.

package app  import (     "google.golang.org/grpc/codes"     "google.golang.org/grpc/status" )  //Ошибки уровня бизнес логики var (     //Ошибки валидации     ErrEmailIncorrect                   = status.Error(codes.InvalidArgument, "Некорректный E-mail")     ErrPasswordIsEmpty                  = status.Error(codes.InvalidArgument, "Password не задан")     ErrUserNameIsEmpty                  = status.Error(codes.InvalidArgument, "E-mail не задан")     ErrUserNameIsExist                  = status.Error(codes.AlreadyExists, "Пользователь уже зарегистрирован")      ErrNotFound                         = status.Error(codes.NotFound, "Пользователь не найден")     ErrIncorrectLoginOrPassword         = status.Error(codes.Unauthenticated,"Некорректный логин или пароль")      //Ошибки CRUD     ErrInsert                           = status.Error(codes.Internal, "Ошибка создания записи")     ErrUpdate                           = status.Error(codes.Internal, "Ошибка сохранения записи")      )  //================================================== // All gRPC err codes //================================================== // codes.OK - http.StatusOK // codes.Canceled - http.StatusRequestTimeout // codes.Unknown - http.StatusInternalServerError // codes.InvalidArgument - http.StatusBadRequest // codes.DeadlineExceeded - http.StatusGatewayTimeout // codes.NotFound - http.StatusNotFound // codes.AlreadyExists - http.StatusConflict // codes.PermissionDenied - http.StatusForbidden // codes.Unauthenticated - http.StatusUnauthorized // codes.ResourceExhausted - http.StatusTooManyRequests // codes.FailedPrecondition - http.StatusBadRequest // codes.Aborted - http.StatusConflict // codes.OutOfRange - http.StatusBadRequest // codes.Unimplemented - http.StatusNotImplemented // codes.Internal - http.StatusInternalServerError // codes.Unavailable - http.StatusServiceUnavailable // codes.DataLoss - http.StatusInternalServerError 

И последнее что хотелось бы рассказать о микросервисе User, это то как он запускается и подключается к базе данных. Эти операции выполняются в файле ./services/user/main.go.

Запуск сервиса:

lis,err:= net.Listen("tcp", fmt.Sprintf(":%s", Port))     if err != nil {         log.Fatalf("failed to listen: %v", err)     }      grpcServer:= grpc.NewServer(         grpc.UnaryInterceptor(protobuf.AccessLogInterceptor),     )     s:=&protobuf.Server{}  … // attach the user service to the server     protobuf.RegisterUserServiceServer(grpcServer, s) 

Подключение к БД (main.go):

//Подключение к БД s.DbConnect() defer s.DbDisconnect() 

Реализация функции DbConnect (./services/user/functions.go):

//-------------------------------------------------- // Подключение/Отключение к БД //-------------------------------------------------- func (o *Server) DbConnect() error {     var client *mongo.Client          // Create client     strURI:=fmt.Sprintf("mongodb://%s:%s@%s:%s",os.Getenv("MONGO_USER"),os.Getenv("MONGO_PASS"),os.Getenv("MONGO_HOST"),os.Getenv("MONGO_PORT"))     client, err:= mongo.NewClient(options.Client().ApplyURI(strURI))     if err != nil {         return err     }      // Create connect     err = client.Connect(context.TODO())     if err != nil {         return err     }     o.DbClient=client     return nil } 


ссылка на оригинал статьи https://habr.com/ru/company/X5RetailGroup/blog/482002/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *