contract.yaml
Info
Данное руководство призвано помочь в описании contract.yaml
Замечание
В первую очередь нас интересуют данные, которые могут быть использованы в работе аналитических команд. Технические поля можно пропускать.
Tip
Перед прочтением статьи настоятельно рекомендуем ознакомиться с глоссарием.
Пример создания контракта#
type UserCart struct {
Id int64 `json:"event_id"` // Идентификатор события
Name string `json:"event_name,omitempty"`
CreateTs time.Time `json:"event_ts"` // Время события
Price decimal.Decimal `json:"price,omitempty"`
IsProcessed bool `json:"is_service_processed,omitempty"`
CartItems []int64 `json:"item_ids,omitempty"`
}
specification: 1.0.1
title: user-cart
type: object
description: "События в корзине покупателя"
properties:
event_id:
type: int64
description: "Идентификатор события"
event_name:
type: string
description: "Наименование события"
event_ts:
type: string
format: "RFC3339Nano"
description: "Время события"
price:
type: float
description: "Цена"
is_service_processed:
type: boolean
description: "Обработано ли событие сервисом"
item_ids:
type: array
description: "Список идентфикаторов товаров в корзине пользователя"
items:
type: int64
description: "Идентификатор товара"
required:
- event_id
- event_ts
primaryKey: # Deprecated: use `deduplication_key` at policy.yaml instead
- event_id
На что стоит обратить внимание в Go структуре при написании контракта:
- Тег
json— определяет имя поля в сообщении. - Тег
omitempty— означает, что поле не является обязательным, то есть значение у такого поля может отсутствовать. - Типы данных — должны быть сопоставлены с типами из спецификации.
- Описание (
description) — обязательное краткое пояснение на русском языке из комментария к полю, или иной документации.
Далее необходимо изучить:
После этого можно начинать работу.
Warning
Мы придерживаемся соглашения об именовании, ознакомьтесь с ним прежде чем приступить к описанию контракта.
Публикация контракта:
- Добавьте готовый контракт в соответствующий репозиторий
- Следуйте инструкции по работе с Git
Актуализация:
- Обновляйте контракт при изменениях модели согласно правилам обновления контракта.
Ключевые слова#
Warning
В контракте не должно быть пустых полей description,type, format.
| Термин | Описание | Тип |
|---|---|---|
| specification | версия спецификации, в соответствии с которой был написан контракт | |
| title | название потока данных, должно быть строкой, состоящей из английских букв в нижнем регистре | |
| description | кратко переданный смысл данных, описываемых контрактом, в целом, а также конкретных полей в properties | |
| properties | описывает поля объекта | object |
| type | определяет тип данных, передаваемых в качестве значений полей. Также присутствует на верхнем уровне, де всегда имеет значение | object |
| format | описывает смысл данных, заключенных в строке | string |
| required | список обязательных полей объекта | object |
| sensitivity | маркер чувствительности данных, содержащихся в значениях полей | |
Требования к контрактам#
- На самом верхнем уровне находится
object, в названии которого должно быть название потока данных, вdescription— его краткое описание, а вproperties— описание полей данных descriptionобязателен для всех полей- Синтаксис и выбор типов строго соответствует спецификации
- Необходимо перечислить обязательные поля в
required - Необходимо задать
deduplication_keyв policy.yaml - Желательно задать
iceberg_partitioningв policy.yaml - Все поля должны иметь явную структуру
- Контракт должен быть настолько "плоским", насколько это возможно
- Именование полей и потоков соответствует соглашению об именовании
Поле specification#
Укажите в контракте версию спецификации, в соответствии с которой написан контракт.
Модели генерируются автоматически на основе контракта с помощью нашего тулинга, что исключает конфликты между спецификацией и её реализациями.
В общем случае, версия должна соответствовать версии прикладного тулинга, с помощью которой контракт был сформирован. Гарантии обратной совместимости сохраняются в рамках мажорной версии.
Типы данных#
Спецификация контрактов определяет следующие типы данных:
| Тип | Соответствие в Go |
|---|---|
| int8 / int16 / int32 / int64 — целое число со знаком (может быть отрицательным) | int8int16int32int64 |
| uint8/ uint16 / uint32 — целое число без знака (не может быть отрицательным) | uint8uint16int32 |
Не поддерживается явно uint64 — целое число без знака, описывается через тип string и string.format — uint64. Рекомендуем избегать его при возможности |
|
| float — число с плавающей точкой | float32 |
| double — число с плавающей точкой двойной точности | float64 |
| decimal — число с фиксированным количеством знаков до и после запятой | Decimal из модуля decimal |
boolean — логическое значение (true или false) |
bool |
| string — строка | string |
| array — массив значений. Также может быть кортежем | array/slice []datatype |
| object — JSON-объект (набором пар “ключ-значение”) | struct/map |
Чисельные типы#
Допустимые значения
| Тип | Диапазон |
|---|---|
| int8 | [-128 : 127] |
| uint8 | [0 : 255] |
| int16 | [-32768 : 32767] |
| uint16 | [0 : 65535] |
| int32 | [-2147483648 : 2147483647] |
| uint32 | [0 : 4294967295] |
| int64 | [-9223372036854775808 : 9223372036854775807] |
| uint64 | [0 : 18446744073709551615] |
| float | [-3.4e+38 : 3.4e+38] |
| double | [-1.7e+308 : +1.7e+308] |
decimal#
Число с фиксированным количеством знаков до и после запятой — 15.235, -42.1.
Этот тип имеет 2 ключевых слова:
scale— количество знаков после запятой. Должно быть целым неотрицательным числом, меньшим чем значениеprecision.precision— общее количество знаков в числе. Должно быть целым неотрицательным числом.
Пример:
boolean#
true или false.
Каст
Будьте внимательны, если при сериализации данных кастуете в boolean значения чисел или строк.
string#
Строка. Последовательность символов Unicode. Примеры: "Abcd Efgh", "", "0123".
string.format#
Строка может дополняться опциональным ключевым словом format, которое используется для описания формата значения.
date и time#
Форматы дат и времени:
| Формат | Описание |
|---|---|
| ISO8601Date | 2020-10-01, без времени |
| ISO8601 | 2022-09-05T06:30:00+03:00, с максимальной точностью до микросекунд |
| RFC3339Nano | 2024-03-12T01:00:00.123456789Z, фиксированная наносекундная точность |
ВНИМАНИЕ!
Для дат и времени, действуют следующие правила:
• Если таймзона указана, то при попадании в хранилище время будет приведено к UTC+3 (МСК).
• Если таймзона НЕ указана, то мы воспринимаем ее по умолчанию как UTC+3 (МСК).
Прочие форматы#
- uuid — UUID. Пример:
cd3b1622-eb52-4909-a8ad-2e48e6180244. - uint64 — Целое неотрицательное число в диапазоне
[0 : 18446744073709551615]. Пример:42.
Пример:
Небольшой совет
Если нет понимания, к какому типу отнести поле — это, скорее всего, строка
array#
Последовательность элементов. Может содержать элементы разных типов, но с помощью ключевого слова items можно ограничить типы.
Примеры:
my_array_of_something:
type: array
description: "Поле с массивом неизвестного наполнения"
my_array_of_strings:
type: array
description: "Поле с массивом строк"
items:
# Из-за нюансов реализации линтера, не смотря на наличие `description` выше, здесь его также необходимо указать
description: "Строка"
type: string
object#
Объект — набор пар "ключ-значение". Пара "ключ-значение" в стандарте JSON Schema, от которого мы наследовали структуру контрактов, называется "свойство" (property). То есть набор полей в данных, описываемых контрактом.
Свойства описываются с помощью ключевого слова properties:
my_object:
type: object
description: "Поле, содержащее объект"
properties:
id:
type: int64
description: "Идентификатор"
name:
type: string
description: "Наименование"
Объект поддерживает вложенность. Например, можно сделать объект внутри объекта...
my_object:
type: object
description: "Поле, содержащее объект"
properties:
another_object:
type: object
description: "Объект внутри объекта"
properties:
id:
type: uint32
description: "Идентификатор"
required:
- id
name:
type: string
description: "Наименование"
required:
- another_object
DISCLAIMER
Несмотря на техническую возможность такой реализации, мы будем требовать делать структуры плоскими. Подробнее о том, как этого можно достичь при формировании контракта, можно прочитать в рекомендациях по работе с вложенными структурами
Или сделать объект элементом массива:
my_array_of_objects:
type: array
description: "Поле с массивом объектов"
items:
type: object
properties:
id:
type: uint32
description: "Идентификатор"
name:
type: string
description: "Наименование"
required:
- id
У объекта есть ключевое слово required, в нем перечисляются обязательные поля.
my_object:
type: object
description: "Поле, содержащее объект"
properties:
id:
type: uint32
description: "Идентификатор"
name:
type: string
description: "Наименование"
ts:
type: string
format: "ISO8601"
description: "Дата создания"
required:
- id
- ts
Первичный ключ — ключевое слово primaryKey#
Перенесено в качестве
deduplication_keyв policy.yaml
Определяет поля, которые составляют первичный ключ сообщения. Первичный ключ — это поле или комбинация полей, однозначно идентифицирующих событие в топике.
Важные правила
-
Первичный ключ обязателен и существует всегда
-
Все поля, указанные в
primaryKey, должны:- Быть объявлены в схеме.
- Входить в список
requiredполей
-
Первичный ключ может быть:
- Простым (одно поле)
- Составным (несколько полей)
Пример:
specification: 1.0.1
title: example-contract
type: object
description: "Пример потока данныз"
properties:
event_id:
type: int64
description: "Идентификатор события"
event_name:
type: string
description: "Наименование события"
event_ts:
type: string
format: "RFC3339Nano"
description: "Дата и время события"
warehouse_id:
type: int32
description: "Идентификатор офиса"
parcel_id:
type: int64
description: "Идентификатор посылки"
employee_id:
type: int64
description: "Идентификатор сотрудника"
sensitivity: 2
user_id:
type: int64
description: "Идентификатор получателя посылки"
sensitivity: 2
required:
- event_id
- event_ts
primaryKey: # Deprecated: use `deduplication_key` at policy.yaml instead
- event_id
(Опционально) Ключ партиционирования — ключевое слово partitionBy#
Перенесено в качестве
iceberg_partitioningв policy.yaml
Ключевое слово partitionBy указывает на то, какое поле/набор полей обеспечит наиболее удобное и равномерное разделение данных в хранилище.
Подробнее о том, как выбрать и указать поле для партиционирования описано в нашей инструкции.
Пример:
specification: 1.0.1
title: example-contract
type: object
description: "Пример потока данных"
properties:
event_id:
type: int64
description: "Идентификатор события"
event_name:
type: string
description: "Наименование события"
event_ts:
type: string
format: "RFC3339Nano"
description: "Дата и время события"
warehouse_id:
type: int32
description: "Идентификатор офиса"
parcel_id:
type: int64
description: "Идентификатор посылки"
employee_id:
type: int64
description: "Идентификатор сотрудника"
sensitivity: 2
statuses:
type: array
description: "История статусов посылки"
items:
type: int16
description: "Статус"
required:
- event_id
- event_ts
primaryKey: # Deprecated: use `deduplication_key` at policy.yaml instead
- event_id
partitionBy: # Deprecated: use `iceberg_partitioning` at policy.yaml instead
- day(event_ts)
Пример контракта, в котором использованы почти все типы и ключевые слова одновременно#
specification: 1.0.1
title: example-contract
type: object
description: "Пример потока данных"
properties:
event_id:
type: int64
description: "Идентификатор события"
event_name:
type: string
description: "Наименование события"
event_ts:
type: string
format: "RFC3339Nano"
description: "Дата и время события"
event_type:
type: int8
description: "Код типа события"
price:
type: float
description: "Цена"
is_service_processed:
type: boolean
description: "Обработано ли событие сервисом"
parcel:
type: object
description: "Информация о посылке"
properties:
parcel_id:
type: string
format: "uint64"
description: "Идентификатор посылки"
statuses:
type: array
description: "Список статусов"
items:
type: uint16
description: "Статус"
date_created:
type: string
format: "ISO8601"
description: "Дата и время создания посылки"
goods:
type: array
description: "Список товаров в посылке, может быть огромным, не сплющивать!"
items:
type: string
format: "uint64"
description: "Наименование товара"
weight:
type: decimal
description: "Вес посылки в килограммах"
scale: 3
precision: 8
length:
type: double
description: "Длина посылки в сантиметрах"
height:
type: double
description: "Высота посылки в сантиметрах"
width:
type: double
description: "Ширина посылки в сантиметрах"
required:
- parcel_id
required:
- event_id
- event_ts
- event_type
primaryKey: # Deprecated: use `deduplication_key` at policy.yaml instead
- event_id
partitionBy: # Deprecated: use `iceberg_partitioning` at policy.yaml instead
- bucket(256, event_type)
О контрактах для потоков с неоднородной структурой сообщений#
TL;DR
Если в ваших данных присутствует Union, их следует разделить на разные потоки.
Если в потоке есть сообщения с разной структурой, например, содержащие разные типы событий и, соответственно, разные наборы полей, то такой поток должен быть разделен на несколько потоков с однородной структурой сообщений в каждом потоке.
Пример#
Представим, что существует поток данных test-parcels куда отправляются некоторые события, связанные с посылками.
Посылки бывают двух видов:
- Коробки с тремя измерениями: длина, ширина и высота.
- Ёмкости, единственная мера измерения у которых — это объем.
Примеры событий
В этом случае необходимо разделить поток test-parcels на два разных, например: test-parcels-box и test-parcels-container, и наполнять их сообщениями только одного типа, заведя для каждого свой контракт.
Итого, контракты для событий
specification: 1.0.1
title: test-parcels-box
type: object
description: "Посылка — коробка"
properties:
chrts:
type: object
description: "Тип 1"
properties:
parcel_id:
type: string
description: "Идентификатор посылки"
height:
type: int64
description: "Высота в см"
length:
type: int64
description: "Длина в см"
width:
type: int64
description: "Ширина в см"
required:
- parcel_id
- height
- length
- width
specification: 1.0.1
title: test-parcels-container
type: object
description: "Посылка — ёмкость"
properties:
chrts:
type: object
description: "Тип 2"
properties:
parcel_id:
type: string
description: "Идентификатор посылки"
volume:
type: int64
description: "Объем в кубических см"
required:
- parcel_id
- volume
Приведённые выше примеры
Не являются полными, в них описана только интересующая нас в контексте данного параграфа часть