Slide 1

Slide 1 text

Бережная обработка ошибок в микросервисах Шамбир Сергей Ведущий инженер, iSpring

Slide 2

Slide 2 text

Архитектура микросервиса 2

Slide 3

Slide 3 text

Уровень domain использует только возможности Go Правила зависимостей 3 Уровень app использует только domain и возможности Go Пакет main всё создаёт, связывает и запускает Уровень infrastructure использует app и domain, а также технологии SQL, Redis, HTTP, GRPC и т.д. ○ Максимум одна технология на один пакет

Slide 4

Slide 4 text

Принципы обработки ошибок Предполагаем, что: ● Логи нужны для анализа инцидентов ● Ошибки и panic возникают при обработке запросов к API 4 Поэтому: ● Все неожиданные ошибки попадают в лог ● Любая ошибка или panic влияет только на один запрос

Slide 5

Slide 5 text

Создание и передача ошибок Я не настоящий программист. Я сооружаю что-то, пока оно не заработает, и иду дальше. Настоящий программист скажет: “да, оно работает, но память течёт”. А я просто буду перезапускать Apache на каждые 10 запросов. Расмус Лердорф, автор языка PHP

Slide 6

Slide 6 text

Используйте sentinel errors package app import "errors" var ErrNoCake = errors.New("no cake found") 6

Slide 7

Slide 7 text

Можно заменить var на const package app type Error string func (e Error) Error() string { return string(e) } const ErrNoCake = Error("no cake found") 7 Источник: Constant errors | Dave Cheney

Slide 8

Slide 8 text

Добавляйте информацию к сторонней ошибке package mysql import "github.com/pkg/errors" func (r *repository) FindOne(...) { row := r.client.QueryRow(sql, params...) switch err := row.Scan(...) { case sql.ErrNoRows: return nil, errors.WithStack(app.ErrNoCake) } } 8

Slide 9

Slide 9 text

Используйте errors.Cause func (s *service) SaveCake(...) error { state, err := s.repo.FindOne(...) if errors.Cause(err) == ErrNoCake { err = nil // No cake is OK, create a new one // ... } else if err != nil { // ... } } 9

Slide 10

Slide 10 text

Обрабатывайте ошибки в defer type UnitOfWork interface { Repository() Repository Complete(err *error) } 10

Slide 11

Slide 11 text

Обрабатывайте ошибки в defer type UnitOfWork interface { Repository() Repository Complete(err *error) } 11 unit, err := factory.New() if err != nil { return err } defer unit.Complete(&err) repo := unit.Repository()

Slide 12

Slide 12 text

Слияние ошибок при завершении операции func (u *unitOfWork) Complete(err *error) { if *err == nil { txErr := u.tx.Commit() *err = errors.Wrap(txErr, "cannot complete transaction ") } else { txErr := return u.tx.Rollback() *err = mergeErrors(*err, errors.Wrap(txErr, "cannot rollback transaction ")) } } 12

Slide 13

Slide 13 text

Создайте функцию слияния ошибок package errors func mergeErrors(err error, nextErr error) error { if err == nil { err = nextErr } else if nextErr != nil { err = errors.Wrap(err, nextErr.Error()) } return err } 13

Slide 14

Slide 14 text

Обработка ошибок Я определяю паттерн как идею, которая оказалась полезной в одном практическом контексте и, скорее всего, будет полезна в других Мартин Фаулер

Slide 15

Slide 15 text

Как мы используем GRPC syntax = "proto3"; service Cooking { rpc ListCakes(ListCakesRequest) returns (ListCakesResponse) { option (google.api.http) = { get: "/api/v1/cakes" }; } } message ListCakesRequest { repeated string cookIDs = 3; } message ListCakesResponse { message Cake { float weight = 1; float price = 2; } 15

Slide 16

Slide 16 text

Middleware для обработки ошибок 16 func NewMiddleware(n api.Server, l log.Logger) api.Server { server := &errorHandlingMiddleware{ next: n, logger: l, } return server }

Slide 17

Slide 17 text

Реализация Middleware func (m *...) ListCakes( ctx context.Context, req *api.ListCakesRequest) (*api.ListCakesResponse, error) { start := time.Now() res, err := m.next.ListCakes(ctx, req) m.logCall(start, err, "ListCakes", log.Fields{ "cookIDs": req.CookIDs, }) return res, translateError(err) } 17

Slide 18

Slide 18 text

Трансляция ошибки func translateError(err error) error { switch errors.Cause(err) { case app.ErrNoCake: err = status.Errorf(codes.NotFound, err.Error()) default: err = status.Errorf(codes.Internal, err.Error()) } return err } 18

Slide 19

Slide 19 text

Трансляция ошибки type statusError interface { GRPCStatus() *status.Status } func translateError(err error) error { _, ok := err.(statusError) if ok { return err } switch errors.Cause(err) { case app.ErrNoCake: err = status.Errorf(codes.NotFound, err.Error()) 19

Slide 20

Slide 20 text

Логирование вызова func (m *...) logCall(start time.Time, err error, method string, fields log.Fields) { fields["duration"] = fmt.Sprintf("%v", time.Since(start)) fields["method"] = method logger := m.logger.WithFields(fields) if err != nil { logger.Error(err, "call failed") } else { logger.Info("call finished") } 20

Slide 21

Slide 21 text

Объявите свой интерфейс Logger package log type Logger interface { WithField(string, interface{}) Logger WithFields(Fields) Logger Debug(...interface{}) Info(...interface{}) Error(error, ...interface{}) } 21 Источник: Давайте поговорим о ведении логов (habr.com)

Slide 22

Slide 22 text

Объявите свой интерфейс Logger package log type MainLogger interface { Logger FatalError(error, ...interface{}) } 22

Slide 23

Slide 23 text

Создайте свой пакет логирования 23

Slide 24

Slide 24 text

1. Установите формат: logrus.JSONFormatter 2. Установите log level 3. Добавьте получение stacktrace Нормализация полей logrus для ELK: Создайте свой пакет логирования 24 time @timestamp msg message

Slide 25

Slide 25 text

Инициализируйте logger в main func initLogger(config Config) (log.MainLogger, error) { logLevel, err := jsonlog.ParseLevel(config.LogLevel) if err != nil { return nil, rrors.Wrap(err, "failed to parse log level" ) } return jsonlog.NewLogger(&jsonlog.Config{ Level: logLevel, AppName: "resumestate" }), nil } 25

Slide 26

Slide 26 text

Подытожим 1. Используйте github.com/pkg/errors, читайте блог Dave Chaney ○ Напишите в дополнение свою библиотеку errors 2. Напишите свои пакеты log и jsonlog 3. Используйте чистую архитектуру, читайте книги Robert Martin 4. Используйте паттерн Middleware, чтобы обрабатывать ошибки в едином месте 26

Slide 27

Slide 27 text

Спасибо за внимание! Вопросы? 27