Во второй части нашего цикла статей «Пишем блог на микросервисах» мы описали «API Gateway».
Здесь мы опишем реализацию микросервиса User.
Наш микросервис должен уметь:
Для начала опишем наш сервис в протофайле (./services/user/protobuf/user.proto).
Указываем используемый синтаксис — proto3. Указываем наименование пакета protobuf, в этом пакете будет реализован автосгенеренный код серверной и клиентской части.
Импортируем библиотеку аннотации google/api/annotations.proto, она понадобится для описания директив для генерации REST API.
Описание сервиса 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.
И будет ожидать в body запроса следующую структуру:
И соответственно в случае успеха будет отдавать структуру:
Либо ошибку. Подробнее про ошибки расскажем чуть ниже в разделе описания функций, реализующих описанные в протофайле интерфейсы.
Остальные интерфейсы (SignIn, Create, Update, Delete, Get, Find) объявляются аналогично.
Теперь, когда у нас есть готовый протофайл. Переходим в корневой каталог проекта и выполняем команду sh ./bin/protogen.sh. Этот скрипт сгенерит основной код.
Далее переходим с каталог ./services/user и в файле functions.go пишем реализацию объявленных интерфейсов.
Для начала реализуем middleware. При каждом запросе к сервису мы вытаскиваем из контекста запроса параметры TraceId, UserId, UserRole и пишем их в лог файл. Здесь же можно реализовать авторизацию запроса.
В методе SignUp определяем структуру ответа.
Далее проверяем параметры запроса.
И если все Ок, заполняем структуру User, пишем в БД и возвращаем ответ.
Отдельно обращаем внимание на возврат ошибки, например:
Так как в конечном итоге эта ошибка вернется нашему api-wg (в его REST часть) и было бы круто, преобразовать ее в стандартный HTTP код-ответ. Чтобы не писать кучу дополнительного кода, следует использовать не стандартный go error, а status.error из пакета google.golang.org/grpc/status.
Все типовые ошибки микросервиса User и как они конвертируются в HTTP коды ответов описаны в файле./services/user/app/errors.go.
И последнее что хотелось бы рассказать о микросервисе User, это то как он запускается и подключается к базе данных. Эти операции выполняются в файле ./services/user/main.go.
Запуск сервиса:
Подключение к БД (main.go):
Реализация функции DbConnect (./services/user/functions.go):
Здесь мы опишем реализацию микросервиса 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
}