Pull to refresh
123.15
X5 Tech
Всё о технологиях в ритейле

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

Reading time6 min
Views5.2K
Во второй части нашего цикла статей «Пишем блог на микросервисах» мы описали «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
}
Tags:
Hubs:
Total votes 4: ↑3 and ↓1+5
Comments1

Articles

Information

Website
x5-tech.ru
Registered
Founded
2006
Employees
over 10,000 employees
Location
Россия