Перейти к содержанию

contract.yaml

Info

Данное руководство призвано помочь в описании contract.yaml

Замечание

В первую очередь нас интересуют данные, которые могут быть использованы в работе аналитических команд. Технические поля можно пропускать.

Tip

Перед прочтением статьи настоятельно рекомендуем ознакомиться с глоссарием.

Пример создания контракта#

{
  "event_id": 999,
  "event_name": "test_event",
  "event_ts": "2024-01-25T05:35:10+00:00",
  "price": 99.0,
  "is_service_processed": true,
  "item_ids": [
    123,
    456,
    789
  ]
}
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

Мы придерживаемся соглашения об именовании, ознакомьтесь с ним прежде чем приступить к описанию контракта.

Публикация контракта:

  1. Добавьте готовый контракт в соответствующий репозиторий
  2. Следуйте инструкции по работе с Git

Актуализация:

Ключевые слова#

Warning

В контракте не должно быть пустых полей description,type, format.

Термин Описание Тип
specification версия спецификации, в соответствии с которой был написан контракт
title название потока данных, должно быть строкой, состоящей из английских букв в нижнем регистре
description кратко переданный смысл данных, описываемых контрактом, в целом, а также конкретных полей в properties
properties описывает поля объекта object
type определяет тип данных, передаваемых в качестве значений полей. Также присутствует на верхнем уровне, де всегда имеет значение object
format описывает смысл данных, заключенных в строке string
required список обязательных полей объекта object
sensitivity маркер чувствительности данных, содержащихся в значениях полей
primaryKey список полей объекта, составляющих первичный ключ
partitionBy список полей объекта, составляющих ключ партиционирования

Требования к контрактам#

  • На самом верхнем уровне находится object, в названии которого должно быть название потока данных, в description — его краткое описание, а в properties — описание полей данных
  • description обязателен для всех полей
  • Синтаксис и выбор типов строго соответствует спецификации
  • Необходимо перечислить обязательные поля в required
  • Необходимо задать deduplication_key в policy.yaml
  • Желательно задать iceberg_partitioning в policy.yaml
  • Все поля должны иметь явную структуру
  • Контракт должен быть настолько "плоским", насколько это возможно
  • Именование полей и потоков соответствует соглашению об именовании

Поле specification#

Укажите в контракте версию спецификации, в соответствии с которой написан контракт.

Модели генерируются автоматически на основе контракта с помощью нашего тулинга, что исключает конфликты между спецификацией и её реализациями.

В общем случае, версия должна соответствовать версии прикладного тулинга, с помощью которой контракт был сформирован. Гарантии обратной совместимости сохраняются в рамках мажорной версии.

Типы данных#

Спецификация контрактов определяет следующие типы данных:

Тип Соответствие в Go
int8 / int16 / int32 / int64 — целое число со знаком (может быть отрицательным) int8
int16
int32
int64
uint8/ uint16 / uint32 — целое число без знака (не может быть отрицательным) uint8
uint16
int32
Не поддерживается явно uint64 — целое число без знака, описывается через тип string и string.formatuint64. Рекомендуем избегать его при возможности
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 — общее количество знаков в числе. Должно быть целым неотрицательным числом.

Пример:

price:
  type: decimal
  description: "Цена товара"
  scale: 2
  precision: 10

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.

Пример:

dt:
  type: string
  format: "ISO8601"
  description: "Время события"
Небольшой совет

Если нет понимания, к какому типу отнести поле — это, скорее всего, строка

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

Определяет поля, которые составляют первичный ключ сообщения. Первичный ключ — это поле или комбинация полей, однозначно идентифицирующих событие в топике.

Важные правила
  1. Первичный ключ обязателен и существует всегда

  2. Все поля, указанные в primaryKey, должны:

    • Быть объявлены в схеме.
    • Входить в список required полей
  3. Первичный ключ может быть:

    • Простым (одно поле)
    • Составным (несколько полей)

Пример:

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 куда отправляются некоторые события, связанные с посылками.

Посылки бывают двух видов:

  • Коробки с тремя измерениями: длина, ширина и высота.
  • Ёмкости, единственная мера измерения у которых — это объем.
Примеры событий
{
  "parcel": {
    "chrts": {
      "parcel_id": "a12345",
      "height": 30,
      "length": 25,
      "width": 15
    }
  }
}
{
  "parcel": {
    "chrts": {
      "parcel_id": "b23456",
      "volume": 2000
    }
  }
}

В этом случае необходимо разделить поток 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
Приведённые выше примеры

Не являются полными, в них описана только интересующая нас в контексте данного параграфа часть