Здесь мы опишем реализацию микросервиса 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/
Добавить комментарий