Концептуально разработку любого сервиса можно разбить на:
- Описание контракта
- Реализацию бизнес-логики
- Логирование запросов
- Метрики сервиса
- Трассировку запросов
- Транспорт
- Клиент для интеграции с сервисом
Генератор tg
предназначен для того, чтобы избавить разработчика от необходимости заниматься рутиной в п.п. 3-7.
Для реализации сервиса разработчику достаточно описать лишь будущий контракт в виде интерфейса на языке Go
и снабдить
аннотациями в виде специфичных для tg
комментариев.
Остальная рутинная работа по генерации всех слоёв будет выполнена tg
, что позволяет сосредоточиться на реализации
единственной ценности сервиса - бизнес-логике
.
В данный момент для tg
основным видом транспорта является jsonRPC 2.0, но
поддерживается также генерация простого HTTP
транспорта.
В качестве основы для транспортного уровня, был выбран go-fiber, основанный
на fasthttp, как альтернативе стандартной библиотеки net/http
, превосходящий
оригинал по скорости более чем в 10 раз.
Инициализация через шаблон не является обязательным шагом для использования tg
, но позволяет упростить работу, в
случае создания сервиса с нуля.
Для генерации сервиса из шаблона с нуля, можно воспользоваться командой init
:
tg init -module <go module name> -service <service name> <project name>
В результате, в папке <project name>
будет сгенерирован работоспособный шаблон проект сервиса.
Источником истины для tg
является интерфейс на языке Go
, снабжённый аннотациями.
К методам интерфейса предъявляются следующие требования:
- Все аргументы и возвращаемые значения методов интерфейса должны быть именованными. Эти имена, по-умолчанию, будут использованы как ключи на транспортном уровне.
- Первым аргументом метода должен быть
context
, а последним возвращаемым значением -error
.
// @tg jsonRPC-server log metrics trace
type Some interface {
Method(ctx context.Context, arg1 string, arg2 int) (ret1 int, ret2 float64, err error)
}
Этого описания достаточно, что генерации сервиса, предоставляющего публичный метода Some.Method
, посредством jsonRPC 2.0 транспорта.
Запрос jsonRPC
для метода Method
интерфейса Some
будет выглядеть следующим образом:
{
"id": 1,
"jsonrpc": "2.0",
"method": "some.method",
"params": {
"arg1": "v",
"arg2": 2
}
}
Ответ:
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"ret1": 2,
"ret2": 2
}
}
Для генерации транспорта, необходимо выполнить команду:
tg transport --services . --out ../internal/transport
Для генерации документации в формате openAPI, необходимо выполнить команду:
tg swagger --services . --outFile ../api/swagger.yaml
Где,
services
- путь до папки с интерфейсом (в норме для tg
эта папка является рабочей)
outPath
- путь, где будет сохранён результат
outPackage
- путь, где будет сохранён package.json
с описанием npm
пакета
Хорошей практикой считается использование утилиты goimports
, после генерации:
goimports -l -w ../internal/transport
Для инициализации сервера, необходимо перечислить сервисы (интерфейсы, описанные [ранее](/#Описание контракта)), которые он будет обслуживать и запустить его любым доступным способом, согласно документации go-fiber
Пример инициализации:
...
svcSome := some.New()
options := []transport.Option{
transport.Use(cors.New()),
transport.WithRequestID("X-Request-Id"),
transport.Some(transport.NewSome(svcSome)),
}
srv := transport.New(log.Logger, options...).WithMetrics().WithLog()
srv.ServeHealth(config.Service().HealthBind, "OK")
srv.ServeMetrics(log.Logger, "/", config.Service().MetricsBind)
go func () {
log.Info().Str("bind", config.Service().Bind).Msg("listen on")
if err := srv.Fiber().Listen(config.Service().Bind); err != nil {
log.Panic().Err(err).Msg("server error")
}
}()
...
Как видно из примера, в списке опций можно передавать не только сервисы, сгенерированные из интерфейсов, но и
вспомогательные обработчики.
Метод transport.Use
поддерживает все возможности, предоставляемые go-fiber. С
перечнем готовых мидлвар можно ознакомиться здесь.
Дополнительно можно указать следующие опции:
Опция позволяет управлять конфигурацией go-fiber
, согласно документации.
Пример:
fiberConfig := fiber.Config{
Prefork: true,
CaseSensitive: true,
StrictRouting: true,
ServerHeader: "Fiber",
AppName: "Some Test App v1.0.1",
}
...
options := []transport.Option{
transport.SetFiberCfg(fiberConfig),
transport.Use(cors.New()),
transport.WithRequestID("X-Request-Id"),
transport.Some(transport.NewSome(svcSome)),
}
srv := transport.New(log.Logger, options...).WithMetrics().WithLog()
...
Опция позволяет указать размер буфера чтения в байтах (по умолчанию 4096).
Опция позволяет указать размер буфера записи в байтах (по умолчанию 4096).
Опция позволяет указать максимальный размер тела запроса в байтах (по умолчанию 4 194 304).
Опция позволяет указать максимальное количество запросов, которые можно передать за раз в батче (по умолчанию 100).
Опция позволяет указать максимальное количество обработчиков, которые будут запускаться параллельно для каждого батч запроса (по умолчанию 10).
Опция позволяет указать таймаут чтения для запросов (по умолчанию unlimited
).
Опция позволяет указать таймаут записи для запросов (по умолчанию unlimited
).
Опция позволяет указать заголовок из которого будет извлекаться идентификатор запроса. Его будет логироваться с
ключом requestID
, передаваться в трассировку и транслироваться в ответе с тем же заголовком.
Для генерации Go
клиента, необходимо выполнить команду (поддерживается генерация клиента для jsonRPC 2.0) :
tg client -go --services . --outPath ../pkg/clients/go
Для генерации javaScript
клиента, необходимо выполнить команду:
tg client client -js --services . --outPath ../pkg/clients/js --outPackage ../
Где,
services
- путь до папки с интерфейсом (в норме для tg
эта папка является рабочей)
outPath
- путь, где будет сохранён результат
outPackage
- путь, где будет сохранён package.json
с описанием npm
пакета
Хорошей практикой считается использование утилиты goimports
, после генерации:
goimports -l -w ../pkg/clients/go
Для инициализации клиента, необходимо указать адрес сервера.
Пример инициализации:
...
cli := some.New("http://127.0.0.1:9000")
...
Где, cli
будет общим клиентом для всех интерфейсов, которые участвовали в генерации.
Чтобы получить клиента для конкретного интерфейса, необходимо его извлечь соответствующим методом, как указано ниже:
...
cli := some.New("http://127.0.0.1:9000")
someCli := cli.Some()
...
При инициализации клиента можно указать следующие опции:
Опция позволяет указать декодер, с помощью которого можно получить нужные типы ошибок. Хорошей практикой является экспорт декодера из репозитория, предоставляющего клиента.
ErrorDecoder
представляет собой функцию со следующей сигнатурой:
type ErrorDecoder func (errData json.RawMessage) error
По умолчанию, если не указал декодер явно, ошибки преобразуются к структуре, имплементирующей интерфейс error
вида:
type errorJsonRPC struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func (err errorJsonRPC) Error() string {
return err.Message
}
Опция, включающая логирование всех запросов клиента в формате curl
.
Опция, включающая логирование всех запросов клиента в формате curl
, если в ответ получена ошибка.
Опция, позволяющая извлечь данные из контекста запроса и передать их через перечисленные заголовки.
В качестве параметров принимаются ключи контекста. Это могут быть как простые строки, так и любые типы, имплементирующие
интерфейс fmt.Stringer
.
Опция, позволяющая установить собственную конфигурацию TLS
для клиента.
Может понадобиться, например, когда на сервере используется самоподписанный сертификат и нужно выключить его проверку.
- интерфейс
Включает генерацию circuit breaker
для методов интерфейса.
Опция, позволяющая установить собственную конфигурацию для circuit breaker
:
type Settings struct {
MaxRequests uint32
Interval time.Duration
Timeout time.Duration
ReadyToTrip func (counts Counts) bool
OnStateChange func (name string, from State, to State)
IsSuccessful func (err error) bool
}
Где,
MaxRequests
— это максимальное количество запросов, которым разрешено пройти, когда circuit breaker
полуоткрыт.
Если MaxRequests
равно 0
, circuit breaker
разрешает только 1
запрос.
Interval
— это циклический период закрытого состояния, в течение которого прерыватель цепи очищает внутренние
счетчики, описанные далее в этом разделе. Если Interval
равен 0
, circuit breaker
не очищает внутренние счетчики во
время закрытого состояния.
Timeout
— это период открытого состояния, по истечении которого состояние circuit breaker
становится полуоткрытым.
Если Timeout
равен 0
, значение тайм-аута circuit breaker
устанавливается равным 60 секундам.
ReadyToTrip
вызывается с Counts
всякий раз, когда запрос завершается сбоем в закрытом состоянии. Если ReadyToTrip
возвращает true
, circuit breaker
будет переведен в открытое состояние. Если ReadyToTrip
равен nil
,
используется ReadyToTrip
по умолчанию. ReadyToTrip
по умолчанию возвращает true
, когда количество последовательных
сбоев превышает 5
.
OnStateChange
вызывается всякий раз, когда изменяется состояние circuit breaker
.
IsSuccessful
вызывается с ошибкой, возвращенной из запроса. Если IsSuccessful
возвращает true
, ошибка считается
нормальным поведением. В противном случае ошибка засчитывается как сбой. Если IsSuccessful
равен nil
,
используется IsSuccessful
по умолчанию, который возвращает false
для всех не нулевых ошибок.
Опция, позволяющая включить fallback
кэширование для circuit breaker
. В качестве параметра принимается любой
объект, имплементирующий интерфейс:
type cache interface {
SetTTL(ctx context.Context, key string, value interface{}, ttl time.Duration) (err error)
GetTTL(ctx context.Context, key string, value interface{}) (createdAt time.Time, ttl time.Duration, err error)
}
При установленной опции, каждый успешный запрос кэшируется с ключом равным хэшу от параметров запроса. Таким образом,
при срабатывании fallBack
обработчика circuit breaker
, в ответе клиента вернётся результат последнего удачного
запроса, вместо ошибки.
Опция, устанавливающая время, на которое кэшируется последний успешный ответ, для fallback
(по умолчанию 24 часа).
Аннотацией в терминах tg
называется комментарий, оформленный специальным образом.
Целью аннотаций является указание генератору параметров и настроек, специфичных для конкретного сервиса.
Аннотации могут быть определены на разных уровнях - пакет, интерфейс, метод интерфейса и на уровне типов.
Аннотации имеют следующие уровни определения:
- на уровне пакета, действуют на все методы всех интерфейсов в этом пакете
- на уровне интерфейса действуют на все методы этого интерфейса
- на уровне метода, действуют только на этот метод
В случае конфликтов, приоритет имеют аннотации с наименьшей зоной действия.
Аннотации имею следующий формат:
// @tg <имя>=<значение>
В случае, когда аннотации имею смысл флагов, значение может не указываться. Несколько аннотаций может быть сгруппировано в одной строке (разделитель пробел). Например:
// @tg http-prefix=v1 jsonRPC-server log metrics trace
Следующая запись синонимична пред идущей:
// @tg http-prefix=v1
// @tg jsonRPC-server
// @tg log metrics trace
- модуль
- интерфейс
Включает генерацию логирования.
- модуль
- интерфейс
Включает генерацию трассировку методов интерфейсов.
- модуль
- интерфейс
Включает генерацию метрик для методов интерфейсов.
- модуль
- интерфейс
- метод
- тип
Добавляет краткое описание той сущности, на уровне которой определён.
Используется, в том числе, при генерации документации в формате openAPI.
В случае генерации web
клиента, описание на уровне пакета используется в package.json
для описания npm
пакета.
- метод
Детальное писание метода в генерируемой
документации openAPI.
Поддерживает перенос строки и прочие возможности форматирования openAPI
.
Позволяет указать дополнительные теги в exchange
структурах метода интерфейса или переопределить существующие.
Типичный пример - сокрытие чувствительных данных поля и логах:
...
// @tg token.tags=dumper:hide,md
Login(ctx context.Context, token string) (cookie *types.Cookie, err error)
...
В результате, в логах, середина строки token
будет заменена на символы *
.
- тип
Указывает тип поля в генерируемой документации, согласно спецификации openAPI
- тип
Для поля можно перечислить список возможных значений.
- тип
Указывает формат поля в генерируемой документации, согласно спецификации openAPI
- тип
Указывает обязательность поля в генерируемой документации, согласно спецификации openAPI
- тип
Указывает пример значения поля в генерируемой документации, согласно спецификации openAPI
- метод
Определяет маппинг параметров, переданных в параметрах URL
, в аргументы метода.
- метод
Определяет маппинг параметров, переданных в пути URL
, в аргументы метода.
Переменные, которые попали в маппинг, исключаются из exchange
структур.
- модуль
- интерфейс
Задаёт префикс к пути URL
методов.
Формула пути, по которому доступен метода выглядит следующим образом:
/globalPrefix/prefix/methodPath
Где,
globalPrefix
- префикс, объявленный на уровне пакета
prefix
- префикс, объявленный на уровне интерфейса
methodPath
- имя интерфейса/имя метода, но может быть переопределён через аннотацию http-path
v
- метод
Определяет маппинг параметров, переданных в заголовках запроса, в аргументы/результаты метода.
Переменные, которые попали в маппинг, исключаются из exchange
структур.
- метод
Определяет маппинг параметров, переданных в cookie
запроса, в аргументы/результаты метода.
Переменные, которые попали в маппинг, исключаются из exchange
структур.
- метод
Указывает HTTP
метод, который будет использован для доступа к методу интерфейса.
- метод
Указывает HTTP
код ответа, который будет считаться успешным, при доступе к методу интерфейса.
- модуль
Переопределяет пакет, который будет использоваться для кодирования/декодирования JSON
.
Используется для случаев, когда нужно особое поведение кодека или есть более оптимальный кодек, предоставляющий тот же
интерфейс, что и стандартный encoding/json
.
Например, github.com/seniorGolang/json
возвращает пустые срезы как []
, а не как nil
, в стандартном encoding/json
и имеет ряд других оптимизаций по скорости работы.
- модуль
Переопределяет пакет, который будет использоваться для кодирования/декодирования UUID
, при конвертации.
В замещающем пакете должен быть определён метод Parse(s string) (UUID, error)
.
По умолчанию используется пакет github.com/google/uuid
.
- интерфейс
Указывает теги для описания интерфейса в формате openAPI.
- метод
Указывает какие переменных из сигнатуры метода нужно исключить из логирования.
- метод
Помечает метод как - deprecated
в документации openAPI.
- интерфейс
По умолчанию для всех полей методов включен тег omitempty
, что исключает пустые поля из ответа.
Может существенно сэкономить трафик, но не всегда fronend
готов к такому поведение и его можно выключить.
- метод
Переключает работу метода в так называемый кастомный
режим.
Это означает, что для этого метода не генерируется никаких обработчиков, а используется тот, который указан в аннотации.
Кастомный обработчик должен иметь следующую сигнатуру:
CustomHandler(ctx *fiber.Ctx, svc <тип интерфеса, к которому принадлежит метод>) (err error)
Рекомендуется использовать кастомные обработчики только в крайнем случае, когда невозможно имплементировать метод другими способами. Т.к. то, что происходит в этом обработчик никак не формализовано, то логи, метрики и прочее нужно реализовать самостоятельно.
- метод
Позволяет указать mime
тип, который ожидается в запросе.
По умолчанию application/ json
.
- метод
Позволяет указать mime
тип, который ожидается в ответе.
По умолчанию application/ json
.
- модуль
Позволяет указать в документации openAPI, что используется авторизация.
- модуль
Указывает генератору документации список адресов, по которым доступен сервис и их человеко читаемые имена.
- модуль
Указывает генератору документации текущую версию сервиса.
- модуль
Указывает генератору документации заголовок к документации сервиса.
- модуль
Указывает генератору NPM
модуля автора сервиса.
- модуль
Указывает генератору NPM
модуля адрес репозитория, где будет опубликован клиент.
- модуль
Указывает генератору NPM
модуля имя пакета, под которым будет опубликован клиент.
- модуль
Указывает генератору NPM
модуля является ли он публичным или приватным.
- модуль
Указывает генератору NPM
модуля под какой лицензией он распространяется.
- интерфейс
Включает генерацию HTTP
сервера на базе интерфейса.
- интерфейс
Включает генерацию jsonRPC 2.0 сервера на базе интерфейса.
RequestCount = prometheus.NewCounterFrom(prometheus.CounterOpts{
Help: "Number of requests received",
Name: "count",
Namespace: "service",
Subsystem: "requests",
}, []string{"method", "service", "success"})
RequestCountAll = prometheus.NewCounterFrom(prometheus.CounterOpts{
Help: "Number of all requests received",
Name: "all_count",
Namespace: "service",
Subsystem: "requests",
}, []string{"method", "service"})
RequestLatency = prometheus.NewHistogramFrom(prometheus.HistogramOpts{
Help: "Total duration of requests in microseconds",
Name: "latency_microseconds",
Namespace: "service",
Subsystem: "requests",
}, []string{"method", "service", "success"})