Русский
Русский
English
Статистика
Реклама

Json

Освобождаем свои данные из корпоративного рабства. Концепция личного хранилища

07.12.2020 10:20:40 | Автор: admin

Автор программы Mathematica Стивен Вольфрам около 40 лет ведёт цифровой лог многих аспектов профессиональной и личной жизни

Сейчас практически всем стала понятна сущность некоторых интернет-корпораций, которые стремятся получить от людей как можно больше личных данных и заработать на этом. Они предлагают бесплатный хостинг, бесплатные мессенджеры, бесплатную почту лишь бы люди отдали свои файлы, фотографии, письма, личные сообщения. Наши данные приносят огромные деньги, а люди стали продуктом. Поэтому техногиганты Google и Facebook самые крупные корпорации в истории человечества. Это неудивительно, ведь в их распоряжении миллиарды единиц бесплатного сырья, то есть пользователей (кстати, этим словом users называют людей только в двух областях: наркоиндустрии и индустрии программного обеспечения).

Настало время положить этому конец. И вернуть данные под свой контроль. В этом суть концепции личных хранилищ данных (personal data services или personal data store, PDS).

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

Разумеется, эти данные должны храниться за всю жизнь человека в абсолютно надёжном хранилище, к которому нет доступа корпораций и посторонних лиц. Нужны удобные инструменты для анализа и статистики. Нужны персональные нейросети для обработки данных и предсказания личных решений (например, для рекомендации музыкальных групп, блюд кухни или людей для общения).

К сожалению, единого общепринятого и удобного подхода к созданию таких решений пока нет. Но идёт работа в нужном направлении.

Инфраструктура для хранения персональных данных


Некоторые исследователи думают над концептуальным решением проблемы, то есть над тем, какой должна быть вся инфраструктура для персональных данных.

Например, разработчик @karlicoss описал концепцию такой инфраструктуры.

Основные принципы:

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

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

Что ещё предусмотреть в концепции PDS? Должны быть API для получения любых данных из персонального архива.

Логично, что самый простой способ работы с данными когда они непосредственно лежат в вашей файловой системе. В реальности персональные данные разбросаны по десяткам разных сервисов и программ, что очень затрудняет работу с ними. Для начала желательно извлечь их оттуда и сохранить локально. Да, теоретически это необязательно, ведь продвинутые PDS могут поддерживать работу с разными источниками данных в разных форматах. Например, данные могут храниться в разных облачных хранилищах, извлекаться через сторонние API из других сервисов и программ. Правда, нужно понимать, что это ненадёжные хранилища.

Например, Twitter через свои API отдаёт 3200 последних твитов, Chrome хранит историю 90 дней, а Firefox удаляет её на основе хитрого алгоритма. Ваш аккаунт в облачном сервисе могут в любой момент закрыть, а все данные удалить. То есть сторонние сервисы никак не предполагают долговременное хранение данных.


Расчётный лист вавилонского рабочего, датирован 3000 г до н. э. Пример долговременного хранения личной информации

Экспорт данных в личное хранилище


В качестве промежуточного решения предлагается концепция зеркала данных (data mirror).

Это специальное приложение, которое непрерывно работает на клиентской стороне в фоновом режиме и постоянно синхронизирует локальный архив со всеми внешними сервисами. Приложение как бы высасывает ваши данные из разных программ и веб-сервисов, сохраняя в открытый машиночитаемый формат вроде JSON/SQLite. По сути, оно строит на диске это самое личное хранилище, которое в будущем должно вместить в себя все виды персональной информации.

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

Эту работу приходится делать в полуручном режиме.

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

Люди понимают, насколько важно сохранить навсегда личные фотографии. Но мало кто осознаёт то же самое для истории чатов во всех мессенджеров, а ведь это поистине бесценная летопись жизни человека. Эта информация с годами стирается из человеческой памяти.

Например, чаты ICQ хранились в простом текстовом виде, так что не нужно было предпринимать особых усилий для их сохранения. Так вот, если сейчас прочитать свои чаты из 90-х годов, то вы откроете заново целый пласт личной истории, которую уже давно забыли. Пожалуй, это очень важная часть персонального архива.

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


Визуализация более миллиона электронных писем, которые Стивен Вольфрам отправил с 1989 года, показывает нарушения сна в годы напряжённой работы

Чтобы упростить себе регулярный экспорт/скрапинг личных данных из разных программ @karlicoss написал ряд скриптов для Reddit, Messenger/Facebook, Spotify, Instapaper, Pinboard, Github и других сервисов, которыми он пользуется.

В идеале, эти программы позволяют найти любое сообщение или заметку, то есть практически любую вашу мысль из прошлого, где бы она ни была зафиксирована в чате Telegram или Вконтакте, комментарии на Хабре, прочитанной книге или в коде, который вы писали. Вся информация хранится в единой базе с полнотекстовым поиском.

Софт


Вместо облачных корпоративных сервисов нужно переходить на локально-ориентированный софт (local-first software). Он так называется по контрасту с облачными приложениями.

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



Предусмотрена синхронизация локальных данных между всеми устройствами, полный контроль человека над его данными, работа в офлайне в первую очередь (движение Offline First), безболезненное решение конфликтов в совместной работе, максимальная защищённость информации, длительная сохранность данных для наших потомков, как тот расчётный лист вавилонского рабочего выше (кстати, в 2016 году расшифровка текста выявила, что труд вавилонского рабочего оплатили спиртным напитком, а конкретно пивом).

Таким образом, локально-ориентированный софт соответствует всем семи обозначенным принципам. По мнению специалистов, лучше всего для реализации такого программного обеспечения подходят структуры данных типа CRDT (conflict-free replicated data type). Эти структуры данных могут реплицироваться среди множества компьютеров в сети, причём реплики обновляются независимо и конкурентно без координации между ними, но при этом всегда сохраняется математическая возможность устранить несогласованность. Это модель сильной согласованности в конечном счёте (Strong Eventual Consistency).



Благодаря такой модели согласованности структуры данных CRDT похожи на системы контроля версий типа Git. Для лучшего знакомства с CRDT можно почитать статью Алексея Бабулевича.

Гит-скрапинг


Идея освобождения личных данных из корпоративного рабства с долговременным локальным хранением в последнее время приобретает особую популярность. Жизнь показала, что от коммерческих веб-сервисов ничего хорошего ждать не приходится. Поэтому отдельные разработчики пытаются создать примеры личных информационных хранилищ.

Например, FOSS-разработчик и консультант Саймон Уиллисон работает над двумя инструментами Datasette и Dogsheep, которые весьма полезны для личных хранилищ.

Datasette веб-приложение для обработки данных и публикации их в читаемом формате, в виде интерактивного веб-сайта (демо). Это лишь один элемент большой экосистемы Datasette опенсорсных инструментов для сбора, анализа и публикации интересных данных. Экосистема делится на две части: инструменты для построения баз данных SQLite (для использования с Datasette) и плагины, которые расширяют функциональность Datasette.

Разные плагины позволяют комбинировать данные друг с другом. Например, накладывать координаты объектов из одной базы данных на географическую карту.

Уиллисон экспериментирует с регулярным скрапингом разных сайтов с публикацией данных в репозитории GitHub. Получается срез данных по изменению некоего объекта во времени. Он называет эту технику гит-скрапингом. В дальнейшем собранные данные можно преобразовать и Datasette.

См. примеры гит-скрапинга на Github. Это одна из ключевых техник для наполнения информацией личного хранилища данных в стандартном открытом формате для долговременного хранения.



Предстоит ещё долгий путь, чтобы освободить свои данные и создать инфраструктуру для надёжного и безопасного хранения личной информации. В будущем можно представить, что эта информация включит в себя также воспоминания и эмоции, которые снимаются с нейро-компьютерного интерфейса типа Neuralink, так что в совокупности хранилище будет практически полностью отражать личность владельца, представляя своеобразный цифровой жизненный слепок или аватар человека.

Очень вдохновляют отдельные примеры героических усилий по цифровизации своей жизни, как у Стивена Вольфрама. На фотографии слева домашний RIAD-массив с его хранилищем информации за 40 лет.

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



На правах рекламы


Закажите и сразу работайте! Создание VDS любой конфигурации в течение минуты, в том числе серверов для хранения большого объёма данных до 4000 ГБ, CEPH хранилище на основе быстрых NVMe дисков от Intel. Эпичненько :)

Подробнее..

Обзор разработки дополнений для amoCRM, с использованием webHook и виджетов

09.03.2021 18:08:33 | Автор: admin

Содержание

  1. WebHook

  2. Виджет

  3. Техническая поддержка

  4. Итог

Мы не использовали все возможности разработки под amoCRM, ограничились приватным виджетом и webHook, поэтому ниже речь пойдет именно об этом

WebHook

К каждому аккаунту(на пробном только в течении 14 дней)можно установитьwebHook,документация подробно описывает процесс. Разработка каких-либо интеграций при этом не нужна.

В нашем случае было достаточно информации одобавлении сделки.

На сервере по указанному url в файле(в данном случаеindex.php)первым делом необходимо сырые POST данные преобразовать из json в массив php:

//если в сырых POST данных первый символ { значит это jsonif(strlen($sRawPost) > 0 && $sRawPost[0] == "{"){    $sDecode = json_decode($sRawPost, true);    if($sDecode !== null)         $_POST = $sDecode;}

ВgetпараметрыwebHookпри создании новой сделки ничего не приходит, а вpostпримерно следующее:

{    "leads": {        "add": [            {                "id": 4564454,                "name": "Название товара",                "status_id": 7534534,                "price" => 0,                "responsible_user_id": 453453453,                "last_modified": 1612007407,                "modified_user_id": 0,                "created_user_id": 0,                "date_create": 1612007407,                "pipeline_id": 4546445,                "tags": [                    {                        "id": 7899                        "name": tilda                    }                ]            }        ],        "account_id": 19277260        "custom_fields": [            {                "id": 448797,                "name": "name_field",                "code": "code_field",                "values": [                    {                        "value": "string"                    }                ]            }        ],        "created_at": 1612007407,        "updated_at": 1612007407    },    "account": [        {            "subdomain": "subdomain",            "id": 19217260,            "_links": [                "self": "https://subdomain.amocrm.ru"            ]        }    ]}

Очевидно чтоидентифицировать аккаунтиз которого была отправка запроса можно по ключуaccount, аleads["add"][0]["account_id"] == account["id"].

Вleads["add"][0]["tags"]находятсяспециальные метки, которые можно присвоить сделке, и по которым на стороне принимающего сервера можно как-то идентифицировать, в нашем случае нужен был тег со значениемtilda.

Но больший интерес представляетleads["add"][0]["custom_fields"]- этомассив произвольных полей сделки.

Изначально у сделки небольшой список полей и есть только "Основная" группа полей, в которую можнодобавить новые поля, а можнодобавить новую группу и в нее добавить новые поля- есть категоризация полей на стороне веб-интерфейса.

Для редактирования полей сделки нужно зайти в любую сделку или в интерфейс создания новой сделки, затем перейти во вкладку "Настроить".

Редактирование полей сделкиРедактирование полей сделки

Новое поле сделки может быть скрыто из веб-интерфейса для редактирования и может быть доступно только на стороне API.

Для работы с полями сделки на стороне сервера принимающего запрос можно так:

$aAdd = $_POST['leads']['add'][0]; //извлекаем имена полей$aNameCustomFields = array_column($aAdd['custom_fields'], 'name'); //здесь пишем проверку наличия нужных полей //получаем значение поля$idOrder = $aAdd['custom_fields'][array_search('ORDERID', $aNameCustomFields)]['values'][0]['value'];
Добавление нового поля сделкиДобавление нового поля сделки

А дальше все зависит от целей использования webHook :)

Изначально мы делали эту интеграцию для клиента на Tilda, так как у клиента было настроено получение заявок из Tilda в amoCRM. Нам необходимо было отслеживать факт поступления оплаты, однако в amoCRM не поступают такие данные ифакт оплаты не удалось выявить, даже в самом интерфейсе CRM

Виджет

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

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

Затем необходимо создатьструктуру виджетасостоящую из директорий и файлов.

Код виджета пишется наjavascript, шаблоны виджета наtwig, в js доступенjquery, есть возможность использованияcss

В директории виджета необходимо наличие файлаmanifest.json- файла конфигурации виджета,в документации есть подробное описание, аздесьописаны типы полей. Не забываем олокализацииi18n.

Стоит учесть что виджет подключается только к темобластям сайта, которые будут указаны вmanifest.json

В документации есть разделWEB SDKкоторый также посвящен созданию виджетов.

Не вижу смысла описывать процесс написания виджета, а также его возможности, так какдокументация исчерпывающая:)

Если виджет используетajax запросы со стороннего сервера(например как было у нас, виджет обращался к нашему серверу), то сервер должен отправлять заголовокAccess-Control-Allow-Origin: *:

header("Access-Control-Allow-Origin: *");

Разработка виджета осуществляется локально, на машине разработчика, тестирование виджета возможно только череззагрузку архива виджетана странице созданной интеграции.

Это не очень удобно, к тому же вmanifest.jsonкаждый разпри загрузке виджета нужно менять версиюwidget.version, иначе обновление виджета произойдет не сразу.

Техническая поддержка

Через чат amoCRM на всех страницах сайта CRM можно быстро получить ответы на многие вопросы. CRM платная для использования, но предоставляется бесплатный доступ на 14 дней. Однако,мы не собирались пользоваться самой CRM, а лишьпредоставлять нашу интеграцию. Возможность разработки виджета возможна только в течении 14 дней. После истечения периода, нам понадобилось продлить пробный период, обратившись в онлайн чат мы получили дополнительные 10 дней. Однако, позже через онлайн-чат удалось выяснить чтодля разработчиков публичных интеграций естьспециальный бесплатныйтехнический аккаунт. Также во время разработки нам потребовалось узнатьip адреса серверов amoCRM, с которых они присылают webHook на наш сервер, тех. поддержка через онлайн чат любезно их предоставила.На момент написания статьи, ip адреса серверов amoCRM не находятся в публичном доступе, узнать информацию о них можно через онлайн-чат на сайте.

Итог

В целом мне понравилось разработка для amoCRM, понятная и объемная документация с примерами, однако загрузка виджета доставляет определенные неудобства.

Подробнее..

Перевод JSON с опциональными полями в Go

22.11.2020 18:15:46 | Автор: admin

Перевод статьи подготовлен специально для будущих студентов курса "Golang Developer. Professional".


Один из наиболее распространенных видов данных, хранящихся в конфигурационных файлах, - это опции. В этой статье я расскажу о некоторых нюансах, которые необходимо учитывать при сохранении опций в JSON и их анмаршалинге в Go.

В частности, наиболее важное различие между опциями и любыми другими данными заключается в том, что опции зачастую, извините за каламбур опциональны. Наша программа может иметь большое количество всевозможных параметров конфигурации (опций), но нам может понадобиться инициировать какую-либо конкретную инвокацию только с ограниченным их подмножеством, оставив для всего остального значения по умолчанию.

Основы - частичный анмаршалинг, omitempty и неизвестные поля

Начнем с основ. Рассмотрим следующую структуру, которая представляет из себя опции произвольной программы:

type Options struct {  Id      string `json:"id,omitempty"`  Verbose bool   `json:"verbose,omitempty"`  Level   int    `json:"level,omitempty"`  Power   int    `json:"power,omitempty"`}

Эта структура содержит всего 4 опции, но в реальных программах их могут быть десятки.

Предположим, мы хотим указать эти опции в JSON-файле конфигурации. Полный список опций может выглядеть примерно так:

{  "id": "foobar",  "verbose": false,  "level": 10,  "power": 221}

Если в ваших файлах конфигурации всегда указаны все опции, то говорить дальше особо не о чем. Просто вызовите json.Unmarshal, и дело с концом.

На практике же достаточно редко все бывает так просто. Нам следует обработать сразу несколько особых случаев:

  1. В JSON-конфиге могут отсутствовать некоторые поля, и мы хотим, чтобы наша структура в Go имела для них значения по умолчанию.

  2. JSON-конфиг может иметь дополнительные поля, которых в нашей структуре нет. В зависимости от обстоятельств мы можем либо проигнорировать их, либо отрапортовать об ошибке.

В случае (1) пакет json Go будет присваивать значения только полям, найденным в JSON; другие поля просто сохранят свои нулевые значения Go. Например, если бы в JSON вообще не было поля level, в анмаршаленной структуре Options Level будет равен 0. Если такое поведение нежелательно для вашей программы, переходите к следующему разделу.

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

{  "id": "foobar",  "bug": 42}

json.Unmarshal без проблем распарсит это в Options, установив Id в значение "foobar", Level и Power в 0, а Verbose в false. Он проигнорирует поле bug.

В одних случаях такое поведение является желательным, в других - нет. К счастью, пакет json позволяет настроить это, предоставляя явную опцию для JSON-декодера в виде DisallowUnknownFields:

dec := json.NewDecoder(bytes.NewReader(jsonText))dec.DisallowUnknownFields()var opts Optionsif err := dec.Decode(&opts2); err != nil {  fmt.Println("Decode error:", err)}

Теперь парсинг вышеупомянутого фрагмента JSON приведет к ошибке.

Наконец, вы можете заметить, что наша структура Options имеет тег omitempty, указанный для всех полей. Это означает, что поля с нулевыми значениями не будут внесены в JSON. Например:

opts := Options{  Id:    "baz",  Level: 0,}out, _ := json.MarshalIndent(opts, "", "  ")fmt.Println(string(out))

Выведет:

{  "id": "baz"}

Потому что все остальные поля имеют нулевые значения. Если вы напротив хотите всегда вносить все поля, не указывайте omitempty.

Установка значений по умолчанию

В приведенном выше примере мы видели, что отсутствующие в JSON-представлении поля будут преобразованы в нулевые значения Go. Это нормально, если значения ваших параметров по умолчанию также являются их нулевыми значениями, что не всегда так. Что, если значение по умолчанию Power должно быть 10, а не 0? То есть, когда JSON не имеет поля power, вы хотите установить Power равным 10, но вместо этого Unmarshal устанавливает его в ноль.

Вы можете подумать - это же элементарно! Я буду устанавливать Power в его значение умолчанию 10 всякий раз, когда он маршалится из JSON как 0! Но подождите. Что произойдет, если в JSON указано значение 0?

На самом деле, эта проблема решается наоборот. Мы установим значения по умолчанию сначала, а затем позволим json.Unmarshal перезаписать поля по мере необходимости:

func parseOptions(jsn []byte) Options {  opts := Options{    Verbose: false,    Level:   0,    Power:   10,  }  if err := json.Unmarshal(jsn, &opts); err != nil {    log.Fatal(err)  }  return opts}

Теперь вместо прямого вызова json.Unmarshal на Options, нам придется вызывать parseOptions.

В качестве альтернативы мы можем хитро спрятать эту логику в пользовательском методе UnmarshalJSON для Options:

func (o *Options) UnmarshalJSON(text []byte) error {  type options Options  opts := options{    Power: 10,  }  if err := json.Unmarshal(text, &opts); err != nil {    return err  }  *o = Options(opts)  return nil}

В этом методе любой вызов json.Unmarshal для типа Options будет заполнять значение по умолчанию Power правильно. Обратите внимание на использование псевдонимного типа options - это нужно для предотвращения бесконечной рекурсии в UnmarshalJSON.

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

Второй недостаток заключается в том, что это работает только для простых случаев. Если наша структура Options содержит срез или мапу других структур, мы не сможем проставить значения по умолчанию таким образом. Примите во внимание:

type Region struct {  Name  string `json:"name,omitempty"`  Power int    `json:"power,omitempty"`}type Options struct {  Id      string `json:"id,omitempty"`  Verbose bool   `json:"verbose,omitempty"`  Level   int    `json:"level,omitempty"`  Power   int    `json:"power,omitempty"`  Regions []Region `json:"regions,omitempty"`}

Если мы хотим заполнить значения по умолчанию для Power каждой Region, мы не сможем сделать это на уровне Options. Мы должны написать собственный метод анмаршалинга для Region. Это сложно масштабировать для произвольно вложенных структур - распространение нашей логики значений по умолчанию на несколько методов UnmarshalJSON не оптимально.

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

Значения по умолчанию и поля-указатели

Мы можем определить нашу структуру Options как:

type Options struct {  Id      *string `json:"id,omitempty"`  Verbose *bool   `json:"verbose,omitempty"`  Level   *int    `json:"level,omitempty"`  Power   *int    `json:"power,omitempty"`}

Это очень напоминает исходное определение, за исключением того, что все поля теперь являются указателями. Предположим, у нас есть следующий текст JSON:

{  "id": "foobar",  "verbose": false,  "level": 10}

Обратите внимание, что указаны все поля, кроме "power". Мы можем анмаршалить это как обычно:

var opts Optionsif err := json.Unmarshal(jsonText, &opts); err != nil {  log.Fatal(err)}

Но теперь мы можем четко различить поля, которые не были указаны вообще (они будут анмаршалены в nil указатель), и поля, которые были указаны с нулевыми значениями (они будут анмаршалены в валидные указатели на значения с нулевыми значениями). Например, мы можем написать следующую обертку парсера для анмаршалинга Options и установки значений по умолчанию по мере необходимости:\

func parseOptions(jsn []byte) Options {  var opts Options  if err := json.Unmarshal(jsonText, &opts); err != nil {    log.Fatal(err)  }  if opts.Power == nil {    var v int = 10    opts.Power = &v  }  return opts}

Обратите внимание, как мы устанавливаем opts.Power; это одно из неудобств работы с указателями, потому что в Go нет синтаксиса, позволяющего принимать адреса литералов встроенных типов, таких как int. Однако это не слишком большая проблема, поскольку существуют простые вспомогательные функции, которые могут сделать нашу жизнь чуть более приятной:

func Bool(v bool) *bool       { return &v }func Int(v int) *int          { return &v }func String(v string) *string { return &v }// и т.д. ...

Имея это под рукой, мы могли бы просто написать opts.Power = Int(10).

Самая полезная особенность этого подхода заключается в том, что он не заставляет нас назначать значения по умолчанию в месте, где парсится JSON. Мы можем передать Options в пользовательский код, и пусть уже он разбирается со значениями по умолчанию, когда ему встречаются поля с nil.

Так являются ли указатели волшебным решением нашей проблемы - отличить неопределенные значения от нулевых значений? Вроде того. Указатели, безусловно, являются жизнеспособным решением, которое должно хорошо работать. Официальный пакет Protobuf использует тот же подход для protobuf-ов proto2, проводящих различие между необходимыми и опциональными полями. Так что этот метод прошел проверку боем!

Тем не менее, это решение не идеально. Прежде всего, несмотря на то, что Go большую часть времени действительно хорошо скрывает дополнительную синтаксическую нагрузку при работе с указателями, в некоторых случаях немного возни все же просачивается (например, получение адреса встроенного литерала, как показано выше). Еще одна потенциальная проблема - производительность. Указатели часто означают динамическое выделение памяти и могут вызывать проблемы с производительностью в некоторых сценариях, хотя, если говорить о структурах параметров, это вряд ли станет проблемой.

Узнать подробнее о курсе.

Подробнее..

Перевод Проектирование API почему для представления отношений в API лучше использовать ссылки, а не ключи

03.02.2021 10:04:43 | Автор: admin
Привет, Хабр!

У нас выходит долгожданное второе издание книги "Веб-разработка с применением Node и Express".



В рамках исследования этой темы нами была найдена концептуальная статья о проектировании веб-API по модели, где вместо ключей и значений базы данных применяются ссылки на ресурсы. Оригинал из блога Google Cloud, добро пожаловать под кат.


Когда мы занимаемся моделированием информации, ключевой вопрос в том, как обозначить отношение и взаимоотношение между двумя сущностями. Описывать закономерности, наблюдаемые в реальном мире, в терминах сущностей и их взаимоотношений это фундаментальная идея, восходящая, как минимум, к Древней Греции. Она играет основополагающую роль и в современной IT-отрасли.

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

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

Наиболее распространенный способ, применяемый разработчиками API для выражения взаимоотношений это предоставление ключей базы данных или прокси для них в полях тех сущностей, которые ассоциированы с этими ключами. Однако, как минимум в одном классе API (веб-ориентированных) такому подходу есть более предпочтительная альтернатива: использование веб-ссылок.

Согласно стандарту Internet Engineering Task Force (IETF), веб-ссылку можно представить как инструмент для описания отношений между страницами в вебе. Наиболее известные веб-ссылки те, что фигурируют на HTML-страницах и заключаются в элементы link или anchor, либо в заголовки HTTP. Но ссылки также могут фигурировать и в ресурсах API, а при использовании их вместо внешних ключей существенно сокращается объем информации, которую поставщику API приходится дополнительно документировать, а пользователю изучать.

Ссылка это элемент в одном веб-ресурсе, содержащий указание на другой веб-ресурс, а также название взаимоотношения между двумя ресурсами. Ссылка на другую сущность записывается в особом формате, именуемом уникальный идентификатор ресурса (URI), для которого существует стандарт IETF. В этом стандарте словом ресурс называется любая сущность, на которую указывает URI. Название типа отношения в ссылке можно считать аналогичным имени того столбца базы данных, в котором содержатся внешние ключи, а URI в ссылке аналогичен значению внешнего ключа. Наиболее полезны из всех URI те, по которым можно получить информацию о ресурсе, на который проставлена ссылка, при помощи стандартного веб-протокола. Такие URI называются унифицированный указатель ресурса (URL) и наиболее важной разновидностью URL для API являются HTTP URL.

В то время как ссылки не находят широкого применения в API, некоторые очень известные веб-API все-таки основаны на HTTP URL, используемых в качестве средства представления взаимоотношений. Таковы, например, Google Drive API и GitHub API. Почему так складывается? В этой статье я покажу, как на практике строится использование внешних ключей API, объясню их недостатки по сравнению с использованием ссылок, и расскажу, как преобразовать дизайн, использующий внешние ключи, в такой, где применяются ссылки.

Представление взаимоотношений при помощи внешних ключей


Рассмотрим популярное учебное приложение зоомагазин. В этом приложении хранятся записи для отслеживания информации о питомцах и их владельцах. У питомцев есть атрибуты, в частности, кличка, вид и порода. У владельцев есть имена и адреса. Каждый питомец взаимосвязан со своим владельцем, а обратное взаимоотношение позволяет найти всех питомцев конкретного владельца.

В типичном дизайне, основанном на использовании ключей, API зоомагазина предоставляет два ресурса, которые выглядят вот так:



Взаимоотношение между Лесси и Джо выражается так: в представлении Лесси Джо обозначен как обладатель имени и значения, соответствующих владельцу. Обратное взаимоотношение не выражено. Значение владельца равно 98765, это внешний ключ. Вероятно, это самый настоящий внешний ключ базы данных то есть, перед нами значение первичного ключа из какой-то строки какой-то таблицы базы данных. Но, даже если реализация API немного изменит значения ключей, она все равно сближается по основным характеристикам с внешним ключом.
Значение 98765 не слишком годится для непосредственного использования клиентом. В наиболее распространенных случаях клиенту, воспользовавшись этим значением, нужно составить URL, а в документации к API необходимо описать формулу для выполнения такого преобразования. Как правило, это делается путем определения шаблона URI, вот так:

/people/{person_id}

Обратное взаимоотношение любимцы принадлежат владельцу также можно предоставить в API, реализовав и документировав один из следующих шаблонов URI (различия между ними только стилистические, а не по существу):

/pets?owner={person_id}
/people/{person_id}/pets


В API, спроектированных по такому принципу, обычно требуется определять и документировать много шаблонов URI. Наиболее популярным языком для определения таких шаблонов является не тот, что задан в спецификации IETF, а язык OpenAPI (ранее известный под названием Swagger). До версии 3.0 в OpenAPI не существовало способа указать, какие значения полей могут быть вставлены в какие шаблоны, поэтому часть документации требовалось составлять на естественном языке, а что-то приходилось угадывать клиенту. В версии 3.0 OpenAPI появился новый синтаксис под названием links, призванный решить эту проблему, но для того, чтобы пользоваться этой возможностью последовательно, надо потрудиться.

Итак, при всей распространенности этого стиля, он требует от поставщика документировать, а от клиента выучить и использовать значительное количество шаблонов URI, не вполне хорошо описанных в нынешних спецификациях API. К счастью, есть вариант получше.

Представление взаимоотношений при помощи ссылок


Что, если бы ресурсы, показанные выше, были видоизменены следующим образом:



Основное отличие заключается в том, что в данном случае значения выражены при помощи ссылок, а не при помощи значений внешнего ключа. Здесь ссылки записаны обычным JSON, в формате пар имя/значение (ниже есть раздел, в котором обсуждаются и другие подходы к написанию ссылок в JSON).

Обратите внимание: обратное взаимоотношение, то есть, от питомца к владельцу, теперь тоже реализовано явно, поскольку к представлению Joel добавлено поле "pets".

Изменение "id" на "self", в сущности, не является необходимым или важным, но существует соглашение, что при помощи "self" идентифицируется ресурс, чьи атрибуты и взаимоотношения указаны другими парами имя/значение в том же объекте JSON. "self" это имя, зарегистрированное в IANA для этой цели.

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

Теперь серверу ничто не мешает в любой момент изменить формат новых URL, не затронув клиентов (разумеется, сервер должен продолжать соблюдать все URL, сформулированные ранее). URL, передаваемый клиенту сервером, должен будет включать первичный ключ сущности, указанный в базе данных, плюс некоторую информацию о маршрутизации. Но, поскольку клиент просто повторяет URL, отзываясь серверу, и клиенту никогда не приходится выполнять синтаксический разбор URL, клиентам не обязательно знать, как форматировать URL. В результате снижается степень связанности между клиентом и сервером. Сервер даже может прибегнуть к обфускации собственных URL при помощи base64 или подобных кодировок, если хочет подчеркнуть клиентам, что те не должны гадать, каков формат URL, либо делать выводы о значении URL по их формату.

В предыдущем примере я использовал в ссылках относительную форму записи URI, например, /people/98765. Возможно, клиенту было бы немного удобнее (хотя, автору при форматировании этого поста было не слишком сподручно), если бы я выразил URI в абсолютной форме, напр. pets.org/people/98765. Клиентам необходимо знать лишь стандартные правила URI, определенные в спецификациях IETF, чтобы преобразовывать такие URI из одной формы в другую, поэтому выбор конкретной формы URI не так важен, как могло бы показаться на первый взгляд. Сравните эту ситуацию с описанным выше преобразованием из внешнего ключа в URL, для чего требовались конкретные знания об API зоомагазина. Относительные URL несколько удобнее для тех, кто занимается реализацией сервера, о чем рассказано ниже, но абсолютные URL, пожалуй, удобнее для большинства клиентов. Возможно, именно поэтому в API Google Drive и GitHub используются абсолютные URL.

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

Подводные камни


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

Перед многими реализациями API поставлены обратные прокси, обеспечивающие безопасность, балансировку нагрузки и пр. Некоторые прокси любят переписывать URL. Когда в API для представления взаимоотношений используются внешние ключи, единственный URL, который требуется переписать в прокси это главный URL запроса. В HTTP этот URL разделяется между адресной строкой (первая строка заголовка) и заголовком хоста.

В API, использующем ссылки для выражения взаимоотношений, будут и другие URL в заголовках и в теле как запроса, так и отклика, и эти URL тоже понадобится переписывать. Есть несколько различных способов справиться с этим:

  1. Не переписывайте URL в прокси. Я стараюсь избегать переписывания URL, но в вашей среде может не быть такой возможности.
  2. В прокси аккуратно найдите и переназначьте им формат везде, где они фигурируют в запросе или в отклике. Я так никогда не делал, поскольку мне это кажется сложным, чреватым ошибками и неэффективным, но кто-то, возможно, так поступает.
  3. Записывайте все ссылки в относительном виде. Можно не только встроить во все прокси некоторые возможности по перезаписи URL; более того, относительные URL могут упростить использование одного и того же кода в тестировании и в продакшене, так как код не придется конфигурировать и знать для этого его хост-имя. Если писать ссылки с использованием относительных URL, то есть, с единственным ведущим слэшем, как я показал в примере выше, то возникают некоторые минусы как для сервера, так и для клиента. Но в таком случае в прокси появляется лишь возможность сменить хост-имя (точнее, те части URL, которые называются схемой и источником), но не путь. В зависимости от того, как построены ваши URL, вы можете реализовать в прокси некоторую возможность переписывать пути, если готовы писать ссылки с использованием относительных URL без ведущих слэшей, но я так никогда не делал, поскольку полагаю, что серверам будет сложно записывать такие URL как следует.


Относительными URL без ведущих слэшей также сложнее пользоваться клиентам, поскольку они должны работать со стандартизированной библиотекой, а не обходиться простым сцеплением строк для обработки этих URL, а также тщательно понимать и сохранять базовый URL.

Использование стандартизированной библиотеки для обращения с URL в любом случае, хорошая практика для клиентов, но на многих клиентах так не делается.
При использовании ссылок вам также, возможно, придется перепроверить версионирование вашего API. Во многих API принято ставить номера версий в URL, вот так:

/v1/pets/12345
/v2/pets/12345
/v1/people/98765
/v2/people/98765


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

Эта ситуация весьма напоминает возможность просмотра одного и того же документа на нескольких естественных языках, для чего существует веб-стандарт; какая жалость, что нет подобного стандарта для версий. Присваивая каждой версии собственный URL, вы повышаете каждую версию по статусу до полноценного веб-ресурса. Нет ничего дурного с версионными URL такого рода, но они не подходят для выражения ссылок. Если клиент запрашивает Лесси в формате версии 2, это не означает, что он также хочет получить в формате 2 и информацию о Джо, хозяине Лесси, поэтому сервер не может выбрать, какой номер версии поставить в ссылку.

Возможно, формат 2 для описания владельцев даже не будет предусмотрен. Также нет концептуального смысла в том, чтобы использовать в ссылках конкретную версию URL ведь Лесси принадлежит не конкретной версии Джо, а Джо как таковому. Поэтому, даже если вы предоставляете URL в формате /v1/people/98765 и идентифицируете таким образом конкретную версию Джо, то также должны предоставлять URL /people/98765 для идентификации самого Джо, и именно второй вариант использовать в ссылках. Другой вариант определить только URL /people/98765 и позволить клиентам выбирать конкретную версию, включая для этого заголовок запроса. Для этого заголовка нет никакого стандарта, но, если называть его Accept-Version, то такой вариант хорошо сочетается с именованием стандартных заголовков. Лично я предпочитаю использовать для версионирования заголовок и избегаю ставить в URL номера версий. но URL с номерами версий популярны, и я часто реализую и заголовок. и версионные URL, так как легче реализовать оба варианта, чем спорить, какой лучше. Подробнее о версионировании API можете почитать в этой статье.

Возможно, вам все равно потребуется документировать некоторые шаблоны URL


В большинстве веб-API URL нового ресурса выделяется сервером, когда новый ресурс создается при помощи метода POST. Если вы пользуетесь этим методом для создания ресурсов и указываете взаимоотношения при помощи ссылок, то вам не требуется публиковать шаблон для URI этих ресурсов. Однако, некоторые API позволяют клиенту контролировать URL нового ресурса. Позволяя клиентам контролировать URL новых ресурсов, мы значительно упрощаем многие паттерны написания скриптов API разработчикам клиентской части, а также поддерживаем сценарии, в которых API используется для синхронизации информационной модели с внешним источником информации. В HTTP для этой цели предусмотрен специальный метод: PUT. PUT означает создай ресурс по этому URL, если он еще не существует, а если он существует обнови его. Если ваш API позволяет клиентам создавать новые сущности при помощи метода PUT, то вы должны документировать правила составления новых URL, возможно, включив для этого шаблон URI в спецификацию API. Также можно предоставить клиентам частичный контроль над URL, включив первичное ключ-подобное значение в тело или заголовки POST. В таком случае не требуется шаблон URI для POST как такового, но клиенту все равно придется выучить шаблон URI, чтобы полноценно пользоваться достигаемой в результате предсказуемостью URI.

Другой контекст, в котором целесообразно документировать шаблоны URL это случай, когда API позволяет клиентам кодировать запросы в URL. Не каждый API позволяет вам запрашивать свои ресурсы, но такая возможность может быть очень полезна для клиентов, и естественно позволить клиентам кодировать запросы в URL и извлекать результаты методом GET. В следующем примере показано, почему.

В вышеприведенном примере мы включили следующую пару имя/значение в представление Джо:

"pets": "/pets?owner=/people/98765"

Клиенту, чтобы пользоваться этим URL, не требуется что-либо знать о его структуре кроме того, что он был записан в соответствии со стандартными спецификациями. Таким образом, клиент может получить по этой ссылке список питомцев Джо, не изучая для этого никакой язык запросов. Также отсутствует необходимость документировать в API форматы его URL но только в случае, если клиент сначала сделает запрос GET к /people/98765. Если же, кроме того, в API зоомагазина документирована возможность выполнения запросов, то клиент может составить такой же или эквивалентный URL запроса, чтобы извлечь питомцев интересующего его владельца, не извлекая перед этим самого владельца достаточно будет знать URI владельца. Возможно, даже важнее, что клиент может формировать и запросы, подобные следующим, что в ином случае было бы невозможно:

/pets?owner=/people/98765&species=Dog
/pets?species=Dog&breed=Collie


Спецификация URI описывает для этой цели часть HTTP URL, называемую "компонент запроса" это участок URL после первого ? и до первого #. Стиль запрашивания URI, который я предпочитаю использовать всегда ставить клиент-специфичные запросы в компонент запроса URI. Но при этом допустимо выражать клиентские запросы и в той части URL, которая называется путь. Так или иначе, необходимо описать клиентам, как составляются эти URL вы фактически проектируете и документируете язык запросов, специфичный для вашего API. Разумеется, также можно разрешить клиентам ставить запросы в теле сообщения, а не в URL, и пользоваться методом POST, а не GET. Поскольку существует практический лимит по размеру URL превышая 4k байт, вы всякий раз испытываете судьбу рекомендуется поддерживать POST для запросов, даже если вы уже поддерживаете GET.

Поскольку запросы такая полезная возможность в API, и поскольку проектировать и реализовывать языки запросов непросто, появились такие технологии, как GraphQL. Я никогда не пользовался GraphQL, поэтому не могу рекомендовать его, но вы можете рассмотреть его в качестве альтернативы для реализации возможности запросов в вашем API. Инструменты для реализации запросов в API, в том числе, GraphQL, лучше всего использовать в качестве дополнения к стандартному HTTP API для считывания и записи ресурсов, а не как альтернативу HTTP.

И кстати Как лучше всего писать ссылки в JSON?


В JSON, в отличие от HTML, нет встроенного механизма для выражения ссылок. Многие по-своему понимают, как ссылки должны выражаться в JSON, и некоторые подобные мнения публиковались в более или менее официальных документах, но в настоящее время нет стандартов, ратифицированных авторитетными организациями, которые бы это регламентировали. В вышеприведенном примере я выражал ссылки при помощи обычных пар имя/значение, написанных на JSON предпочитаю такой стиль и, кстати, этот же стиль используется в Google Drive и GitHub. Другой стиль, который вам, вероятно, встретится, таков:
  {"self": "/pets/12345", "name": "Lassie", "links": [   {"rel": "owner" ,    "href": "/people/98765"   } ]}

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

Есть и другой стиль написания ссылок на JSON, который мне нравится, и он выглядит так:
 {"self": "/pets/12345", "name": "Lassie", "owner": {"self": "/people/98765"}}


Польза этого стиля в том, что он явно дает: "/people/98765" это URL, а не просто строка. Я изучил этот паттерн по RDF/JSON. Одна из причин освоить этот паттерн вам так или иначе придется им пользоваться, всякий раз, когда вы захотите отобразить информацию об одном ресурсе, вложенную в другом ресурсе, как показано в следующем примере. Если использовать этот паттерн повсюду, код приобретает красивое единообразие:

{"self": "/pets?owner=/people/98765", "type": "Collection",  "contents": [   {"self": "/pets/12345",    "name": "Lassie",    "owner": {"self": "/people/98765"}   } ]}


Более подробно о том, как лучше всего использовать JSON для представления данных, рассказано в статье Terrifically Simple JSON.

Наконец, в чем же разница между атрибутом и взаимоотношением?


Думаю, большинство читателей согласится, что в JSON отсутствует встроенный механизм для выражения ссылок, но существует и такой способ трактовки JSON, который позволяет утверждать обратное. Рассмотрим следующий JSON:

{"self": "/people/98765", "shoeSize": 10}

Принято считать, что shoeSize это атрибут, а не взаимоотношение, а 10 это значение, а не сущность. Правда, не менее логично утверждать, что строка '10 фактически является ссылкой, записанной специальной нотацией, предназначенной для ссылок на числа, до 11-го целого числа, которое само по себе является сущностью. Если 11-е целое число совершенно полноценная сущность, а строка '10' лишь указывает на нее, то пара имя/значение '"shoeSize": 10' концептуально является ссылкой, хотя, здесь и не используются URI.

То же можно утверждать и относительно булевых значений, и строк, поэтому все пары имя/значение в JSON можно трактовать как ссылки. Если такое восприятие JSON кажется вам оправданным, то естественно использовать простые пары имя/значение в JSON как ссылки на те сущности, на которые также можно указывать при помощи URL.

В более общем виде этот аргумент формулируется как нет фундаментальной разницы между атрибутами и взаимоотношениями. Атрибуты это просто взаимоотношения между сущностью или другой абстрактной или конкретной сущностью, например числом или цветом. Но исторически сложилось, что к их обработке относились по-особенному. Признаться, это достаточно абстрактный вариант восприятия мира. Так, если вы покажете кому-либо черного кота и спросите, сколько тут объектов большинство людей ответит вам, что всего один. Немногие скажут, что видят два объекта кота и его черный цвет а также взаимоотношение между ними.

Ссылки попросту лучше


Веб-API, передающие ключи базы данных, а не просто ссылки, сложнее в изучении, а также сложнее в использовании для клиентов. Также API первого рода теснее связывают клиент и сервер, требуя в качестве общего знаменателя более подробную информацию, и всю эту информацию нужно документировать и читать. Единственное преимущество API первого рода в том, что они настолько распространены, программисты с ними освоились, знают, как их создавать и как потреблять. Если вы стремитесь предоставить клиентам высококачественные API, не требующие тонн документации и максимально обеспечивающие независимость клиента от сервера, то подумайте о том, чтобы предоставлять в ваших веб-API ссылки, а не ключи базы данных.
Подробнее..

Мониторинг Tarantool логи, метрики и их обработка

28.12.2020 18:17:25 | Автор: admin

Tarantool это платформа in-memory вычислений с гибкой схемой данных. На её основе можно создать распределённое хранилище, веб-сервер, высоконагруженное приложение или, в конце концов, сервис, включающий в себя всё вышеперечисленное. Но какой бы ни была ваша промышленная задача, однажды настанет момент, когда её решение придётся мониторить. В этой статье я хочу дать обзор существующих средств для мониторинга приложения на базе Tarantool и пройтись по основным кейсам работы с ними.


Мониторинг Tarantool


Я работаю в команде, которая занимается разработкой, внедрением и поддержкой готовых решений на основе Tarantool. Для вывода наших приложений в эксплуатацию на контуре заказчика было необходимо не только разобраться в текущих возможностях мониторинга, но и доработать их. Большая часть доработок в результате вошла в те или иные стандартные пакеты. Данный материал является текстовой выжимкой этого опыта, и может пригодиться тем, кто решит пройти по той же тропе.


Настройка логов в Tarantool


Базовое конфигурирование и использование логов


Для работы с логами в приложениях на базе Tarantool существует пакет log. Это встроенный модуль, который присутствует в каждой инсталляции Tarantool. Процесс по умолчанию заполняет лог сообщениями о своём состоянии и состоянии используемых пакетов.


Каждое сообщение лога имеет свой уровень детализации. Уровень логирования Tarantool характеризуется значением параметра log_level (целое число от 1 до 7):


  1. SYSERROR.
  2. ERROR сообщения log.error(...).
  3. CRITICAL.
  4. WARNING сообщения log.warn(...).
  5. INFO сообщения log.info(...).
  6. VERBOSE сообщения log.verbose(...).
  7. DEBUG сообщения log.debug(...).

Значение параметра log_level N соответствует логу, в который попадают сообщения уровня детализации N и всех предыдущих уровней детализации < N. По умолчанию log_level имеет значение 5 (INFO). Чтобы настроить этот параметр при использовании Cartridge, можно воспользоваться cartridge.cfg:


cartridge.cfg( { ... }, { log_level = 6, ... } )

Для отдельных процессов настройка производится при помощи вызова box.cfg:


box.cfg{ log_level = 6 }

Менять значение параметра можно непосредственно во время работы программы.


Стандартная стратегия логирования: писать об ошибках в log.error() или log.warn() в зависимости от их критичности, отмечать в log.info() основные этапы работы приложения, а в log.verbose() писать более подробные сообщения о предпринимаемых действиях для отладки. Не стоит использовать log.debug() для отладки приложения, этот уровень диагностики в первую очередь предназначен для отладки самого Tarantool. Не рекомендуется также использовать уровень детализации ниже 5 (INFO), поскольку в случае возникновения ошибок отсутствие информационных сообщений затруднит диагностику. Таким образом, в режиме отладки приложения рекомендуется работать при log_level 6 (VERBOSE), в режиме штатной работы при log_level 5 (INFO).


local log = require('log')log.info('Hello world')log.verbose('Hello from app %s ver %d', app_name, app_ver) -- https://www.lua.org/pil/20.htmllog.verbose(app_metainfo) -- type(app_metainfo) == 'table'

В качестве аргументов функции отправки сообщения в лог (log.error/log.warn/log.info/log.verbose/log.debug) можно передать обычную строку, строку с плейсхолдерами и аргументы для их заполнения (аналогично string.format()) или таблицу (она будет неявно преобразована в строку методом json.encode()). Функции лога также работают с нестроковыми данными (например числами), приводя их к строке c помощью tostring().


Tarantool поддерживает два формата логов: plain и json:


2020-12-15 11:56:14.923 [11479] main/101/interactive C> Tarantool 1.10.8-0-g2f18757b72020-12-15 11:56:14.923 [11479] main/101/interactive C> log level 52020-12-15 11:56:14.924 [11479] main/101/interactive I> mapping 268435456 bytes for memtx tuple arena...

{"time": "2020-12-15T11:56:14.923+0300", "level": "CRIT", "message": "Tarantool 1.10.8-0-g2f18757b7", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/main.cc", "line": 514}{"time": "2020-12-15T11:56:14.923+0300", "level": "CRIT", "message": "log level 5", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/main.cc", "line": 515}{"time": "2020-12-15T11:56:14.924+0300", "level": "INFO", "message": "mapping 268435456 bytes for memtx tuple arena...", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/box\/tuple.c", "line": 261}

Настройка формата происходит через параметр log_format так же, как для параметра log_level. Подробнее о форматах можно прочитать в соответствующем разделе документации.


Tarantool позволяет выводить логи в поток stderr, в файл, в конвейер или в системный журнал syslog. Настройка производится с помощью параметра log. О том, как конфигурировать вывод, можно прочитать в документации.


Обёртка логов


Язык Lua предоставляет широкие возможности для замены на ходу практически любого исполняемого кода. Здесь я хотел бы поделиться способом помещения стандартных методов логирования в функцию-обёртку, который мы использовали при реализации сквозного логирования в своих приложениях. Стоит отметить, что способ этот расположен в "серой" зоне легальности, так что прибегать к нему стоит только при отсутствии возможностей решить проблему более элегантно.


local log = require('log')local context = require('app.context')local function init()    if rawget(_G, "_log_is_patched") then        return    end    rawset(_G, "_log_is_patched", true)    local wrapper = function(level)        local old_func = log[level]        return function(fmt, ...)            local req_id = context.id_from_context()            if select('#', ...) ~= 0 then                local stat                stat, fmt = pcall(string.format, fmt, ...)                if not stat then                    error(fmt, 3)                end            end            local wrapped_message            if type(fmt) == 'string' then                wrapped_message = {                    message = fmt,                    request_id = req_id                }            elseif type(fmt) == 'table' then                wrapped_message = table.copy(fmt)                wrapped_message.request_id = req_id            else                wrapped_message = {                    message = tostring(fmt),                    request_id = req_id                }            end            return old_func(wrapped_message)        end    end    package.loaded['log'].error = wrapper('error')    package.loaded['log'].warn = wrapper('warn')    package.loaded['log'].info = wrapper('info')    package.loaded['log'].verbose = wrapper('verbose')    package.loaded['log'].debug = wrapper('debug')    return trueend

Данный код обогащает информацию, переданную в лог в любом поддерживаемом формате, идентификатором запроса request_id.


Настройка метрик в Tarantool


Подключение метрик


Для работы с метриками в приложениях Tarantool существует пакет metrics. Это модуль для создания коллекторов метрик и взаимодействия с ними в разнообразных сценариях, включая экспорт метрик в различные базы данных (InfluxDB, Prometheus, Graphite). Материал основан на функционале версии 0.6.0.


Чтобы установить metrics в текущую директорию, воспользуйтесь стандартной командой:


tarantoolctl rocks install metrics 0.6.0

Чтобы добавить пакет в список зависимостей вашего приложения, включите его в соответствующий пункт rockspec-файла:


dependencies = {    ...,    'metrics == 0.6.0-1',}

Для приложений, использующих фреймворк Cartridge, пакет metrics предоставляет специальную роль cartridge.roles.metrics. Включение этой роли на всех процессах кластера упрощает работу с метриками и позволяет использовать конфигурацию Cartridge для настройки пакета.


Встроенные метрики


Сбор встроенных метрик уже включён в состав роли cartridge.roles.metrics.


Для включения сбора встроенных метрик в приложениях, не использующих фреймворк Cartridge, необходимо выполнить следующую команду:


local metrics = require('metrics')metrics.enable_default_metrics()

Достаточно выполнить её единожды на старте приложения, например поместив в файл init.lua.


В список метрик по умолчанию входят:


  • информация о потребляемой Lua-кодом RAM;
  • информация о текущем состоянии файберов;
  • информация о количестве сетевых подключений и объёме сетевого трафика, принятого и отправленного процессом;
  • информация об использовании RAM на хранение данных и индексов (в том числе метрики slab-аллокатора);
  • информация об объёме операций на спейсах;
  • характеристики репликации спейсов Tarantool;
  • информация о текущем времени работы процесса и другие метрики.

Подробнее узнать о метриках и их значении можно в соответствующем разделе документации.


Для пользователей Cartridge также существует специальный набор встроенных метрик для мониторинга состояния кластера. На данный момент он включает в себя метрику о количестве проблем в кластере, разбитых по уровням критичности (аналогична Issues в WebUI Cartridge).


Плагины для экспорта метрик


Пакет metrics поддерживает три формата экспорта метрик: prometheus, graphite и json. Последний можно использовать, например, в связке Telegraf + InfluxDB.


Чтобы настроить экспорт метрик в формате json или prometheus для процессов с ролью cartridge.roles.metrics, добавьте соответствующую секцию в конфигурацию кластера:


metrics:  export:    - path: '/metrics/json'      format: json    - path: '/metrics/prometheus'      format: prometheus

Экспорт метрик в формате json или prometheus без использования кластерной конфигурации настраивается средствами модуля http так же, как любой другой маршрут.


local json_metrics = require('metrics.plugins.json')local prometheus = require('metrics.plugins.prometheus')local httpd = require('http.server').new(...)httpd:route(    { path = '/metrics/json' },    function(req)        return req:render({            text = json_metrics.export()        })    end)httpd:route( { path = '/metrics/prometheus' }, prometheus.collect_http)

Для настройки graphite необходимо добавить в код приложения следующую секцию:


local graphite = require('metrics.plugins.graphite')graphite.init{    host = '127.0.0.1',    port = 2003,    send_interval = 60,}

Параметры host и port соответствуют конфигурации вашего сервера Graphite, send_interval периодичность отправки данных в секундах.


При необходимости на основе инструментов пакета metrics можно создать собственный плагин для экспорта. Советы по написанию плагина можно найти в соответствующем разделе документации.


Добавление пользовательских метрик


Ядро пакета metrics составляют коллекторы метрик, созданные на основе примитивов Prometheus:


  • counter предназначен для хранения одного неубывающего значения;
  • gauge предназначен для хранения одного произвольного численного значения;
  • summary хранит сумму значений нескольких наблюдений и их количество, а также позволяет вычислять перцентили по последним наблюдениям;
  • histogram агрегирует несколько наблюдений в гистограмму.

Cоздать экземпляр коллектора можно следующей командой:


local gauge = metrics.gauge('balloons')

В дальнейшем получить доступ к объекту в любой части кода можно этой же командой.


Каждый объект коллектора способен хранить сразу несколько значений метрики с разными лейблами. Например, в результате кода


local gauge = metrics.gauge('balloons')gauge:set(1, { color = 'blue' })gauge:set(2, { color = 'red' })

внутри коллектора возникнет два различных значения метрики. Для того чтобы каким-либо образом изменить значение конкретной метрики в коллекторе, необходимо указать соответствующие ей лейблы:


gauge:inc(11, { color = 'blue' }) -- increase 1 by 11

Лейблы концепт, который также был вдохновлён Prometheus используются для различения измеряемых характеристик в рамках одной метрики. Кроме этого, они могут быть использованы для агрегирования значений на стороне базы данных. Рассмотрим рекомендации по их использованию на примере.


В программе есть модуль server, который принимает запросы и способен сам их отправлять. Вместо того, чтобы использовать две различных метрики server_requests_sent и server_requests_received для хранения данных о количестве отправленных и полученных запросов, следует использовать общую метрику server_requests с лейблом type, который может принимать значения sent и received.


Подробнее о коллекторах и их методах можно прочитать в документации пакета.


Заполнение значений пользовательских метрик


Пакет metrics содержит полезный инструмент для заполнения коллекторов метрик коллбэки. Рассмотрим принцип его работы на простом примере.


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


local metrics = require('metrics')local buffer = require('app.buffer')metrics.register_callback(function()    local gauge = metrics.gauge('buffer_count')    gauge.set(buffer.count())end)

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


Мониторинг HTTP-трафика


Пакет metrics содержит набор инструментов для подсчёта количества входящих HTTP-запросов и измерения времени их обработки. Они предназначены для работы со встроенным пакетом http, и подход будет отличаться в зависимости от того, какую версию вы используете.


Чтобы добавить HTTP-метрики для конкретного маршрута при использовании пакета http 1.x.x, вам необходимо обернуть функцию-обработчик запроса в функцию http_middleware.v1:


local metrics = require('metrics')local http_middleware = metrics.http_middlewarehttp_middleware.build_default_collector('summary', 'http_latency')local route = { path = '/path', method = 'POST' }local handler = function() ... endhttpd:route(route, http_middleware.v1(handler))

Для хранения метрик можно использовать коллекторы histogram и summary.


Чтобы добавить HTTP-метрики для маршрутов роутера при использовании пакета http 2.x.x, необходимо воспользоваться следующим подходом:


local metrics = require('metrics')local http_middleware = metrics.http_middlewarehttp_middleware.build_default_collector('histogram', 'http_latency')router:use(http_middleware.v2(), { name = 'latency_instrumentation' })

Рекомендуется использовать один и тот же коллектор для хранения всей информации об обработке HTTP-запросов (например, выставив в начале коллектор по умолчанию функцией build_default_collector или set_default_collector). Прочитать больше о возможностях http_middleware можно в документации.


Глобальные лейблы


В пункте "Добавление пользовательских метрик" вводится понятие лейблов, предназначенных для различения измеряемых характеристик в рамках одной метрики. Их выставление происходит непосредственно при измерении наблюдения в рамках одного коллектора. Однако это не единственный тип лейблов, которые поддерживает пакет metrics.


Для того чтобы прибавить к каждой метрике какой-то общий для процесса или группы процессов Tarantool (например, имя машины или название приложения) лейбл, необходимо воспользоваться механизмом глобальных лейблов:


local metrics = require('metrics')local global_labels = {}-- постоянное значениеglobal_labels.app = 'MyTarantoolApp'-- переменные конфигурации кластера (http://personeltest.ru/aways/www.tarantool.io/ru/doc/latest/book/cartridge/cartridge_api/modules/cartridge.argparse/)local argparse = require('cartridge.argparse')local params, err = argparse.parse()assert(params, err)global_labels.alias = params.alias-- переменные окружения процессаlocal host = os.getenv('HOST')assert(host)global_labels.host = hostmetrics.set_global_labels(global_labels)

Обратите внимание, что метод set_global_labels полностью перезаписывает таблицу глобальных лейблов при каждом вызове.


Роль cartridge.roles.metrics по умолчанию выставляет alias процесса Tarantool в качестве глобального лейбла.


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


Мониторинг внешних параметров


Tarantool не только позволяет организовывать сбор внутренних метрик о работе приложения, но и способен выступать как агент мониторинга внешних параметров. Подмодуль psutils пакета metrics является примером реализации такого поведения.


С помощью psutils можно настроить сбор метрик об использовании CPU процессами Tarantool. Его информация основывается на данных /proc/stat и /proc/self/task. Подключить сбор метрик можно с помощью следующего кода:


local metrics = require('metrics')metrics.register_callback(function()    local cpu_metrics = require('metrics.psutils.cpu')    cpu_metrics.update()end)

Возможность писать код на Lua делает Tarantool гибким инструментом, позволяющим обходить различные препятствия. Например, psutils возник из необходимости следить за использованием CPU вопреки отказу администраторов со стороны заказчика "подружить" в правах файлы /proc/* процессов Tarantool и плагин inputs.procstat Telegraf, который использовался на местных машинах в качестве основного агента.


Пакет metrics сам по себе способен решить многие прикладные проблемы, но, конечно же, далеко не все, особенно если речь идёт о внешнем мониторинге. Но стоит иметь в виду, что возможность обогатить его с помощью кода всегда остаётся в распоряжении разработчика, и, если возникает необходимость, этой возможностью уж точно не стоит пренебрегать.


Визуализация метрик


Пример из tarantool/grafana-dashboard


Хранение метрик в Prometheus


Настройка пути для экспорта метрик Tarantool в формате Prometheus описана в пункте "Плагины для экспорта метрик". Ответ запроса по такому маршруту выглядит следующим образом:


...# HELP tnt_stats_op_total Total amount of operations# TYPE tnt_stats_op_total gaugetnt_stats_op_total{alias="tnt_router",operation="replace"} 1tnt_stats_op_total{alias="tnt_router",operation="select"} 57tnt_stats_op_total{alias="tnt_router",operation="update"} 43tnt_stats_op_total{alias="tnt_router",operation="insert"} 40tnt_stats_op_total{alias="tnt_router",operation="call"} 4...

Чтобы настроить сбор метрик в Prometheus, необходимо добавить элемент в массив scrape_configs. Этот элемент должен содержать поле static_configs с перечисленными в targets URI всех интересующих процессов Tarantool и поле metrics_path, в котором указан путь для экспорта метрик Tarantool в формате Prometheus.


scrape_configs:  - job_name: "tarantool_app"    static_configs:      - targets:         - "tarantool_app:8081"        - "tarantool_app:8082"        - "tarantool_app:8083"        - "tarantool_app:8084"        - "tarantool_app:8085"    metrics_path: "/metrics/prometheus"

В дальнейшем найти метрики в Grafana вы сможете, указав в качестве job соответствующий job_name из конфигурации.


Пример готового docker-кластера Tarantool App + Prometheus + Grafana можно найти в репозитории tarantool/grafana-dashboard.


Хранение метрик в InfluxDB


Чтобы организовать хранение метрик Tarantool в InfluxDB, необходимо воспользоваться стеком Telegraf + InfluxDB и настроить на процессах Tarantool экспорт метрик в формате json (см. пункт "Плагины для экспорта метрик"). Ответ формируется следующим образом:


{    ...    {        "label_pairs": {            "operation": "select",            "alias": "tnt_router"        },        "timestamp": 1606202705935266,        "metric_name": "tnt_stats_op_total",        "value": 57    },    {        "label_pairs": {            "operation": "update",            "alias": "tnt_router"        },        "timestamp": 1606202705935266,        "metric_name": "tnt_stats_op_total",        "value": 43    },    ...}

HTTP-плагин Telegraf требует заранее указать список возможных тегов для метрики. Конфигурация, учитывающая все стандартные лейблы, будет выглядеть следующим образом:


[[inputs.http]]    urls = [        "http://tarantool_app:8081/metrics/json",        "http://tarantool_app:8082/metrics/json",        "http://tarantool_app:8083/metrics/json",        "http://tarantool_app:8084/metrics/json",        "http://tarantool_app:8085/metrics/json"    ]    timeout = "30s"    tag_keys = [        "metric_name",        "label_pairs_alias",        "label_pairs_quantile",        "label_pairs_path",        "label_pairs_method",        "label_pairs_status",        "label_pairs_operation"    ]    insecure_skip_verify = true    interval = "10s"    data_format = "json"    name_prefix = "tarantool_app_"    fieldpass = ["value"]

Список urls должен содержать URL всех интересующих процессов Tarantool, настроенные для экспорта метрик в формате json. Обратите внимание, что лейблы метрик попадают в Telegraf и, соответственно, InfluxDB как теги, название которых состоит из префикса label_pairs_ и названия лейбла. Таким образом, если ваша метрика имеет лейбл с ключом mylbl, то для работы с ним в Telegraf и InfluxDB необходимо указать в пункте tag_keys соответствующего раздела [[inputs.http]] конфигурации Telegraf значение ключа label_pairs_mylbl, и при запросах в InfluxDB ставить условия на значения лейбла, обращаясь к тегу с ключом label_pairs_mylbl.


В дальнейшем найти метрики в Grafana вы сможете, указав measurement в формате <name_prefix>http (например, для указанной выше конфигурации значение measurement tarantool_app_http).


Пример готового docker-кластера Tarantool App + Telegraf + InfluxDB + Grafana можно найти в репозитории tarantool/grafana-dashboard.


Стандартный дашборд Grafana


Для визуализации метрик Tarantool с помощью Grafana на Official & community built dashboards опубликованы стандартные дашборды. Шаблон состоит из панелей для мониторинга HTTP, памяти для хранения данных вместе с индексами и операций над спейсами Tarantool. Версию для использования с Prometheus можно найти здесь, а для InfluxDB здесь. Версия для Prometheus также содержит набор панелей для мониторинга состояния кластера, агрегированной нагрузки и потребления памяти.



Чтобы импортировать шаблон дашборды, достаточно вставить необходимый id или ссылку в меню Import на сервере Grafana. Для завершения процесса импорта необходимо задать переменные, определяющие место хранения метрик Tarantool в соответствующей базе данных.


Генерация дашбордов Grafana с grafonnet


Стандартные дашборды Grafana были созданы с помощью инструмента под названием grafonnet. Что это за заморский зверь и как мы к нему пришли?


С самого начала перед нами стояла задача поддерживать не одну, а четыре дашборды: два похожих проекта, экземпляр каждого из которых находился в двух разных зонах. Изменения, такие как переименование метрик и лейблов или добавление/удаление панелей, происходили чуть ли не ежедневно, но после творческой работы по проектированию улучшений и решению возникающих проблем непременно следовало механическое накликивание изменений мышью, умножавшееся в своём объёме на четыре. Стало ясно, что такой подход следует переработать.


Одним из первых способов решить большинство возникающих проблем было использование механизма динамических переменных (Variables) в Grafana. Например, он позволяет объединить дашборды с метриками из разных зон в одну с удобным переключателем. К сожалению, мы слишком быстро столкнулись с проблемой: использование механизма оповещений (Alert) не совместимо с запросами, использующими динамические переменные.


Любой дашборд в Grafana по сути представляет собой некоторый json. Более того, платформа позволяет без каких-либо затруднений экспортировать в таком формате существующие дашборды. Работать с ним в ручном режиме несколько затруднительно: размер даже небольшого дашборда составляет несколько тысяч строк. Первым способом решения проблемы был скрипт на Python, который заменял необходимые поля в json, по сути превращая один готовый дашборд в другой. Когда разработка библиотеки скриптов пришла к задаче добавления и удаления конкретных панелей, мы начали осознавать, что пытаемся создать генератор дашбордов. И что эту задачу уже кто-то до нас решал.


В открытом доступе можно найти несколько проектов, посвящённых данной теме. К счастью или несчастью, проблема выбора решилась быстро: на контуре заказчика для хранения метрик мы безальтернативно пользовались InfluxDB, а поддержка запросов к InfluxDB хоть в какой-то форме присутствовала только в grafonnet.


grafonnet opensource-проект под эгидой Grafana, предназначенный для программной генерации дашбордов. Он основан на языке программирования jsonnet языке для генерации json. Сам grafonnet представляет собой набор шаблонов для примитивов Grafana (панели и запросы разных типов, различные переменные) с методами для объединения их в цельный дашборд.


Основным преимуществом grafonnet является используемый в нём язык jsonnet. Он прост и минималистичен, и всегда имеет дело с json-объектами (точнее, с их местной расширенной версией, которая может включать в себя функции и скрытые поля). Благодаря этому на любом этапе работы выходной объект можно допилить под себя, добавив или убрав какое-либо вложенное поле, не внося при этом изменений в исходный код.


Начав с форка проекта и сборки нашей дашборды на основе этого форка, впоследствии мы оформили несколько Pull Request-ов в grafonnet на основе наших изменений. Например, один из них добавил поддержку запросов в InfluxDB на основе визуального редактора.


Визуальный редактор запросов InfluxDB


Код наших стандартных дашбордов расположен в репозитории tarantool/grafana-dashboard. Здесь же находится готовый docker-кластер, состоящий из стеков Tarantool App + Telegraf + InfluxDB + Grafana, Tarantool App + Prometheus + Grafana. Его можно использовать для локальной отладки сбора и обработки метрик в вашем собственном приложении.


При желании из стандартных панелей можно собрать дашборд, расширенный панелями для отображения собственных метрик приложения.


На что смотреть?


В первую очередь, стоит следить за состоянием самих процессов Tarantool. Для этого подойдёт, например, стандартный up Prometheus. Можно соорудить простейший healthcheck самостоятельно:


httpd:route(    { path = '/health' },    function(req)        local body = { app = app, alias = alias, status = 'OK' }        local resp = req:render({ json = body })        resp.status = 200        return resp    end)

Рекомендации по мониторингу внешних параметров ничем принципиально не отличаются от ситуации любого другого приложения. Необходимо следить за потреблением памяти на хранение логов и служебных файлов на диске. Заметьте, что файлы с данными .snap и .xlog возникают даже при использовании движка memtx (в зависимости от настроек). При нормальной работе нагрузка на CPU не должна быть чересчур большой. Исключение составляет момент восстановления данных после рестарта процесса: построение индексов может загрузить все доступные потоки процессора на 100 % на несколько минут.


Потребление RAM удобно разделить на два пункта: Lua-память и память, потребляемая на хранение данных и индексов. Память, доступная для выполнения кода на Lua, имеет ограничение в 2 Gb на уровне Luajit. Обычно приближение метрики к этой границе сигнализирует о наличии какого-то серьёзного изъяна в коде. Более того, зачастую такие изъяны приводят к нелинейному росту используемой памяти, поэтому начинать волноваться стоит уже при переходе границы в 512 Mb на процесс. Например, при высокой нагрузке в наших приложениях показатели редко выходили за предел 200-300 Mb Lua-памяти.


При использовании движка memtx потреблением памяти в рамках заданного лимита memtx_memory (он же метрика quota_size) заведует slab-аллокатор. Процесс происходит двухуровнево: аллокатор выделяет в памяти ячейки, которые после занимают сами данные или индексы спейсов. Зарезервированная под занятые или ещё не занятые ячейки память отображена в quota_used, занятая на хранение данных и индексов arena_used (только данных items_used). Приближение к порогу arena_used_ratio или items_used_ratio свидетельствует об окончании свободных зарезервированных ячеек slab, приближение к порогу quota_used_ratio об окончании доступного места для резервирования ячеек. Таким образом, об окончании свободного места для хранения данных свидетельствует приближение к порогу одновременно метрик quota_used_ratio и arena_used_ratio. В качестве порога обычно используют значение 90 %. В редких случаях в логах могут появляться сообщения о невозможности выделить память под ячейки или данные даже тогда, когда quota_used_ratio, arena_used_ratio или items_used_ratio далеки от порогового значения. Это может сигнализировать о дефрагментации данных в RAM, неудачном выборе схем спейсов или неудачной конфигурации slab-аллокатора. В такой ситуации необходима консультация специалиста.


Рекомендации по мониторингу HTTP-нагрузки и операций на спейсах также являются скорее общими, нежели специфичными для Tarantool. Необходимо следить за количеством ошибок и временем обработки запроса в зависимости от кода ответа, а также за резким возрастанием нагрузки. Неравномерность нагрузки по операциям на спейсы может свидетельствовать о неудачном выборе ключа шардирования.


Заключение


Как этот материал, так и пакет metrics назвать "всеохватными" и "универсальными" на данный момент нельзя. Открытыми или находящимися на данный момент в разработке являются вопросы метрик репликации, мониторинга движка vinyl, метрики event loop и полная документация по уже существующим методам metrics.


Не стоит забывать о том, что metrics и grafana-dashboard являются opensource-разработками. Если при работе над своим проектом вы наткнулись на ситуацию, которая не покрывается текущими возможностями пакетов, не стесняйтесь внести предложение в Issues или поделиться вашим решением в Pull Requests.


Надеюсь, что данный материал помог закрыть некоторые вопросы, которые могли возникнуть при решении задачи мониторинга приложения на базе Tarantool. А если какие-то из них так и не получили ответа, всегда можно обратиться напрямую в наш чат в Telegram.


Полезные ссылки:


Подробнее..

Procmeminfo gawk удобный JSON для discovery метрик в zabbix

04.12.2020 20:21:06 | Автор: admin

В работе над одной задачей, понадобилось добавить в мониторинг все счетчики памяти из /proc/meminfo с нескольких linux хостов, для отслеживания состояние памяти в течении времени

root@server:~# cat /proc/meminfo                MemTotal:        8139880 kBMemFree:          146344 kBMemAvailable:    4765352 kBBuffers:          115436 kBCached:          6791672 kBSwapCached:         9356 kBActive:          4743296 kBInactive:        2734088 kBActive(anon):    2410780 kBInactive(anon):   340628 kBActive(file):    2332516 kBInactive(file):  2393460 kBUnevictable:           0 kBMlocked:               0 kBSwapTotal:       3906556 kBSwapFree:        3585788 kBDirty:               804 kBWriteback:             0 kBAnonPages:        567172 kBMapped:          2294276 kBShmem:           2182128 kBKReclaimable:     198800 kBSlab:             340540 kBSReclaimable:     198800 kBSUnreclaim:       141740 kBKernelStack:        7008 kBPageTables:        90520 kBNFS_Unstable:          0 kBBounce:                0 kBWritebackTmp:          0 kBCommitLimit:     7976496 kBCommitted_AS:    5171488 kBVmallocTotal:   34359738367 kBVmallocUsed:       25780 kBVmallocChunk:          0 kBPercpu:            24480 kBHardwareCorrupted:     0 kBAnonHugePages:         0 kBShmemHugePages:        0 kBShmemPmdMapped:        0 kBFileHugePages:         0 kBFilePmdMapped:         0 kBCmaTotal:              0 kBCmaFree:               0 kBHugePages_Total:       0HugePages_Free:        0HugePages_Rsvd:        0HugePages_Surp:        0Hugepagesize:       2048 kBHugetlb:               0 kBDirectMap4k:      773632 kBDirectMap2M:     7606272 kBDirectMap1G:     2097152 kBroot@server:~#

Набор счетчиков должен приезжать в мониторинг автоматически при помощи discovery прямиком с файла /proc/meminfo

После разведки и создании метрик, все метрики должны приезжать в мониторинг раз в минуту одной операцией, для снижения эффекта наблюдения

Первое что пришло на ум - создать скрипт, запилить в нем всю логику, разложить на сервера и добавить юсер-параметр zabbix агента, но это уже не спортивно, так как эту же штуку можно сделать используя лишь gawk

Создадим шаблон в мониторинге, переходим во вкладку макросы, создаем три макроса:

макрос {$PATH} нужен для того чтобы gawk нашелся при попытке его запустить

Содержание макроса:

PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; LANG=en_US.UTF-8;

макрос {$S} нужен будет в discovery ( простыню про gawk, BEGIN я объясню в конце статьи )

gawk  'BEGIN {FS=":";ORS="";print "{\"data\": [ " }{b=gensub(/ +/,"","g",gensub(/kB/,"","g",$2) );$1=gensub(/\(|\)/,"_","g",$1);printf "%s{\"{#TYPE}\": \"%s\", \"{#VALUE}\": \"%s\"}",separator, $1, b;separator = ",";} END { print " ]}" }' /proc/meminfo

макрос {$VALUE} нужен будет при получении метрик раз в минуту

gawk  'BEGIN { FS=":"; ORS = ""; print "{" } { b=gensub(/ +/,"","g",gensub(/kB/,"","g",$2) ); $1=gensub(/\(|\)/,"_","g",$1); printf "%s\"%s\":\"%s\"",separator,$1,b;separator=",";} END { print "}" }' /proc/meminfo

вот наш шаблон

Создадим обычную метрику meminfo system.run[{$PATH} {$VALUE},wait]

Обратите внимание на конструкцию вызова system.run а внутри два макроса {$PATH} {$VALUE} очень коротко, лаконично и удобно

Метрика meminfo является источником данных для метрик которые будут обнаружены

С метрикой пока все

Создаем правило обнаружения meminfo

Перейдем в фильтр и добавим {#TYPE} [не равно] VmallocTotal|VmallocChunk это делается для исключения двух ненужных нам параметров

Создаем прототип элемента данных, тип данных зависимый элемент, в качестве источника данных выбираем метрику созданную ранее

В пред-обработке настраиваем JSONPath + добавляем множитель так как цифры приезжают к килобайтах, а нужны в байтах.

Линкуем созданный шаблон с хостом

Пробуем

Должно получится

метрики нашлись, данные по ним распределились в правильном исчислении то есть в байтах

можем посмотреть и графики

Теперь самое интересное, как использовать gawk для создания JSON для discavery и для базовой метрики

Вот полный честный вывод JSON для discavery

root@server:~# gawk  'BEGIN {FS=":";ORS="";print "{\"data\": [ " }{b=gensub(/ +/,"","g",gensub(/kB/,"","g",$2) );$1=gensub(/\(|\)/,"_","g",$1);printf "%s{\"{#TYPE}\": \"%s\", \"{#VALUE}\": \"%s\"}",separator, $1, b;separator = ",";} END { print " ]}" }' /proc/meminfo{"data": [ {"{#TYPE}": "MemTotal", "{#VALUE}": "8139880"},{"{#TYPE}": "MemFree", "{#VALUE}": "147628"},{"{#TYPE}": "MemAvailable", "{#VALUE}": "4764232"},{"{#TYPE}": "Buffers", "{#VALUE}": "115316"},{"{#TYPE}": "Cached", "{#VALUE}": "6789504"},{"{#TYPE}": "SwapCached", "{#VALUE}": "9356"},{"{#TYPE}": "Active", "{#VALUE}": "4742408"},{"{#TYPE}": "Inactive", "{#VALUE}": "2733636"},{"{#TYPE}": "Active_anon_", "{#VALUE}": "2411644"},{"{#TYPE}": "Inactive_anon_", "{#VALUE}": "340828"},{"{#TYPE}": "Active_file_", "{#VALUE}": "2330764"},{"{#TYPE}": "Inactive_file_", "{#VALUE}": "2392808"},{"{#TYPE}": "Unevictable", "{#VALUE}": "0"},{"{#TYPE}": "Mlocked", "{#VALUE}": "0"},{"{#TYPE}": "SwapTotal", "{#VALUE}": "3906556"},{"{#TYPE}": "SwapFree", "{#VALUE}": "3585788"},{"{#TYPE}": "Dirty", "{#VALUE}": "368"},{"{#TYPE}": "Writeback", "{#VALUE}": "0"},{"{#TYPE}": "AnonPages", "{#VALUE}": "568164"},{"{#TYPE}": "Mapped", "{#VALUE}": "2294960"},{"{#TYPE}": "Shmem", "{#VALUE}": "2182128"},{"{#TYPE}": "KReclaimable", "{#VALUE}": "198800"},{"{#TYPE}": "Slab", "{#VALUE}": "340536"},{"{#TYPE}": "SReclaimable", "{#VALUE}": "198800"},{"{#TYPE}": "SUnreclaim", "{#VALUE}": "141736"},{"{#TYPE}": "KernelStack", "{#VALUE}": "7040"},{"{#TYPE}": "PageTables", "{#VALUE}": "90568"},{"{#TYPE}": "NFS_Unstable", "{#VALUE}": "0"},{"{#TYPE}": "Bounce", "{#VALUE}": "0"},{"{#TYPE}": "WritebackTmp", "{#VALUE}": "0"},{"{#TYPE}": "CommitLimit", "{#VALUE}": "7976496"},{"{#TYPE}": "Committed_AS", "{#VALUE}": "5189180"},{"{#TYPE}": "VmallocTotal", "{#VALUE}": "34359738367"},{"{#TYPE}": "VmallocUsed", "{#VALUE}": "25780"},{"{#TYPE}": "VmallocChunk", "{#VALUE}": "0"},{"{#TYPE}": "Percpu", "{#VALUE}": "24480"},{"{#TYPE}": "HardwareCorrupted", "{#VALUE}": "0"},{"{#TYPE}": "AnonHugePages", "{#VALUE}": "0"},{"{#TYPE}": "ShmemHugePages", "{#VALUE}": "0"},{"{#TYPE}": "ShmemPmdMapped", "{#VALUE}": "0"},{"{#TYPE}": "FileHugePages", "{#VALUE}": "0"},{"{#TYPE}": "FilePmdMapped", "{#VALUE}": "0"},{"{#TYPE}": "CmaTotal", "{#VALUE}": "0"},{"{#TYPE}": "CmaFree", "{#VALUE}": "0"},{"{#TYPE}": "HugePages_Total", "{#VALUE}": "0"},{"{#TYPE}": "HugePages_Free", "{#VALUE}": "0"},{"{#TYPE}": "HugePages_Rsvd", "{#VALUE}": "0"},{"{#TYPE}": "HugePages_Surp", "{#VALUE}": "0"},{"{#TYPE}": "Hugepagesize", "{#VALUE}": "2048"},{"{#TYPE}": "Hugetlb", "{#VALUE}": "0"},{"{#TYPE}": "DirectMap4k", "{#VALUE}": "773632"},{"{#TYPE}": "DirectMap2M", "{#VALUE}": "7606272"},{"{#TYPE}": "DirectMap1G", "{#VALUE}": "2097152"} ]}root@server:~#

Вот так выглядит JSON для discavery

{"data": [ {"{#TYPE}": "MemTotal", "{#VALUE}": "8139880"},{"{#TYPE}": "MemFree", "{#VALUE}": "147628"},{"{#TYPE}": "MemAvailable", "{#VALUE}": "4764232"},{"{#TYPE}": "Buffers", "{#VALUE}": "115316"},{"{#TYPE}": "Cached", "{#VALUE}": "6789504"},{"{#TYPE}": "SwapCached", "{#VALUE}": "9356"},.....{"{#TYPE}": "DirectMap4k", "{#VALUE}": "773632"},{"{#TYPE}": "DirectMap2M", "{#VALUE}": "7606272"},{"{#TYPE}": "DirectMap1G", "{#VALUE}": "2097152"} ]}root@server:~# 
root@server:~# gawk 'BEGIN {FS=":";ORS="";print "{\"data\": [ " }{b = gensub(/ +/,"","g",  gensub(/kB/,"","g",$2) );$1=gensub(/\(|\)/,"_","g",$1);printf "%s{\"{#TYPE}\": \"%s\", \"{#VALUE}\": \"%s\"}",separator, $1, b;separator = ",";}END { print " ]}" }' /proc/meminfo

BEGIN {FS=":";ORS="";print "{\"data\": [ " }

FS=":"; - разделяет строку на части и передает разделившеся части в $1, $2 ... $n

print "{\"data\": [ " - выполняется только один раз, в самом начале

Дальше происходит магия gawk организует сам цикл с перечислением всех полученных строк, принимая во внимание то что строки будут разделятся по ":" то мы в $1 получать всегда имя параметра, а в $2 всегда его значение, правда не очень в удобном формате

собственно это выглядит как: есть строка из цикла CommitLimit: 7976496 kB которая разделяется:

$1=CommitLimit

$2= 7976496 kB

Нужно предобработать обе строки

b = gensub(/ +/,"","g", gensub(/kB/,"","g",$2) ); содержится две функции gensub

Вложенная функция gensub(/kB/,"","g",$2) исключает из вывода kB а базовая gensub(/ +/,"","g", ........ ); исключает пробелы, получаем чистую цифиру.

Следом в имени параметров {#TYPE} нужно предусмотреть что недолжно быть символов скобок ( ) так как мониторинг их не перевариват, ни при создании метрик, ни при разнесении данных.

Сказанно - сделано, регуляркой уберем скобки из имени параметра, заменим поджопником нижней чертой _ $1=gensub(/\(|\)/,"_","g",$1);

После подготовленные параметры, собираем JSON и выводим

printf "%s{\"{#TYPE}\": \"%s\", \"{#VALUE}\": \"%s\"}",separator, $1, b;separator = ",";}

После того как все строки обработаны, gawk выполняет END { print " ]}" что закрывает JSON и финализирует.

Вот таким простым способом без скриптов можно сделать обнаружения и формирование JSON с данными, добавив метрики в мониторинг и получая максимальный эффект от простоты применения.

Подробнее..

Импорт ЕГРЮЛ ФНС средствами Apache NiFi. Шаг 3 преобразование JSON с помощью JOLT

11.02.2021 18:04:31 | Автор: admin

В одном из проектов возникла необходимость перевести процессы импорта данных сторонних систем на микросервисную архитектуру. В качестве инструмента выбран Apache NiFi. В качестве первого подопытного выбран импорт ЕГРЮЛ ФНС.

В предыдущей статье был описан способ преобразования XML в JSON с использованием AVRO schema.

В данной статье описан способ преобразования JSON с помощью JOLT спецификации.

Используемые процессоры и контроллеры

Деление JSON на части

FlowFile, полученный на предыдущем этапе, содержит JSON с массивом выписок ЕГРЮЛ по разным организациям. Для начала разделим его на части, чтобы каждый FlowFile содержал одну выписку.

Для этого используем процессор SplitJson. Из настроек - требуется указать выражение JsonPath для разделения json на части. В данном случае $.*

Документация по JsonPath здесь

Потренироваться можно здесь

Преобразование JSON

Полученный JSON имеет излишне сложную структуру для того, чтобы в дальнейшем хранить и обрабатывать его. Адрес и ФИО лучше объединить в одну строку, некоторые элементы перенести выше по иерархии.

JSON перед трансформацией
{  "reportDate" : "2020-05-20",  "ogrn" : "1234567890123",  "ogrnDate" : "2002-12-30",  "inn" : "1234567890",  "kpp" : "123456789",  "opfCode" : "12300",  "opfName" : "Общества с ограниченной ответственностью",  "name" : {    "fullName" : "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ",    "shortName" : "ООО"  },  "address" : {    "addressRF" : {      "region" : {        "type" : "ОБЛАСТЬ",        "name" : "МОСКОВСКАЯ"      },      "district" : null,      "town" : {        "type" : "ГОРОД",        "name" : "ИСТРА"      },      "settlement" : null,      "street" : {        "type" : "ПЕРЕУЛОК",        "name" : "ВОЛОКОЛАМСКИЙ"      },      "index" : "143500",      "regionCode" : "50",      "kladr" : "500000570000011",      "house" : null,      "building" : null,      "apartment" : null    }  },  "termination" : null,  "capital" : null,  "manageOrg" : null,  "director" : [ {    "fl" : {      "lastName" : "ИВАНОВ",      "firstName" : "ИВАН",      "patronymic" : "ИВАНОВИЧ",      "inn" : "123456789012"    },    "position" : {      "ogrnip" : null,      "typeCode" : "02",      "typeName" : "Руководитель юридического лица",      "name" : "ГЕНЕРАЛЬНЙ ДИРЕКТОР"    },    "disqualification" : null  } ],  "founders" : {    "founderULRF" : null,    "founderULForeign" : null,    "founderFL" : [ {      "fl" : {        "lastName" : "ИВАНОВ",        "firstName" : "ИВАН",        "patronymic" : "ИВАНОВИЧ",        "inn" : "123456789012"      },      "capitalPart" : {        "nominal" : 20000.0,        "size" : {          "percent" : 50.0,          "decimalPart" : null,          "simplePart" : null        }      }    }, {      "fl" : {        "lastName" : "ПЕТРОВ",        "firstName" : "ПЕТР",        "patronymic" : "ПЕТРОВИЧ",        "inn" : "123456789021"      },      "capitalPart" : {        "nominal" : 20000.0,        "size" : {          "percent" : 50.0,          "decimalPart" : null,          "simplePart" : null        }      }    } ],    "founderGov" : null,    "founderPIF" : null  },  "capitalPart" : null,  "holderReestrAO" : null,  "okved" : {    "mainOkved" : {      "code" : "47.11",      "name" : "Торговля розничная преимущественно пищевыми продуктами, включая напитки, и табачными изделиями в неспециализированных магазинах"    },    "addOkved" : null  }}

Для трансформации JSON используется процессор JoltTransformJSON.

Настройки:

  • Jolt Transformation DSL - тип трансформации. В данном случае Chain - цепочка из нескольких трансформаций

  • Jolt Specification - собственно сама спецификация. Ее разбор ниже

JOLT спецификация

Собственно, сам субъект - ссылка на исходники и документацию.

Потренироваться можно здесь.

Меня интересовали операции сдвига элементов по иерархии - операция shift и преобразование самих данных - операция modify-overwrite-beta. По последней доки как таковой и нет. Исходники операции в Modifier.java, там можно посмотреть список доступных функций. Но на jolt-demo.appspot.com внизу есть примеры для этой операции. Так что методом научного тыка есть возможность прийти к решению.

JOLT спецификация
[{"operation": "modify-overwrite-beta","spec": {"address": {"addressRF": {"region": "=concat(@(type), ' ', @(name))","district": "=concat(@(type), ' ', @(name))","town": "=concat(@(type), ' ', @(name))","settlement": "=concat(@(type), ' ', @(name))","street": "=concat(@(type), ' ', @(name))"}},"director": {"*": {"fl": {"fio": "=concat(@(1,lastName), ' ', @(1,firstName), ' ', @(1,patronymic))"}}},"founders": {"founderFL": {"*": {"fl": {"fio": "=concat(@(1,lastName), ' ', @(1,firstName), ' ', @(1,patronymic))"}}},"founderGov": {"*": {"founderImplFL": {"fl": {"fio": "=concat(@(1,lastName), ' ', @(1,firstName), ' ', @(1,patronymic))"}}}}}}},{"operation": "modify-overwrite-beta","spec": {"address": {"addressRF": {"value": "=concat(@(1,index), ', ', @(1,region), ', ', @(1,district), ', ', @(1,town), ', ', @(1,settlement), ', ', @(1,street), ', ', @(1,house), ', ', @(1,building), ', ', @(1,apartment))","fias": null}}}},{"operation": "shift","spec": {"reportDate|ogrn|ogrnDate|inn|kpp|opfCode|opfName": "&","name": {"*": "&"},"address": {"addressRF": {"kladr|regionCode|value|fias": "&2.&"}},"termination": {"method": {"*": "&2.&"},"*": "&1.&"},"capital": "&","manageOrg": {"egrulData": {"*": "&2.&"}},"director": {"*": {"fl": {"fio|inn": "&3[&2].&"},"position": {"name": "&3[&2].&1","*": "&3[&2].&"},"disqualification": "&2[&1].&"}},"founders": {"founderULRF|founderULForeign": {"*": {"egrulData|foreignReg": {"*": "&4.&3[&2].&"},"*": "&3.&2[&1].&"}},"founderFL": {"*": {"fl": {"fio|inn": "&4.&3[&2].&"},"*": "&3.&2[&1].&"}},"founderGov": {"*": {"govOrg": {"*": "&4.&3[&2].&"},"capitalPart": "&3.&2[&1].&","founderImplUL": {"egrulData": {"*": "&5.&4[&3].&2.&"}},"founderImplFL": {"fl": {"fio|inn": "&5.&4[&3].&2.&"}}}},"founderPIF": {"*": {"PIFName": {"name": "&4.&3[&2].&1"},"manageOrg": {"egrulData": {"*": "&5.&4[&3].&"}},"capitalPart": "&3.&2[&1].&"}}},"capitalPart": "&","holderReestrAO": {"egrulData": {"*": "&2.&"}},"okved": "&"}}]

Операцию modify-overwrite-beta пришлось делать два раза, т.к. по другому собрать адрес в одну строку у меня не получилось.

Как видно, вся спецификация представляет собой массив из трех операций: две - modify-overwrite-beta и одна - shift. Описание каждой операции содержит ее тип - элемент operation и спецификацию - элемент spec.

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

Операция modify-overwrite-beta

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

Преобразование адреса

Разберем преобразование адреса.

Первый этап (см. первый блок modify-overwrite-beta) - объединить type и name для region, district, town, settlement и street. Для этого прописываем путь к элементам и в правой части для каждого пишем "=concat(@(type), ' ', @(name))" .

"address": {"addressRF": {"region": "=concat(@(type), ' ', @(name))","district": "=concat(@(type), ' ', @(name))","town": "=concat(@(type), ' ', @(name))","settlement": "=concat(@(type), ' ', @(name))","street": "=concat(@(type), ' ', @(name))"}}

Что это означает. Например, "region": "=concat(@(type), ' ', @(name))", означает: на выходе требуется получить элемент region, а в качестве его содержимого требуется получить конкатенацию содержимого элементов type и name. Причем искать эти элементы необходимо непосредственно внутри существующего элемента region, о чем говорит конструкция @(type).

Второй этап (см. второй блок modify-overwrite-beta) - объединить составляющие адреса в одну строку и записать в элемент value.

"address": {"addressRF": {"value": "=concat(@(1,index), ', ', @(1,region), ', ', @(1,district), ', ', @(1,town), ', ', @(1,settlement), ', ', @(1,street), ', ', @(1,house), ', ', @(1,building), ', ', @(1,apartment))","fias": null}}

Здесь примерно то же самое, но теперь используется конструкция вида @(1,index). Она означает, что для поиска элемента index необходимо подняться на один уровень вверх от текущего и искать его там. Т.е. от уровня value необходимо перейти к уровню addressRF, и в пределах addressRF найти элемент index.

Следует обратить внимание, что не должно быть пробелов между = и concat, а также в @(1,index).

Элемент fias был добавлен в надежде в дальнейшем осуществить поиск кода ФИАС по адресу в каком-нибудь стороннем сервисе.

На этом преобразование адреса завершено. Про операцию shift будет ниже.

Объединение ФИО для физических лиц

Объединение ФИО для физических лиц осуществляется аналогичным образом. Здесь обращает на себя внимание только наличие селектора "*" в левой части. Он используется, т.к. содержимое элемента director представляет собой массив, а это отдельный уровень иерархии.

"director": {"*": {"fl": {"fio": "=concat(@(1,lastName), ' ', @(1,firstName), ' ', @(1,patronymic))"}}}

Операция shift

В блок shift на вход поступает следующий JSON.

Промежуточный JSON
{  "reportDate" : "2020-05-20",  "ogrn" : "1234567890123",  "ogrnDate" : "2002-12-30",  "inn" : "1234567890",  "kpp" : "123456789",  "opfCode" : "12300",  "opfName" : "Общества с ограниченной ответственностью",  "name" : {    "fullName" : "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ",    "shortName" : "ООО"  },  "address" : {    "addressRF" : {      "region" : "ОБЛАСТЬ МОСКОВСКАЯ",      "district" : " ",      "town" : "ГОРОД ИСТРА",      "settlement" : " ",      "street" : "ПЕРЕУЛОК ВОЛОКОЛАМСКИЙ",      "index" : "143500",      "regionCode" : "50",      "kladr" : "500000570000011",      "house" : null,      "building" : null,      "apartment" : null,      "value" : "143500, ОБЛАСТЬ МОСКОВСКАЯ,  , ГОРОД ИСТРА,  , ПЕРЕУЛОК ВОЛОКОЛАМСКИЙ, , , ",      "fias" : null    }  },  "termination" : null,  "capital" : null,  "manageOrg" : null,  "director" : [ {    "fl" : {      "lastName" : "ИВАНОВ",      "firstName" : "ИВАН",      "patronymic" : "ИВАНОВИЧ",      "inn" : "123456789012",      "fio" : "ИВАНОВ ИВАН ИВАНОВИЧ"    },    "position" : {      "ogrnip" : null,      "typeCode" : "02",      "typeName" : "Руководитель юридического лица",      "name" : "ГЕНЕРАЛЬНЙ ДИРЕКТОР"    },    "disqualification" : null  } ],  "founders" : {    "founderULRF" : null,    "founderULForeign" : null,    "founderFL" : [ {      "fl" : {        "lastName" : "ИВАНОВ",        "firstName" : "ИВАН",        "patronymic" : "ИВАНОВИЧ",        "inn" : "123456789012",        "fio" : "ИВАНОВ ИВАН ИВАНОВИЧ"      },      "capitalPart" : {        "nominal" : 20000,        "size" : {          "percent" : 50,          "decimalPart" : null,          "simplePart" : null        }      }    }, {      "fl" : {        "lastName" : "ПЕТРОВ",        "firstName" : "ПЕТР",        "patronymic" : "ПЕТРОВИЧ",        "inn" : "123456789021",        "fio" : "ПЕТРОВ ПЕТР ПЕТРОВИЧ"      },      "capitalPart" : {        "nominal" : 20000,        "size" : {          "percent" : 50,          "decimalPart" : null,          "simplePart" : null        }      }    } ],    "founderGov" : null,    "founderPIF" : null  },  "capitalPart" : null,  "holderReestrAO" : null,  "okved" : {    "mainOkved" : {      "code" : "47.11",      "name" : "Торговля розничная преимущественно пищевыми продуктами, включая напитки, и табачными изделиями в неспециализированных магазинах"    },    "addOkved" : null  }}

Как видно, остались лишние элементы - например, данные адреса, фамилия, имя отчество. Стоит отметить, что если в операции modify-overwrite-beta не указано преобразование для элемента, то он переносится в неизменном виде. В отличие от этого, в операции shift - если преобразование для элемента не указано, то он будет удален.

Описание операции shift
{"operation": "shift","spec": {"reportDate|ogrn|ogrnDate|inn|kpp|opfCode|opfName": "&","name": {"*": "&"},"address": {"addressRF": {"kladr|regionCode|value|fias": "&2.&"}},"termination": {"method": {"*": "&2.&"},"*": "&1.&"},"capital": "&","manageOrg": {"egrulData": {"*": "&2.&"}},"director": {"*": {"fl": {"fio|inn": "&3[&2].&"},"position": {"name": "&3[&2].&1","*": "&3[&2].&"},"disqualification": "&2[&1].&"}},"founders": {"founderULRF|founderULForeign": {"*": {"egrulData|foreignReg": {"*": "&4.&3[&2].&"},"*": "&3.&2[&1].&"}},"founderFL": {"*": {"fl": {"fio|inn": "&4.&3[&2].&"},"*": "&3.&2[&1].&"}},"founderGov": {"*": {"govOrg": {"*": "&4.&3[&2].&"},"capitalPart": "&3.&2[&1].&","founderImplUL": {"egrulData": {"*": "&5.&4[&3].&2.&"}},"founderImplFL": {"fl": {"fio|inn": "&5.&4[&3].&2.&"}}}},"founderPIF": {"*": {"PIFName": {"name": "&4.&3[&2].&1"},"manageOrg": {"egrulData": {"*": "&5.&4[&3].&"}},"capitalPart": "&3.&2[&1].&"}}},"capitalPart": "&","holderReestrAO": {"egrulData": {"*": "&2.&"}},"okved": "&"}}

В операции shift присутствуют левая и правая часть инструкции. Левая часть указывает, где брать данные, а правая указывает путь, куда их разместить. Путь представляет собой цепочку наименований элементов, разделенных точкой. С помощью знака & осуществляется подстановка наименований существующих элементов. Исчисление начинается с того элемента, который указан в левой части, ему соответствует &0. Ноль при этом можно опустить. Выше него по иерархии будет &1, и т.д. К знакам & можно добавлять префиксы и суффиксы - например, pre-&-post. Т.е. если & соответствует элементу name, то на выходе получим pre-name-post. Результирующая цепочка элементов размещается в корне иерархии. Рассмотрим на примерах.

Самое простое - "reportDate|ogrn|ogrnDate|inn|kpp|opfCode|opfName": "&". Будет взят каждый из перечисленных элементов, и они будут размещены в корне. Перечисление осуществляется с помощью |.

Далее переносим fullName и shortName на один уровень вверх с помощью инструкции "name": { "*": "&" }.
"*" означает, что требуется выбрать содержимое всех элементов, вложенных в name.
"&" означает, что их требуется разместить в корне иерархии.

Следующее - перенос данных адреса.

"address": {"addressRF": {"kladr|regionCode|value|fias": "&2.&"}}

Здесь мы указываем нужные элементы. Ненужные не указываем. Инструкция для размещения - "&2.&". Она означает, что требуется составить цепочки из нулевого и второго уровня, минуя первый. &2 соответствует элементу address, а & - элементам из перечисления. &1 соответствует элементу addressRF, он будет удален из иерархии. Т.обр. будут составлены четыре цепочки: address.kladr, address.regionCode, address.value и address.fias. И все они буду размещены в корне результирующего JSON.

Массивы разберем на примере данных о директоре

"director" : [ {    "fl" : {      "lastName" : "ИВАНОВ",      "firstName" : "ИВАН",      "patronymic" : "ИВАНОВИЧ",      "inn" : "123456789012",      "fio" : "ИВАНОВ ИВАН ИВАНОВИЧ"    },    "position" : {      "ogrnip" : null,      "typeCode" : "02",      "typeName" : "Руководитель юридического лица",      "name" : "ГЕНЕРАЛЬНЙ ДИРЕКТОР"    },    "disqualification" : null  } ]

Нужно убрать lastName, firstName и patronymic.
inn и fio перенести на один уровень выше.
ogrnip, typeCode и typeName также перенести на один уровень выше.
Значение name установить в качестве значения position.
disqualification оставить без изменений.

В общем-то алгоритм действий тот же самый, но следуют помнить, что массив - это отдельный уровень иерархии. Когда работаем с массивом, то в цепочке иерархии соответствующий ему & должен быть помещен в квадратные скобки - [&].

"director": {"*": {"fl": {"fio|inn": "&3[&2].&"},"position": {"name": "&3[&2].&1","*": "&3[&2].&"},"disqualification": "&2[&1].&"}}

Например, fio и inn. Для них цепочка &3[&2].&. Точку перед открывающей квадратной скобкой можно опустить. Получаем: &3 - соответствует элементу director, [&2] - соответствует уровню элементов массива, & - сами fio и inn.

Элемент name в position. &3 - соответствует элементу director, [&2] - соответствует уровню элементов массива, &1 - соответствует элементу position. &, соответствующий самому элементу name отсутствует, значит его содержимое будет перенесено в position.

Остальные элементы в position просто переносятся на один уровень вверх. disqualification остается без изменений.

Далее используются аналогичные конструкции.

Пример

Ну и напоследок продублирую исходный JSON, JOLT спецификацию и результирующий JSON

Исходный JSON
{  "reportDate": "2020-05-20",  "ogrn": "1234567890123",  "ogrnDate": "2002-12-30",  "inn": "1234567890",  "kpp": "123456789",  "opfCode": "12300",  "opfName": "Общества с ограниченной ответственностью",  "name": {    "fullName": "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ",    "shortName": "ООО"  },  "address": {    "addressRF": {      "region": {        "type": "ОБЛАСТЬ",        "name": "МОСКОВСКАЯ"      },      "district": null,      "town": {        "type": "ГОРОД",        "name": "ИСТРА"      },      "settlement": null,      "street": {        "type": "ПЕРЕУЛОК",        "name": "ВОЛОКОЛАМСКИЙ"      },      "index": "143500",      "regionCode": "50",      "kladr": "500000570000011",      "house": null,      "building": null,      "apartment": null    }  },  "termination": null,  "capital": null,  "manageOrg": null,  "director": [    {      "fl": {        "lastName": "ИВАНОВ",        "firstName": "ИВАН",        "patronymic": "ИВАНОВИЧ",        "inn": "123456789012"      },      "position": {        "ogrnip": null,        "typeCode": "02",        "typeName": "Руководитель юридического лица",        "name": "ГЕНЕРАЛЬНЙ ДИРЕКТОР"      },      "disqualification": null    }  ],  "founders": {    "founderULRF": null,    "founderULForeign": null,    "founderFL": [      {        "fl": {          "lastName": "ИВАНОВ",          "firstName": "ИВАН",          "patronymic": "ИВАНОВИЧ",          "inn": "123456789012"        },        "capitalPart": {          "nominal": 20000,          "size": {            "percent": 50,            "decimalPart": null,            "simplePart": null          }        }      },      {        "fl": {          "lastName": "ПЕТРОВ",          "firstName": "ПЕТР",          "patronymic": "ПЕТРОВИЧ",          "inn": "123456789021"        },        "capitalPart": {          "nominal": 20000,          "size": {            "percent": 50,            "decimalPart": null,            "simplePart": null          }        }      }    ],    "founderGov": null,    "founderPIF": null  },  "capitalPart": null,  "holderReestrAO": null,  "okved": {    "mainOkved": {      "code": "47.11",      "name": "Торговля розничная преимущественно пищевыми продуктами, включая напитки, и табачными изделиями в неспециализированных магазинах"    },    "addOkved": null  }}
JOLT спецификация
[{"operation": "modify-overwrite-beta","spec": {"address": {"addressRF": {"region": "=concat(@(type), ' ', @(name))","district": "=concat(@(type), ' ', @(name))","town": "=concat(@(type), ' ', @(name))","settlement": "=concat(@(type), ' ', @(name))","street": "=concat(@(type), ' ', @(name))"}},"director": {"*": {"fl": {"fio": "=concat(@(1,lastName), ' ', @(1,firstName), ' ', @(1,patronymic))"}}},"founders": {"founderFL": {"*": {"fl": {"fio": "=concat(@(1,lastName), ' ', @(1,firstName), ' ', @(1,patronymic))"}}},"founderGov": {"*": {"founderImplFL": {"fl": {"fio": "=concat(@(1,lastName), ' ', @(1,firstName), ' ', @(1,patronymic))"}}}}}}},{"operation": "modify-overwrite-beta","spec": {"address": {"addressRF": {"value": "=concat(@(1,index), ', ', @(1,region), ', ', @(1,district), ', ', @(1,town), ', ', @(1,settlement), ', ', @(1,street), ', ', @(1,house), ', ', @(1,building), ', ', @(1,apartment))","fias": null}}}},{"operation": "shift","spec": {"reportDate|ogrn|ogrnDate|inn|kpp|opfCode|opfName": "&","name": {"*": "&"},"address": {"addressRF": {"kladr|regionCode|value|fias": "&2.&"}},"termination": {"method": {"*": "&2.&"},"*": "&1.&"},"capital": "&","manageOrg": {"egrulData": {"*": "&2.&"}},"director": {"*": {"fl": {"fio|inn": "&3[&2].&"},"position": {"name": "&3[&2].&1","*": "&3[&2].&"},"disqualification": "&2[&1].&"}},"founders": {"founderULRF|founderULForeign": {"*": {"egrulData|foreignReg": {"*": "&4.&3[&2].&"},"*": "&3.&2[&1].&"}},"founderFL": {"*": {"fl": {"fio|inn": "&4.&3[&2].&"},"*": "&3.&2[&1].&"}},"founderGov": {"*": {"govOrg": {"*": "&4.&3[&2].&"},"capitalPart": "&3.&2[&1].&","founderImplUL": {"egrulData": {"*": "&5.&4[&3].&2.&"}},"founderImplFL": {"fl": {"fio|inn": "&5.&4[&3].&2.&"}}}},"founderPIF": {"*": {"PIFName": {"name": "&4.&3[&2].&1"},"manageOrg": {"egrulData": {"*": "&5.&4[&3].&"}},"capitalPart": "&3.&2[&1].&"}}},"capitalPart": "&","holderReestrAO": {"egrulData": {"*": "&2.&"}},"okved": "&"}}]
Результирующий JSON
{  "reportDate" : "2020-05-20",  "ogrn" : "1234567890123",  "ogrnDate" : "2002-12-30",  "inn" : "1234567890",  "kpp" : "123456789",  "opfCode" : "12300",  "opfName" : "Общества с ограниченной ответственностью",  "fullName" : "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ",  "shortName" : "ООО",  "address" : {    "kladr" : "500000570000011",    "regionCode" : "50",    "value" : "143500, ОБЛАСТЬ МОСКОВСКАЯ,  , ГОРОД ИСТРА,  , ПЕРЕУЛОК ВОЛОКОЛАМСКИЙ, , , ",    "fias" : null  },  "capital" : null,  "director" : [ {    "fio" : "ИВАНОВ ИВАН ИВАНОВИЧ",    "inn" : "123456789012",    "ogrnip" : null,    "typeCode" : "02",    "typeName" : "Руководитель юридического лица",    "position" : "ГЕНЕРАЛЬНЙ ДИРЕКТОР",    "disqualification" : null  } ],  "founders" : {    "founderFL" : [ {      "fio" : "ИВАНОВ ИВАН ИВАНОВИЧ",      "inn" : "123456789012",      "capitalPart" : {        "nominal" : 20000,        "size" : {          "percent" : 50,          "decimalPart" : null,          "simplePart" : null        }      }    }, {      "fio" : "ПЕТРОВ ПЕТР ПЕТРОВИЧ",      "inn" : "123456789021",      "capitalPart" : {        "nominal" : 20000,        "size" : {          "percent" : 50,          "decimalPart" : null,          "simplePart" : null        }      }    } ]  },  "capitalPart" : null,  "okved" : {    "mainOkved" : {      "code" : "47.11",      "name" : "Торговля розничная преимущественно пищевыми продуктами, включая напитки, и табачными изделиями в неспециализированных магазинах"    },    "addOkved" : null  }}

Далее

Далее получившийся JSON следует куда-то разместить для хранения и дальнейшего использования. Но это выходит за рамки повествования. Тут уж кому что удобно.

Подробнее..

Aio api crawler

31.10.2020 06:11:16 | Автор: admin
Всем пример. Я начал работать над библиотекой для выдергивания данных из разных json api. Также она может использоваться для тестирования api.

Апишки описываются в виде классов, например

class Categories(JsonEndpoint):    url = "http://127.0.0.1:8888/categories"    params = {"page": range(100), "language": "en"}    headers = {"User-Agent": get_user_agent}    results_key = "*.slug"categories = Categories()class Posts(JsonEndpoint):    url = "http://127.0.0.1:8888/categories/{category}/posts"    params = {"page": range(100), "language": "en"}    url_params = {"category": categories.iter_results()}    results_key = "posts"    async def comments(self, post):        comments = Comments(            self.session,            url_params={"category": post.url.params["category"], "id": post["id"]},        )        return [comment async for comment in comments]posts = Posts()


В params и url_params могут быть функции(как здесь get_user_agent возвращает случайный useragent), range, итераторы, awaitable и асинхронные итераторы(таким образом можно увязать их между собой).

В параметрах headers и cookies тоже могут быть функции и awaitable.

Апи категорий в примере выше возвращает массив объектов, у которых есть slug, итератор будет возвроащать именно их. Подсунув этот итератор в url_params постов, итератор пройдется рекурсивно по всем категориям и по всем страницам в каждой. Он прервется когда наткнется на 404 или какую-то другую ошибку и перейдет к следующей категории.

А репозитории есть пример aiohttp сервера для этих классов чтобы всё можно было протестировать.

Помимо get параметров можно передавать их как data или json и задать другой method.

results_key разбивается по точке и будет пытаться выдергивать ключи из результатов. Например comments.*.text вернет текст каждого комментария из массива внутри comments.

Результаты оборачиваются во wrapper у которого есть свойства url и params. url это производное строки, у которой тоже есть params. Таким образом можно узнать какие параметры использовались для получения данного результата Это демонстрируется в методе comments.

Также там есть базовый класс Sink для обработки результатов. Например, складывания их в mq или базу данных. Он работает в отдельных тасках и получает данные через asyncio.Queue.

class LoggingSink(Sink):    def transform(self, obj):        return repr(obj)    async def init(self):        from loguru import logger        self.logger = logger    async def process(self, obj):        self.logger.info(obj)        return Truesink = LoggingSink(num_tasks=1)


Пример простейшего Sink. Метод transform позволяет провести какие-то манипуляции с объектом и вернуть None, если он нам не подходит. т.е. в тем также можно сделать валидацию.

Sink это асинхронный contextmanager, который при выходе по-идее будет ждать пока все объекты в очереди будут обработаны, потом отменит свои таски.

Ну и, наконец, для связки этого всего вместе я сделал класс Worker. Он принимает один endpoint и несколько sink`ов. Например,

worker = Worker(endpoint=posts, sinks=[loggingsink, mongosink])worker.run()


run запустит asyncio.run_until_complete для pipeline`а worker`а. У него также есть метод transform.

Ещё есть класс WorkerGroup который позволяет создать сразу несколько воркеров и сделать asyncio.gather для них.

В коде есть пример сервера, который генерит данные через faker и обработчиков для его endpoint`ов. Думаю это нагляднее всего.

Всё это на ранней стадии развития и я пока что часто менял api. Но сейчас вроде пришел к тому как это должно выглядеть. Буду раз merge request`ам и комментариям к моему коду.
Подробнее..
Категории: Python , Json , Python 3 , Aiohttp

Очередная причуда Win 10 и как с ней бороться

13.05.2021 00:05:04 | Автор: admin

Квалификацию надо иногда повышать, и вообще учиться для мозгов полезно. А потому пошел я недавно на курсы - поизучать Python и всякие его фреймворки. На днях вот до Django добрался. И тут мы в ходе обучения коллективно выловили не то чтобы баг, но дивный эффект на стыке Python 3, Sqlite 3, JSON и Win 10. Причем эффект был настолько дивен, что гугль нам не помог - пришлось собираться всей заинтересованной группой вместе с преподавателем и коллективным разумом его решать.
А дело вот в чем: изучали мы базу данных (а у Django предустановлена Sqlite 3) и, чтоб каждый раз заново руками данные не вбивать, прикрутили загрузку скриптом из json-файлов. А в файлы данные из базы штатно дампили питоновскими же методами:

python manage.py dumpdata -e contenttypes -o db.json

Внезапно те, кто работал под виндой (за все версии не поручусь, у нас подобрались только обитатели Win 10), обнаружили, что дамп у них производится в кодировке windows-1251. Более того, джейсоны в этой кодировке отлично скармливаются базе. Но стоило только переформатировать их в штатную по документам для Sqlite 3, Python 3 и особенно для JSON кодировку UTF-8, как в лучшем случае кириллица в базе превращалась в тыкву, а в худшем ломался вообще весь процесс загрузки данных.
Ничего подобного найти не удалось ни в документации, ни во всем остальном гугле, считая и англоязычный. Что самое загадочное, ручная загрузка тех же самых данных через консоль или админку проекта работала как часы, хотя уж там-то кодировка была точно UTF-8. Более того, принудительное прописывание кодировки базе никакого эффекта не дало.
Мы предположили, что причиной эффекта было взаимодействие джейсона с операционной системой - каким-то образом при записи и чтении именно джейсонов система навязывала свою родную кодировку вместо нормальной. И действительно, когда при открытии файла принудительно устанавливалась кодировка UTF-8:

open(os.path.join(JSON_PATH, file_name + '.json'), 'r', encoding="utf-8")

в базу попадали не кракозябры, а нормальные русские буквы. Но проблему с созданием дампа таким способом не решишь, а переделывать кодировку потом руками тоже как-то не по-нашему. И тогда мы решили поискать способ укротить винду.
И такой способ нашелся. Вот он:

  • открываем панель управления, но не новую красивую, а старую добрую:

  • открываем (по стрелке) окошко региона:

  • по стрелкам переключаем вкладку "Дополнительно" и открываем окошко "Изменить язык системы":

  • и в нем ставим галку по стрелке в чекбоксе "Бета-версия: Использовать Юникод (UTF-8) для поддержки языка во всем мире.

Система потребует перезагрузки, после чего проблема будет решена.

Не могу сказать, чтобы этот мелкий странный баг был так уж важен или интересен (питоновские проекты обычно живут под линуксами, где такого не бывает), но мозги он нам поломал изрядно - вследствие чего я и решил написать эту заметку. Мало ли кто еще из новичков как раз во время учебы попадется.

Подробнее..
Категории: Python , Python3 , Sqlite , Json , Windows 10 , Encodings

Yaml vs. Json что круче?

17.11.2020 10:12:56 | Автор: admin
image

Всем привет!

Сегодня поговорим об интересном (и таинственном для фронтов) формате YAML. Он считается одним из наиболее популярных форматов для файлов конфигураций.

Файлы с расширением .yaml или .yml вы можете встретить довольно часто, например .travis.yml (для Travis Build), .gitlab-ci.yml (для git lab CI) и др.
И тогда возникают резонные вопросы: что это за формат и чем он отличается от JSON-а?

Цель этой статьи познакомить вас со структурой YAML, помочь понимать, читать и изменять YAML-файлы. Для тех, кто уже знаком с форматом напомнить про некоторые его особенности. И сравнить YAML с JSON.


Цели создания


Сообщество разработчиков устало от зоопарка различных форматов для конфигов, им хотелось упростить себе жизнь и прийти к единому понятному формату. И в 2001 году Кларк Эванс создал YAML 1.0.

Его основные цели:

  1. быть понятным человеку;
  2. поддерживать структуры данных, родные для языков программирования;
  3. быть переносимым между языками программирования;
  4. использовать цельную модель данных для поддержки обычного инструментария;
  5. поддерживать потоковую обработку;
  6. быть выразительным и расширяемым;
  7. быть лёгким в реализации и использовании.


В официальной документации YAML можно увидеть такое определение:



Рис.1. Определение из официальной документации

Сейчас последняя версия YAML 1.2, и она в основном используется как формат для файлов конфигурации Ruby on Rails, Dancer, Symfony, GAE framework, Google App Engine и Dart. Также YAML является основным языком описания классов, ресурсов и манифестов для пакетов приложений OpenStack Murano Project и Swagger.io.

YAML vs. JSON


По сути YAML это язык разметки, который является расширенной версией известного нам формата JSON.

Чтобы лучше разобраться в формате, давайте сначала рассмотрим пример JSON-конфига:

tsconfig.json
{  "compilerOptions": {    "module": "system",    "noImplicitAny": true,    "removeComments": true,    "preserveConstEnums": true,    "outFile": "../../built/local/tsc.js",    "sourceMap": false,    "types": ["node", "lodash", "express"]  },  "include": ["src/**/*"],  "exclude": ["node_modules", "**/*.spec.ts"]}

Этот файл очень легко читается, мы можем быстро определить, что к чему, но нужно знать, что у JSON-формата есть некоторые ограничения:

  • нельзя создавать переменные;
  • нельзя использовать внешние переменные (например, переменные окружения);
  • нельзя переопределять значения.

Чтобы смягчить эти ограничения, можно заменить переменные с помощью создания JSON-файлов, а также использовать .eslintrc или .eslint.js. Но в других языках программирования это не вариант, поэтому YAML и стал так популярен.

Попробуем переписать наш пример в формате YAML. Пока рассмотрим только синтаксис, а остальные особенности чуть позже.

*Хабр не умеет подсвечивать YAML, так что пришлось разместить картинки


Как вам? Мне на первый взгляд показалось, что очень похоже на Python, но с какими-то опечатками.

Рассмотрим синтаксис подробнее.

Концепции, типы, синтаксис


Отступы


В YAML для разделения информации очень важны отступы. Нужно помнить, что используются только пробелы, табы не допускаются.
При отсутствии отступа перед первым объявлением YAML поймет, что это корень (уровень 0) вашего файла.

Если вы привыкли использовать tab-ы вместо пробелов, то можно использовать какой-нибудь плагин в вашей IDE, чтобы заменить все пропуски на пробелы (например, editorconfig).

Ключ/Значение


Как и в JSON/JS, в YAML есть синтаксис ключ/значение, и вы можете использовать его различными способами:



Комментарии


Чтобы написать комментарий, вы можете использовать #, а затем ваше сообщение.



Это круто, когда нужно задокументировать какое-то решение или сделать заметку в конфиге. К сожалению, мы не можем так сделать в JSON.

Списки


В YAML есть 2 способа написания списков:

  • Синтаксис JSON: массив строк

    Помните, что YAML это расширенный JSON? Поэтому мы можем использовать его синтаксис
    people: ['Anne', 'John', 'Max']
    

  • Синтаксис дефиса

    Наиболее распространенный и рекомендуемый
    people: - Anne - John - Max
    


Числа


Тут все стандартно: целые числа и числа с плавающей точкой.


Строки


Есть несколько способов объявить строку в YAML:


Если вы хотите использовать какой-нибудь специальный символ, например, _ или @, то нужны будут кавычки.

Напомню, что в JSON у нас есть только один способ написания строк двойные кавычки.

И главная фишка

Якорь (переменная или ссылка)


Якорь это механизм для создания переменных, на которые затем можно ссылаться.

Давайте представим, что вам нужно создать конфигурацию для вашего CI. Он будет иметь версию для production и development сред. Обе версии будут иметь почти одинаковые базовые настройки.
В JSON нам пришлось бы дублировать эти конфиги:

{  "production": {    "node_version": "13.0.0",    "os": "ubuntu",    "package_manager": "yarn",    "run": ["yarn install", "NODE_ENV=${ENVIRONMENT} yarn build"],    "env": {      "ENVIRONMENT": "production"    }  },  "development": {    "node_version": "13.0.0",    "os": "ubuntu",    "package_manager": "yarn",    "run": ["yarn install", "NODE_ENV=${ENVIRONMENT} yarn build"],    "env": {      "ENVIRONMENT": "development"    }  }}


Копирование и вставка очень раздражают, особенно когда нужно что-то изменить во всех местах.
Якорь решает эту проблему. Для его создания используется символ якоря (&), а для вставки алиас (*).



Итог




Рис.2. Отсылка к фильму Матрица

Возможности YAML-а покоряют при его сравнении с JSON. Но если у вас довольно простой конфиг в пару-тройку строк, то нет смысла усложнять и использовать дополнительные возможности YAML, лучше выбрать JSON. А если же у вас довольно большой и витиеватый файл, то тут однозначно стоит рассмотреть YAML.

У YAML есть еще несколько интересных фишек таких как: многострочный ввод, мерж блоков, матрицы, наследование и другие.

Так как YAML это расширенный JSON, то мы можем писать YAML-файл в JSON-формате, и он будет работать. Это отличная особенность, которая позволит легче начать изучение YAML.

P.S. Если кто-то хочет напоследок большей жести, то можете почитать про сравнение YAML c XML или про переход с XML на YAML.

Полезные ссылки и используемые материалы:


  1. YAML для веб-разработчиков
  2. YAML. Общее описание. Википедия
  3. Документация
  4. Библиотечки для YAML
  5. 10 шагов к YAML-дзену
  6. Некоторые приемы YAML
Подробнее..

Как написать удобный API 10 рекомендаций

25.05.2021 14:04:29 | Автор: admin

Я разработчик и большую часть моей карьеры я строю API различных сервисов. Рекомендации для этой статьи, были собраны на основе наиболее часто встречающихся проблем при проектировании своего сервиса в команде или использовании сторонних API.

Скорее всего вы сталкивались с провайдерами ужасного API. Работа с ними, как правило, сопряжена повышенной эмоциональностью и недопониманием. Большую часть таких проблем можно избежать проектируя интерфейс приложения используя советы ниже.

1. Не используйте глаголы в URL *

* - если это одна из CRUD-операций.

За действие с ресурсом отвечают CRUD-методы запроса: POST - создать (create), GET - получить (read), PUT/PATH - обновить (update), DELETE - удалить (ну вы поняли). Плохо:

POST /users/{userId}/delete - удаление пользователяPOST /bookings/{bookingId}/update - обновление бронировки

Хорошо:

DELETE /users/{userId}PUT /bookings/{bookingId}

2. Используйте глаголы в URL

Плохо:

POST /users/{userId}/books/{bookId}/create - добавить книгу пользователю

Хорошо:

POST /users/{userId}/books/{bookId}/attachPOST /users/{userId}/notifications/send - отправить уведомление пользователю

3. Выделяйте новые сущности

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

POST /wishlist/{userId}/{bookId}

4. Используйте один идентификатор ресурса *

* - если ваша структура данных это позволяет.

Это значит если у вас есть записи вида один ко многим, например
бронь -> путешественники (booking->travellers), вам будет достаточно передавать в запросе идентификатор путешественника.

Плохо:

# получение данных путешественникаGET /bookings/{bookingId}/travellers/{travellerId}

Хорошо:

GET /bookings/travellers/{travellerId}

Так же замечу что /bookings/travellers/ лучше чем просто /travellers хорошо придерживаться иерархии данных в своем API.

5. Все ресурсы во множественном числе

Плохо:

GET /user/{userId} - получение данных пользователяPOST /ticket/{ticketId}/book - бронирование билета

Хорошо:

GET /users/{userId}POST /tickets/{ticketId}/book

6. Используйте HTTP-статусы по максимуму

Самый простой способ обработки ошибок - это ответить соответствующим кодом состояния. В большинстве случает один этот статус может дать исчерпывающую информацию о результате обработки запроса. Одни из самых распространенных кодов ответов:

  • 400 Bad Request - клиент отправил неверный запрос, например, отсутствует обязательный параметр запроса.

  • 401 Unauthorized - клиенту не удалось пройти обязательную аутентификацию на сервере для обработки запроса.

  • 403 Forbidden - клиент аутентифицирован, но не имеет разрешения на доступ к запрошенному ресурсу.

  • 404 Not Found - запрошенный ресурс не существует.

  • 409 Conflict - этот ответ отправляется, когда запрос конфликтует с текущим состоянием сервера.

  • 500 Internal Server Error - на сервере произошла общая ошибка.

  • 503 Service Unavailable - запрошенная услуга недоступна.

7. Модификаторы получения ресурса

Логика построения роутов может быть не связана с архитектурой проекта или структурой базы данных. Например, в бд есть викторины и пройденные викторины - две отдельные таблицы (quizzes и passed_quizzes). Но для апи это могут быть просто викторины, а пройденные викторины это модификатор.

Пример: /quizzes и /quizzes/passed. Здесь quizzes - ресурс (викторины), passed - модификатор (пройденные).

Плохо:

GET /passed-quizzes - получение пройденных викторинGET /booked-tickets - получение забронированных билетовPOST /gold-users - создание премиум пользователя

Хорошо:

GET /tickets/bookedPOST /users/gold

8. Выберите одну структуру ответов

Когда на два запроса к API может быть получен совсем разный по структуре ответ - это грустно. Старайтесь сформировать одну четкую структуру которой всегда будете придерживаться, будет круто еще включить служебные поля, несущие дополнительную информацию.

Плохо:

GET /book/{bookId}{    "name": "Harry Potter and the Philosopher's Stone",    "genre": "fantasy",    "status": 0, # статус вашего приложения    "error": false,     ...}

Хорошо:

GET /book/{bookId}{    "status": 0,    "message": "ok",    "data": {...}}

В этом примере 3 поля универсальны и могут использоваться для любого ответа от апи. status, message - собственный статус и сообщение приложения по которому клиент сможет ориентироваться, эти поля сообщат ему дополнительную информацию о процессе обработки запроса, но не данные ресурса. Например, в нашем приложении в один момент времени, пользователь может проходить только одну викторину. Тогда запрос на начало новой может выдать 409й статус а в полях status и message дополнительную информацию почему была получена ошибка.

9. Все параметры и json в camelCase

9.1 В параметрах запросов
Плохо:

GET /users/{user-id}GET /users/{user_id}GET /users/{userid}

Хорошо:

GET /users/{userId}POST /ticket/{ticketId}/gold

9.2 В теле ответа или принимаемого запроса
Плохо:

{    "ID": "fb6ad842-bd8d-47dd-b7e1-68891d8abeec",    "Name": "soccer3000",    "provider_id": 1455,    "Created_At": "25.05.2020"}

Хорошо:

{    "id": "fb6ad842-bd8d-47dd-b7e1-68891d8abeec",    "Name": "soccer3000",    "providerId": 1455,    "createdAt": "25.05.2020"}

10. Пользуйтесь Content-Type

Плохо:

GET /tickets.jsonGET /tickets.xml

Хорошо:

GET /tickets// и в хедереСontent-Type: application/json// илиСontent-Type: application/xml

Заключение

Перечисленные выше рекомендации это далеко не весь список способов сделать API лучше. Для дальнейшего изучения рекомендую разобрать спецификации REST API и список кодов http-статусов (вы удивитесь на сколько их много и какие ситуации они охватывают).

А в комментариях, предлагаю написать свою рекомендацию по построению REST API которую вы считаете важной.

Подробнее..

Из песочницы Вредные советы для идеального REST API

15.11.2020 14:11:45 | Автор: admin

Всем привет!


Почему 'идеального' написано в кавычках?!

То, что написано ниже относится к разряду "так делать не надо", однако, если вы считаете иначе интересно будет услышать ваше мнение на этот счёт )


Наверное, многие из нас делали REST API, либо пользовались чьим-то готовым. Разберём в статье "невероятные" трюки, которые помогут сделать ваше API на голову выше, чем у других.


Белый пояс


Все значения в json строка и не иначе!


Ну что ж, возьмём простейший объект:


{  "stringValue" : "value",  "intValue": 123}

Вот к чему 123 тут задавать числом, зачем подобная путаница? Пусть будет строкой, десериализатор разберётся:


{  "stringValue" : "value",  "intValue": "123"}

Гораздо лучше, не так ли? Хм А что если у нас объект в качестве значения свойства?


{  "stringValue" : "value",  "intValue": "123",  "complexValue": {    "key": "value"  }}

Мда Непорядок! Надо сделать по-нормальному:


{  "stringValue" : "value",  "intValue": "123",  "complexValue": "{    \"key\": \"value\"  }"}

Что? Там ещё одно вложенное свойство? Ну, так в чем проблема? Рецепт есть уже, всё продумано!


{  "stringValue" : "value",  "intValue": "123",  "complexValue": "{    \"key\": \"value\",    \"anotherComplexValue\": {      \"superKey\": \"megaValue\"    }  }"}

Вот, везде строка! Не надо заморачиваться с типами! Попарсил строчки и будь здоров! Что? Библиотека не парсит как надо? Ну, так бери нормальную, которая всё правильно сделает. complexValue как строка воспринимается? Ну, это вообще ерунда, очевидно, что там объект, просто грамотно обёрнутый в строку.


"Key": Value это скучно, да и на сеть нагрузка большая...


Если в объекте 2-3 свойства, зачем городить сложный объект? Есть хорошее решение:


[  25000,   "Петька",   {    "key1": "value1",    "key2": "value2"  }]

Супер! Надо Петькину зарплату вытащить? Так бери первое значение! Как Петьку зовут? Второе. Так, а с третьим то непорядок!


[  25000,   "Петька",   "{    \"key1\": \"value1\",    \"key2\": \"value2\"  }"]

Во! Теперь по уму! А нет, надо же нагрузку снизить на сеть, у нас ведь 5 запросов в секунду:


[  25000,   "Петька",  [    "value1",    "value2"  ] ]

Просто супер! Вот бы весь json такой был, цены бы ему не было!


Желтый пояс


Порядок не важен


Если проблемка с последним примером: все значения строки, помнишь?


[  "Петька",  "[    \"value1\",    \"value2\"  ]",  "25000"]

Что случилось? Зарплата 3-им значением теперь стала? Ну, да, возможно, но так даже лучше. А какая разница? Там ведь значение 25000, понятно же, что число. Сделать числом? Зачем? Все значения только строки, запомни!


Хранимые процедуры. Ммм Лакомство


Давай что-нибудь с этим джейсоном полезное сделаем. Например, универсальный исполнитель запросов сделаем, без него любой бэкенд не бэкенд. Берём объект:


{  "queryType": "select",  "table": "lyudi",  "where": "name = Витька AND zarplata > 15000"}

и в хранимую процедуру его! Она сама разберётся и запрос твой оптимизирует! Где такую взять? А вот сделал добрый человек, пользуйся на здоровье, да добрым словом поминай)
Супер, да?
Не надо спрашивать как это под капотом работает, штука огонь!
Хотя Постой! Можно же лучше сделать:


{  "query": "select * from lyudi where name = Витька AND zarplata > 15000"}

Красота да и только! Что? Обычный запрос проще сделать? А кто права на это даст? Вот есть хранимая процедура для универсального выполнения запросов, права даём только на неё и ей одной родимой и пользуемся. ORM? Это что за база такая? Нет-нет. MSSQL и точка.


Красный пояс


Всё переводим на "хранимки"! Ням-ням


Вот может вопрос возникнуть: при чем тут API и хранимые процедуры? Всё очевидно: избавляемся от накладных расходов! Вызвал хэпэшку и вуаля! Поэтому никакого rest'а не надо. Смысл такой: из хранимой процедуры возвращаем несколько курсоров, по ним проходимся и всё готово! И быстро и красиво!


А где пример сего чуда?!

Простите, люди добрые, под рукой примера не было, столь он "идеален", что с наскоку не воспроизведу, но ежели сильно нужно, то просьбам вниму, добавлю примерчик)


Ну, а ежели партнёры со сторонних компаний захотят api использовать так vpn можно сделать, пускай тоже по уму делают: хранимые процедуры используют.


Коричневый пояс


Не надо насчёт коричнего цвета что-то не то думать, это почти чёрный! Высокий уровень то-есть!


JSON произвольной структуры


Если с тех. заданием есть проблемы и заказчик не знает до конца чего он хочет, есть отличный метод! Для данных, о которых пока мало-что знаем заготовим объект произвольной структуры:


{}

Вот так он выглядит на начальном этапе:


{  "key1": "value1",  "key2": 2}

потом свойства добавятся:


{  "key1": "value1",  "key2": 2,  "key3": {    "123": 456  }}

но ничего страшного, возможно что вообще всё поменяется:


{  "objectAsArray": ["Vasya", 123, 456, "Piter"]}

Вот и объект красивый получился! Конфетка просто!


Хранение произвольных данных


Тут вообще проблем нет! Произвольный JSON пишем в базу: для этого заводим столбец с типом VARCHAR(MAX). Всё гениальное просто!


Оптимизация и ничего кроме!


Вот был раньше формат dbf, отчего про него забыли нынче? Применим!


{  "data": "Vasya     123  456  Piter                  "}

10 символов для имени, 5 для номера квартиры, 5 для номер дома, 20 для города. Универсально, красиво! Да, иногда символы лишние, но зато какая структура красивая!


Чёрный пояс


Быть ближе к железу!


Строка это что? Массив байтов, ну, так и давайте следовать определению!


{  "data": [56, 61, 73, 79, 61, 20, 20, 20, 20, 20, 31, 32, 33, 20, 20, 34, 35, 36, 20, 20, 50, 69, 74, 65, 72, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20]}

Чуть не забыл! Значения должны быть строкой:


{  "data": "[56, 61, 73, 79, 61, 20, 20, 20, 20, 20, 31, 32, 33, 20, 20, 34, 35, 36, 20, 20, 50, 69, 74, 65, 72, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20]"}

И универсально, и красиво!


P.S. Не столь "идеальный" P.S. как всё написанное выше...


Если вам показалось, что автор утрирует и выдумывает, то Автор хотел бы, чтобы так оно и было. Однако, сталкиваться приходилось с каждым из этих случаев. Давайте уважать друг друга и делать добро, а не какую-то ерунду) Спасибо за внимание, надеюсь, местами было не только грустно, но и весело!

Подробнее..
Категории: Sql , Api , Json , Rest , Хранимые процедуры

Работа с сложными JSON-объектами в Swift (Codable)

20.03.2021 20:11:26 | Автор: admin

Написать эту статью меня сподвиг почти случившийся нервный срыв, причиной которого стало мое желание научиться общаться с сторонними API, конкретно меня интересовал процесс декодирования JSON-докуметов! Нервного срыва я, к счастью, избежал, поэтому теперь настало время сделать вклад в сообщество и попробовать опубликовать свою первую статью на Хабре.

Почему вообще возникли проблемы с такой простой задачей?

Чтобы понять, откуда проблемы, нужно сначала рассказать об инструментарии, которым я пользовался. Для декодирования JSON-объектов я использовал относительно новый синтезированный (synthesized) протокол библиотеки Foundation - Сodable.

Codable - это встроенный протокол, позволяющий заниматься кодированием в объект текстового формата и декодированием из текстового формата. Codable - это протокол-сумма двух других протоколов: Decodable и Encodable.

Cтоит сделать оговорку, что протокол называется синтезированным тогда, когда часть его методов и свойств имеют дефолтную реализацию. Зачем такие протоколы нужны? Чтобы облегчить работу с их подписанием, как минимум уменьшив количество boilerplate-кода.

Еще такие протоколы позволяют работать с композицией, а не наследованием!

Вот теперь поговорим о проблемах:

  • Во-первых, как и все новое, этот протокол плохо описан в документации Apple. Что я имею в виду под "плохо описан"? Разбираются самые простые случаи работы с JSON объектами; очень кратко описаны методы и свойства протокола.

  • Во-вторых, проблема языкового барьера. На понимание того, как составить поисковой запрос ушло несколько часов. Нервы в это время кончались с молниеносной скоростью.

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

Теперь давайте поговорим о конкретном кейсе. Кейс такой: используя API сервиса Flickr, произвести поиск N-фотографий по ключевому слову (ключевое слово: читай поисковой запрос) и вывести их на экран.

Сначала все стандартно: получаем ключ к API, ищем нужный REST-метод в документации к API, смотрим описание аргументов запроса к ресурсу, cоставляем и отправляем GET-запрос.

И тут видим это в качестве полученного от Flickr JSON объекта:

{   "photos":{      "page":1,      "pages":"11824",      "perpage":2,      "total":"23648",      "photo":[         {            "id":"50972466107",            "owner":"191126281@N@7" ,            "secret":"Q6f861f8b0",            "server":"65535",            "farm":66,            "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",            "ispublic":1,            "isfriend":0,            "isfamily":0         },         {            "id":"50970556873",            "owner":"49965961@NG0",            "secret":"21f7a6424b",            "server":"65535",            "farm" 66,            "title":"IMG_20210222_145514",            "ispublic":1,            "isfriend":0,            "isfamily":0         }      ]   },   "stat":"ok"}

Ага, все вроде бы хорошо. Но где же фотографии? А фотографий здесь нет, потому что это логично. Почему логично? Фото - тяжелый объект, передающийся по сети в виде последовательности байт (чем лучше качество фото, тем больше байт). Так вот из-за того, что фото громоздкие, а интерфейс приложения в идеале должен быть отзывчивым, поэтому информация о таких объектах передается в виде их местонахождения (местонахождения той самой последовательности байт) на другом ресурсе (сервере) сервиса Flickr для того, чтобы потом можно было распараллелить процесс загрузки фотографий.

Что имеем? Имеем ситуацию, где необходимо составить два последовательных GET-запроса к разным ресурсам, причем второй запрос будет использовать информацию первого! Соответственно алгоритм действий: запрос-декодирование-запрос-декодирование. И вот тут. и начались проблемы. От JSON-объекта полученного после первого запроса мне были нужен только массив с информацией о фото и то не всей, а только той, которая репрезентует информацию о его положении на сервере. Эту информацию предоставляют поля объекта "photo": "id", "secret", "server"

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

struct Photo {    let id: String    let secret: String    let server: String}let results: [Photos] = // ...

Вся остальная "мишура" на не нужна. Так вот материала, описывающего best practices обработки такого JSON-объекта очень мало.

Давайте разбираться. Сначала предположим, что никакой лишней информации нам не пришло. Таким образом я хочу ввести в курс дела тех, кто не работал с Codable. Представим, что наш объект выглядит вот так.

{   "id":"50972466107",   "owner":"191126281@N07",   "secret":"06f861f8b0"}

Здесь все предельно просто. Нужно создать структуру данных, имена свойств которой будут совпадать с ключами JSON-объекта (здесь это "id", "secret", "server"); типы свойств нашей структуры также обязаны удовлетворять типам, которым равны значения ключей (надеюсь, не запутал). Далее нужно просто подписаться на протокол Decodable, который сделает все за нас, потому что он умеет работать с вложенными типами (то есть если типы свойств являются его подписчиками, то и сам объект тоже будет по умолчанию на нее подписан). Что значит подписан? Это значит, что все методы смогут определиться со своей реализацией "по умолчанию". Далее процесс парсинга целиком. (Я декодирую из строки, которую предварительно перевожу в объект типа Data, потому что метод decode(...) объекта JSONDecoder работает с Data).

Полезные советы:

  • Используйте сервисы, чтобы тренироваться на простых примерах, например если вы не хотите работать не с чьим API - используйте сервис jsonplaceholder.typicode.com, он даст вам доступ к простейшим JSON-объектами, получаемым с помощью GET-запросов.

  • Также полезным на мой взгляд является сервис jsonformatter.curiousconcept.com . Он предоставляет доступ к функционал выравниванивания "кривых" REST объектов, которые обычно показывает нам консоль Playground Xcode.

  • Последний мощный tool - app.quicktype.io - он описывает структуру данных на Swift по конкретному JSON-объекту.

Вернемся к мучениям. Парсинг:

struct Photo: Decodable {    let id: String    let secret: String    let server: String}let json = """{   "id":"50972466107",   "owner":"191126281@N07",   "secret":"06f861f8b0"}"""let data = json.data(using: .utf8)let results: Photo = try! JSONDecoder().decode(Photo.self, from: data)

Обратите внимание на то, что любой ключ JSON-объекта, использующий следующую нотацию "key" : "sometexthere" для Decodable видится как видится как String, поэтому такой код создаст ошибку в run-time. Decodable не умеет явно coerce-ить (приводить типы).

struct Photo: Decodable {    let id: Int    let secret: String    let server: Int}let json = """{   "id":"50972466107",   "owner":"191126281@N07",   "secret":"06f861f8b0"}"""let data = json.data(using: .utf8)let results: Photo = try! JSONDecoder().decode(Photo.self, from: data)

Усложним задачу. А что если нам пришел такой объект?

    {       "id":"50972466107",       "owner":"191126281@N07",       "secret":"06f861f8b0",       "server":"65535",       "farm":66,       "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",       "ispublic":1,       "isfriend":0,       "isfamily":0    }

Здесь все элементарно, потому что Decodable умный протокол, который умеет парсить только те свойства, которые описывает наша структура и не "ругаться " на то, что некоторые свойства отсутствуют. Это логично, потому что хоть API и должно являться устойчивым, по мнению последователей "Чистого архитектора" Роберта Мартина, но никто не гарантирует нам то, что его разработчики баз данных не захотят, например, внести новых свойств. Если бы это не работало - наши приложения бы постоянно "крашились".

Разберем дополнительный функционал доступный из коробки. Следующий вид JSON-объекта:

[    {       "id":"50972466107",       "owner":"191126281@N07",       "secret":"06f861f8b0",       "server":"65535",       "farm":66,       "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",       "ispublic":1,       "isfriend":0,       "isfamily":0    },    {       "id":"50970556873",       "owner":"49965961@N00",       "secret":"21f7a6524b",       "server":"65535",       "farm":66,       "title":"IMG_20210222_145514",       "ispublic":1,       "isfriend":0,       "isfamily":0    }]

Тут придется добавить всего несколько логичных строк (или даже символов) кода. Помним, что массив объектов конкретного типа - это тоже тип!

struct Photo: Decodable {    let id: String    let secret: String    let server: String}let json = """[    {       "id":"50972466107",       "owner":"191126281@N07",       "secret":"06f861f8b0",       "server":"65535",       "farm":66,       "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",       "ispublic":1,       "isfriend":0,       "isfamily":0    },    {       "id":"50970556873",       "owner":"49965961@N00",       "secret":"21f7a6524b",       "server":"65535",       "farm":66,       "title":"IMG_20210222_145514",       "ispublic":1,       "isfriend":0,       "isfamily":0    }]"""let data = json.data(using: .utf8)let results: [Photo] = try! JSONDecoder().decode([Photo].self, from: data)

Добавление квадратных скобок к имени типа все сделало за нас, а все потому что [Photo] - это конкретный тип в нотации Swift. На что можно напороться с массивами: массив из одного объекта - тоже массив!

Теперь разберем последний, обсуждаемый везде функционал, который пригодится в дальнейшем.

Сначала залезем немного "под капот" Decodable и еще раз поймем, что такое JSON.

  • JSON-объект - это текст, удовлетворяющий заданной над ним формальной грамматике. Формальная грамматика - набор правил, задающих будущий вид объектов. Формальная грамматика однозначно может определить, является ли текст JSON-объектом или нет! Придуман он для того, чтобы обеспечить кросс-платформенность, потому что такой элементарный тип как Character есть в любом языке, а значит можно его использовать для идентификации содержимого JSON-объекта и дальнейшего приведения к клиентским структурам данных.

  • JSON - ОО текстовый формат. Что же это значит? То, что он общается с программистами на уровне объектов в привычном понимании ООП, только объекты эти утрированные: методы с помощью JSON мы передать не можем (можем, но зачем). Что все это значит? Это значит то, что любая открывающая фигурная скобка открывает новый контейнер (область видимости)

  • Decodable может перемещаться по этим контейнерам и забирать оттуда только необходимую нам информацию.

Сначала поймем, каким инструментарием необходимо пользоваться. Протокол Decodable определяет generic enum CodingKeys, который сообщает парсеру (декодеру) то, как именно необходимо соотносить ключ JSON объекта с названием свойства нашей структуры данных, то есть это перечисление является ключевым для декодера объектом, с помощью него он понимает, в какое именно свойство клиентской структуры присваивать следующее значение ключа! Для чего может быть полезна перегрузка этого перечисления в топорных условиях, я думаю всем ясно. Например, для того, чтобы соблюсти стиль кодирования при парсинге: JSON-объект использует snake case для имен ключей, а Swift поощряет camel case. Как это работает?

struct Photo: Decodable {    let idInJSON: String    let secretInJSON: String    let serverInJSON: String        enum CodingKeys: String, CodingKey {        case idInJSON = "id_in_JSON"        case secretInJSON = "secret_in_JSON"        case serverInJSON = "server_in_JSON"    }}

rawValue перечисления CodingKeys говорят, как выглядят имена наших свойств в JSON-документе!

Отсюда мы и начнем наше путешествие внутрь контейнера! Еще раз посмотрим на JSON который нужно было изначально декодировать!

{   "photos":{      "page":1,      "pages":"11824",      "perpage":2,      "total":"23648",      "photo":[         {            "id":"50972466107",            "owner":"191126281@N@7" ,            "secret":"Q6f861f8b0",            "server":"65535",            "farm":66,            "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",            "ispublic":1,            "isfriend":0,            "isfamily":0         },         {            "id":"50970556873",            "owner":"49965961@NG0",            "secret":"21f7a6424b",            "server":"65535",            "farm" 66,            "title":"IMG_20210222_145514",            "ispublic":1,            "isfriend":0,            "isfamily":0         }      ]   },   "stat":"ok"}

Опишем контейнеры:

  • Первый контейнер определяет объект, состоящий из двух свойств: "photos", "stat"

  • Контейнер "photos" в свою очередь определяет объект состоящий из пяти свойств: "page", "pages", "perpages", "total", "photo"

  • "photo" - просто массив контейнеров, некоторый свойства из которых нам нужны.

Как можно решать задачу в лоб?

  • Объявить кучу вложенных типов и радоваться жизни. Итог пишем dummy алгоритмы ручного приведение между уже клиентскими объектами. Это нехорошо!

  • Пользоваться функционалом Decodable протокола, а точнее его перегружать дефолтную реализацию инициализатора и переопределять CodingKeys! Это хорошо! Оговорка: к сожалению Swift (по понятным причинам!) не дает определять в extension stored properties, а computed properties не видны Сodable/Encodable/Decodable, поэтому красиво работать с чистыми JSON массивами не получится.

Решая вторым способом, мы прокладываем для декодера маршрут к тем данным, которые нам нужны: говорим ему зайди в контейнера photos и забери массив из свойства photo c выбранными нами свойствами

Сразу приведу код этого решения и уже потом объясню, как он работает!

// (1) Определили объект Photo только с необходимыми к извлечению свойствами.struct Photo: Decodable {    let id: String    let secret: String    let server: String}// (2) Определяем JSONContainer, то есть описываем куда нужно идти парсеру и что забирать.struct JSONContainer: Decodable {    // (3) photos совпадает c именем ключа "photos" в JSON, но теперь мы написали, что хранить этот ключ будет не весь контейнер, а только его часть - массив, который является значением ключа photo!    let photos: [Photo]}extension JSONContainer {    // (4) Описываем CodingKeys для парсера.    enum CodingKeys: String, CodingKey {        case photos        // (5) Здесь определяем только те имена ключей, которые будут нужны нам внутри контейнера photos.        // (6) Здесь необязательно соблюдать какие-то правила именования, но название PhotosKeys - дает представление о том, что мы рассматриваем ключи внутри значения ключа photos        enum PhotosKeys: String, CodingKey {            // (7) Описываем конкретно интересующий нас ключ "photo"            case photoKey = "photo"        }    }    // (8) Дальше переопределяем инициализатор    init(from decoder: Decoder) throws {        // (9) Заходим внутрь JSON, который определяется контейнером из двух ключей, но нам из них нужно только одно - photos        let container = try decoder.container(keyedBy: CodingKeys.self)        // (10) Заходим в контейнер (nested - вложенный) ключа photos и говорим какие именно ключи смы будем там рассматривать        let photosContainer = try container.nestedContainer(keyedBy: CodingKeys.PhotosKeys.self, forKey: .photos)        // (11) Декодируем уже стандартным методом        // (12) Дословно здесь написано следующее положи в свойство photos объект-массив, который определен своим типом и лежит .photoKey (.photoKey.rawValue == "photo")        photos = try photosContainer.decode([Photo].self, forKey: .photoKey)    }}

Вот и все, теперь когда экземпляр объекта JSONDecoder. Будет вызывать decode() - под капотом он будет использовать наш инициализатор для работы с декодированием

Подводя итоги, хотелось бы сказать, что работа с сетью в iOS-разработке наполнена разнообразными "сюрпризами", поэтому если вы можете что-то добавить в комментариях - обязательно делайте это!

Всем спасибо!

P.S. По прошествии некоторого времени был сделан вывод, что мапить к итоговой структуре в коде, пользуясь только встроенным поведением Codable, нормально. Вывод был сделан после просмотра сессии WWDC, разбирающей работу с данными пришедшими из сети

Ссылка на сессию

Подробнее..

Как настроить мониторинг любых бизнес-процессов, в БД Oracle построение графиков, используя бесплатную версию Grafana

10.01.2021 14:07:00 | Автор: admin

Вводные. Зачем мне это было нужно

Лично мне нужно было организовать мониторинг домашней солнечной электростанции.

Кратко о матчасти (хотя этот пост не про неё):

  • Инвертор МАП Энергия и 3 солнечных контроллера того же производителя.

  • Внутри инвертора установлен микрокомпьютер (производитель его называет "Малина"), который кое-что умеет в плане мониторинга, но не всё что мне нужно, и не очень удобно. Ценность микрокомпьютера в том, что он снимает данные с com-портов инвертора и контроллеров и публикует их насвоём http-сервере в виде Json. Данные веб-сервисов обновляются примерно каждую секунду. Также есть веб-сервисы для управления встроенными в контроллеры и инвертор реле

  • Парочка Ethernet-устройств SR-201 это такие платы с релюхами, используются для управления нагрузкой и кое-чем еще, управляются по протоколу tcp и udp.

  • Домашний сервер под управлением Centos-8, на нём установлен Oracle (разумеется Express Edition со всеми своими ограничениями, но для домашнего сервера достаточно)

  • В оракле крутятся 2 JOBa (на самом деле это persistent процессы, которые крутят бесконечный цикл и перезапускаются примерно раз в полчаса):

    1. Раз в секуну снимает данные с вебсервисов "Малины", текущее состояние реле устройств SR-201 и пишет это всё в БД Oracle. С Малины снимает с помощью несложных функций на основе utl_http, с реюх - через utl_tcp. Собственно это и есть статистика, которую будем мониторить

    2. Постоянно пересчитывает статистику за некоторый промежуток времени, и на основе полученных результатов, управляет нагрузкой и еще кое-чем через SR-201 и встроенные реле инвертора и контроллеров.

Вот это всё хозяйство мне нужно мониторить. Причем мониторить не события (событиями занимаетс Job2), а строить графики на основе накопленной статистической информации, визуализировать их на компе и мобилке. Сама "Малина" кое-что умеет, но во-первых не всё (про мои SR-201 она точно ничего не знает), во-вторых неудобный интерфейс - нельзя всё посмотреть на одном экране в удомном мне виде, а в третьих - в некоторых местах кривовато.

Вопросы: Почему Oracle а не Postgres например? Ну просто лень, хотелось сделать из того что умею... :-)

Выбор пал на Grafana https://grafana.com - довольно мощное средство визуализации статистики и прочей ерунды. Легко настраивается, удобно использовать. Работает с многими БД...

Собственно описание проекта

Данный пост ни разу не претендует на качественный пошаговый tutorial, я лишь хочу указать путь, по которому Вам будет легче идти.

Итак:

Устанавливаем grafana

$ sudo nano /etc/yum.repos.d/grafana.repo[grafana]name=grafanabaseurl=https://packages.grafana.com/oss/rpmrepo_gpgcheck=1enabled=1gpgcheck=1gpgkey=https://packages.grafana.com/gpg.keysslverify=1sslcacert=/etc/pki/tls/certs/ca-bundle.crt
dnf updatednf install grafanasystemctl daemon-reloadsystemctl enable --now grafana-serversystemctl status grafana-server

Selinux у меня отключен, файрвол тоже, так что в эти нюансы вдаваться не буду

Далее одна проблемка: Grafana конечно с Oracle работать умеет, но данная опция (плагин) предоставляется только в Enterprise версии, которая начинается от 24к$ и это в мои планы не входит. Устанавливаем плагин grafana-simple-json-datasource

grafana-cli plugins install grafana-simple-json-datasourcesystemctl restart grafana-server

То есть графана у нас в оракл ходить не будет. Она будет брать данные из вебсервиса, теперь дело за малым - вебсервис написать.

Вебсервис будем делать на apache + php

Для этого потребуется установить и настроить:

httpd, php и php-fpm (у меня php 7.2) установлен и сконфигрирован вместе с freepbx которая живёт на том же сервере :-)

Для php нужно подключить библиотеку oci8 - тут есть сложность в том, что для php 7.2 не получится поставить oci8 командой pecl.

В общем путь такой:

Подключаем репозиторий remi, и оттуда:

dnf install php-pecl-oci8

Подключаем oci8 к php

/etc/hp.d/20-oci8.ini

В принципе достаточно раскомментировать 1 строку

extension=oci8.so

Далее этот oci8 не очень хочет запускаться, тут помогут примерно такие строки в

/etc/php-fpm.d/www.conf

env[ORACLE_HOSTNAME] = myserver.localdomainenv[ORACLE_UNQNAME] = mydbenv[ORACLE_BASE] = /u01/app/oracleenv[ORACLE_HOME] = /u01/app/oracle/product/18.4.0/dbhome_1env[ORA_INVENTORY] = /u01/app/oraInventoryenv[ORACLE_SID] = mydbenv[LD_LIBRARY_PATH] = /u01/app/oracle/product/18.4.0/dbhome_1/lib:/lib:/usr/libenv[NLS_LANG] = AMERICAN_CIS.UTF8

Теперь при исполнении php-скрипта на вебсервере, oci8 прекрасно запускается

Выкладываем скрипт на вебсервер

/var/www/html/gr/gr.php

<?phpheader("Content-Type: application/json;");$conn = oci_pconnect('www', 'www$password', 'mydb', 'AL32UTF8');if (!$conn) {    $e = oci_error();    trigger_error(htmlentities($e['message'], ENT_QUOTES), E_USER_ERROR);}// Подготовка выражения$stid = oci_parse($conn, 'begin  LGRAFANA.GetJson(:vPath, :vInp, :vOut); end;');if (!$stid) {    $e = oci_error($conn);    trigger_error(htmlentities($e['message'], ENT_QUOTES), E_USER_ERROR);}// Создадим дескрипторы$vInp = oci_new_descriptor($conn, OCI_DTYPE_LOB);$vOut = oci_new_descriptor($conn, OCI_DTYPE_LOB);// Привяжем переменные$vPath = $_SERVER["PATH_INFO"];$postdata = file_get_contents("php://input");$vInp->writeTemporary($postdata, OCI_TEMP_BLOB);oci_bind_by_name($stid, ":vPath", $vPath);oci_bind_by_name($stid, ":vInp", $vInp, -1, OCI_B_BLOB);oci_bind_by_name($stid, ":vOut", $vOut, -1, OCI_B_BLOB);// Выполним логику запроса$r = oci_execute($stid);if (!$r) {    $e = oci_error($stid);    trigger_error(htmlentities($e['message'], ENT_QUOTES), E_USER_ERROR);}echo $vOut->load(); $vInp ->close();$vOut ->close();oci_free_statement($stid);oci_commit($conn);oci_close($conn);?>

Вебсервис готов.

В нашей БД есть пакет LGRAFANA, из которого наружу торчит только одна процедура

procedure GetJson(pPathInfo in varchar2, pInpPost in blob, pOutPost out blob);

она и будет обрабатывать Json - запросы от графаны. Эту процедуру целиком приведу чуть ниже, Вы понимаете что это лишь пример для моей электростанции, а в целом формат Json запросов-ответов описан тут

https://grafana.com/grafana/plugins/grafana-simple-json-datasource

Теперь настройка в самой графане:

Configuration - Data Sources - Add DataSource - Simple JSON

Дальше можно идти добавлять DashBoard и накидывать туда панели с нужными графиками

... Если у Вас уже есть реализация пакета LGRAFANA разумеется.

Да кстати про пакет. Он у меня написан не совсем на PL/SQL, но в целом Вы сможете это использовать для того чтобы понять, что надо написать в своём пакете. Это легко переводится на pl/sql.

Вкратце так:

  1. Реализуем метод, который реагирует на pahinfo=/search и отдаёт массив имён метрик которые мы умеем считать

  2. Реализуем метод /query который формирует массив данных по нужным метрикам

Полный текст пакета
pragma include([DEBUG_TRIGGER]::[MACRO_LIB]);CPALL const varchar2(30) := 'Мощность нагр.';CPNET const varchar2(30) := 'Мощность сеть';CPACB const varchar2(30) := 'Мощность АКБ';CPI2C const varchar2(30) := 'Мощность I2C';CPADD const varchar2(30) := 'Доп. Нагрузка';CPMP1 const varchar2(30) := 'Мощность MPPT1';CPMP2 const varchar2(30) := 'Мощность MPPT2';CPMP3 const varchar2(30) := 'Мощность MPPT3';CEDAY const varchar2(30) := 'Выработка за день';CEMP1 const varchar2(30) := 'Выработка MPPT1';CEMP2 const varchar2(30) := 'Выработка MPPT2';CEMP3 const varchar2(30) := 'Выработка MPPT3';CETOB const varchar2(30) := 'На заряд батареи';CEFRB const varchar2(30) := 'Взято от батареи';CEFRN const varchar2(30) := 'Взято от сети';CUNET const varchar2(30) := 'Напряжение сети';CUOUT const varchar2(30) := 'Напряжение выход';CUACB const varchar2(30) := 'Напряжение АКБ';public function TsToUTs(v_Ts in timestamp) return number isv_Dt date;beginv_Dt := v_ts;return trunc((v_Dt - to_date('01.01.1970','DD.MM.YYYY')) -- Кол-во дней с 1 янв 1970 * (24 * 60 * 60)) -- Теперь это кол-во секунд * 1000 -- Теперь миллисекунд + to_number(to_char(v_ts,'FF3')); -- Добавили миллисекундыend;procedure get_query(pInp in out nocopy JSON_OBJECT_T, pOut in out nocopy JSON_ARRAY_T) istype rtflag is record ( fTp varchar2(30),fOb json_object_t,fAr json_array_t);type ttflag is table of rtflag index by string;tflag ttflag;vTmpOb json_object_t;vTmpAr json_array_t;vTmpId varchar2(30);vDBeg timestamp;vDEnd timestamp;vDDBeg date;vDDEnd date;num_tz number;curts number;function GetFlag(pFlagName in varchar2) return boolean isbeginif tflag.exists(pFlagName) thenreturn true;elsereturn false;end if;end;--function GetFlagType(pFlagName in varchar2) return varchar2 is--begin--if tflag.exists(pFlagName) then--return tflag(pFlagName).fTp;--else--pragma error('Нет значения ['||pFlagName||'] в мвссиве tflag');--end if;--end;procedure AddTrgData(pTrgName in varchar2, pStamp in number, pValue in number) isbeginvTmpAr := Json_Array_t;vTmpAr.append(pValue);vTmpAr.append(pStamp);tFlag(pTrgName).fAr.append(vTmpAr);end;begin&debug('pInp='||pInp.to_string())vTmpOb := pInp.get_Object('range');num_tz := to_number(::[GA_MAP_STAT].[LIB].GetSetting('MALINA_TIME_ZONE'));vDBeg := vTmpOb.get_Timestamp('from') + numtodsinterval(num_tz,'hour');vDEnd := vTmpOb.get_Timestamp('to')   + numtodsinterval(num_tz,'hour');vDDBeg := to_date(to_char(vDBeg,'dd.mm.yyyy hh24:mi:ss'),'dd.mm.yyyy hh24:mi:ss');vDDEnd := to_date(to_char(vDEnd,'dd.mm.yyyy hh24:mi:ss'),'dd.mm.yyyy hh24:mi:ss');&debug('vDBeg='||to_char(vDBeg,'dd.mm.yyyy hh24:mi:ss:ff'))&debug('vDEnd='||to_char(vDEnd,'dd.mm.yyyy hh24:mi:ss:ff'))vTmpAr := pInp.get_Array('targets');for i in 0 .. vTmpAr.get_size - 1 loopvTmpOb := JSON_OBJECT_T(vTmpAr.get(i));vTmpId := vTmpOb.get_string('target');tflag(vTmpId).fTp := vTmpOb.get_string('type');tflag(vTmpId).fOb := Json_object_t;tflag(vTmpId).fAr := Json_array_t;tflag(vTmpId).fOb.put('target',vTmpId);end loop;-- Взять значения мощностей из статистики МАПif GetFlag(CPALL) or GetFlag(CPNET) or GetFlag(CPACB) or GetFlag(CPI2C) or GetFlag(CUNET) or GetFlag(CUOUT) or GetFlag(CUACB) thenfor (select x(x.[QTIME]:qtime, x.[F__PNET_CALC]:pnet -- Мощность сеть, - x.[F__PLOAD_CALC] + x.[F__PNET_CALC]:pall -- Мощность нагр., - x.[F__PLOAD_CALC]:pacb -- Мощность АКБ, x.[F__P_MPPT_AVG]:pi2c -- Мощность I2C, x.[F__UNET]:unet, x.[F__UOUTMED]:uout, x.[F__UACC]:uacb) in ::[GA_MAP_STAT] allwhere x.[QTIME] >= vDBeg and x.[QTIME] <= vDEndorder by x.[QTIME]) loopcurts := TsToUTs(x.qtime - numtodsinterval(num_tz,'hour'));vTmpId := tflag.first;while vTmpId is not null loopcase vTmpId of:CPALL: AddTrgData(vTmpId,curts,x.pall);:CPNET: AddTrgData(vTmpId,curts,x.pnet);:CPACB: AddTrgData(vTmpId,curts,x.pacb);:CPI2C: AddTrgData(vTmpId,curts,x.pi2c);:CUNET: AddTrgData(vTmpId,curts,x.unet);:CUOUT: AddTrgData(vTmpId,curts,x.uout);:CUACB: AddTrgData(vTmpId,curts,x.uacb);end;vTmpId := tflag.next(vTmpId);end loop;end loop;end if;-- Взять статистику панелейif GetFlag(CPMP1) or GetFlag(CPMP2) or GetFlag(CPMP3) thenfor (select x(x.[QTIME]:qtime,x.[F_UID]:fuid,x.[F_P_CURR]:fpower -- Мощность заряда) in ::[GA_MPPT_STAT] allwhere x.[QTIME] >= vDBeg and x.[QTIME] <= vDEndorder by x.[QTIME], x.[F_UID]) loopcurts := TsToUTs(x.qtime - numtodsinterval(num_tz,'hour'));case x.fuid of:1: if GetFlag(CPMP1) then AddTrgData(CPMP1,curts,x.fpower); end if;:2: if GetFlag(CPMP2) then AddTrgData(CPMP2,curts,x.fpower); end if;:3: if GetFlag(CPMP3) then AddTrgData(CPMP3,curts,x.fpower); end if;end;end loop;end if;-- Взять значения мощностей из статистики допнагрузкиif GetFlag(CPADD) thendeclaretqend timestamp;paend number;beginfor (select x(  x.[QTIME]:qtime, x.[FPOWER]:padd -- Доп. Нагрузка) in ::[GA_LOAD_H] allwhere x.[QTIME] >= (select x(nvl(max(x.[QTIME]),to_timestamp('01.01.1970','dd.mm.yyyy')))in ::[GA_LOAD_H] allwhere x.[QTIME] < vDBeg)and x.[QTIME] < vDEndorder by x.[QTIME]) loopcurts := TsToUTs(x.qtime - numtodsinterval(num_tz,'hour'));tqend := x.qtime;paend := x.padd;AddTrgData(CPADD,curts,x.padd);end loop;curts := TsToUTs(vDEnd - numtodsinterval(num_tz,'hour'));AddTrgData(CPADD,curts,paend);end;end if;-- Взять значения выработки по датамif GetFlag(CEDAY) or GetFlag(CEMP1) or GetFlag(CEMP2) or GetFlag(CEMP3) or GetFlag(CEFRN) thendeclarevDEBeg date;vDEEnd date;vDECur date;curEn number;prven number;vTSCur timestamp;curEnToBat number;curEnFromBat number;procedure GetCeMp(vCeMp in varchar2, vMpUID in number) isbeginvDECur := vDEBeg;while vDECur <= vDEEnd loopvTSCur := to_timestamp(to_char(vDECur,'dd.mm.yyyy'),'dd.mm.yyyy');select x(nvl(max(x.[F_PWR_KW]),0)*1000) in ::[GA_MPPT_STAT] allwhere x.[qtime] >= vTSCurand x.[qtime] < (vTSCur + numtodsinterval(1,'day'))and x.[F_TIMESTAMP] >= vDECurand x.[F_TIMESTAMP] < (vDECur+1)and x.[F_UID] = vMpUIDinto curEn;curts := TsToUTs(vTsCur - numtodsinterval(num_tz,'hour'));AddTrgData(vCeMp,curts,curen);vDECur := vDECur + 1;end loop;end;beginvDEBeg := trunc(vDDBeg);vDEEnd := trunc(vDDEnd);if GetFlag(CEDAY) or GetFlag(CETOB) or GetFlag(CEFRB) thenvDECur := vDEBeg;while vDECur <= vDEEnd loopvTSCur := to_timestamp(to_char(vDECur,'dd.mm.yyyy'),'dd.mm.yyyy');select x( nvl(max(x.[S1].[F_MPPT_DAY_E]),0),nvl(max(x.[S1].[F_ESUM_TO_BAT]),0),nvl(max(x.[S1].[F_ESUM_FROM_BAT]),0)) in ::[GA_BAT_STAT] allwhere x.[qtime] >= vTSCurand x.[qtime] < (vTSCur + numtodsinterval(1,'day'))and x.[S1].[F_TIMESTAMP] >= vDECurand x.[S1].[F_TIMESTAMP] < (vDECur+1)into curEn,curEnToBat,curEnFromBat;curts := TsToUTs(vTsCur - numtodsinterval(num_tz,'hour'));if GetFlag(CEDAY) then AddTrgData(CEDAY,curts,curen); end if;if GetFlag(CETOB) then AddTrgData(CETOB,curts,curenToBat); end if;if GetFlag(CEFRB) then AddTrgData(CEFRB,curts,curenFromBat); end if;vDECur := vDECur + 1;end loop;end if;if GetFlag(CEMP1) thenGetCeMp(CEMP1,1);end if;if GetFlag(CEMP2) thenGetCeMp(CEMP2,2);end if;if GetFlag(CEMP3) thenGetCeMp(CEMP3,3);end if;-- Посчитать сколько взято от сетиif GetFlag(CEFRN) thenvDECur := vDEBeg-1;prven := null;while vDECur <= vDEEnd loopvTSCur := to_timestamp(to_char(vDECur,'dd.mm.yyyy'),'dd.mm.yyyy');curen := 0;for (select x(x.[F__E_NET_B]*10:enet)in ::[GA_MAP_STAT] allwhere x.[qtime] >= vTSCurand x.[qtime] < (vTSCur + numtodsinterval(1,'day'))and x.[F_TIMESTAMP] >= vDECurand x.[F_TIMESTAMP] < (vDECur+1)order by x.[qtime] desc) loopcuren := x.enet;exit;end loop;if curen = 0 and prven != 0 thencuren := prven;end if;if prven is null thenprven := curen;elseif prven = 0 thenprven := curen;end if;curts := TsToUTs(vTsCur - numtodsinterval(num_tz,'hour'));&debug('1. dcur = '||to_char(vDECur,'dd.mm.yyyy')||' prven = '||prven||' curen ='||curen||' diff='||to_char(curen - prven))AddTrgData(CEFRN,curts,curen - prven);prven := curen;end if;vDECur := vDECur + 1;end loop;end if;end;end if;-- Выгрузить собранные массивы  ответvTmpId := tflag.first;while vTmpId is not null looptflag(vTmpId).fOb.put('datapoints',tflag(vTmpId).fAr);tflag(vTmpId).fAr := null;pOut.append(tflag(vTmpId).fOb);tflag(vTmpId).fOb := null;vTmpId := tflag.next(vTmpId);end loop;end;procedure get_search(pInp in out nocopy JSON_OBJECT_T, pOut in out nocopy JSON_ARRAY_T) isvTarget varchar2(100);begin&debug('pInp='||pInp.to_string())vTarget := trim(pInp.get_String('target'));if vTarget is null thenpOut.Append(CPALL);pOut.Append(CPNET);pOut.Append(CPACB);pOut.Append(CPI2C);pOut.Append(CPADD);pOut.Append(CPMP1);pOut.Append(CPMP2);pOut.Append(CPMP3);pOut.Append(CEDAY);pOut.Append(CEMP1);pOut.Append(CEMP2);pOut.Append(CEMP3);pOut.Append(CETOB);pOut.Append(CEFRB);pOut.Append(CEFRN);pOut.Append(CUNET);pOut.Append(CUOUT);pOut.Append(CUACB);end if;end;public procedure GetJson(pPathInfo in varchar2, pInpPost in blob, pOutPost out blob) isvInp JSON_OBJECT_T;vOut JSON_ARRAY_T;beginvInp := JSON_OBJECT_T(pInpPost);vOut := JSON_ARRAY_T();&debug('pPathInfo='||pPathInfo)-- Маршрутизация запроса в зависимости от pPathInfoif pPathInfo = '/search' thenget_search(vInp, vOut);elsif pPathInfo = '/query' thenget_query(vInp, vOut);end if;pOutPost := vOut.to_Blob;end;

Возможно это кому-то окажется полезным :-)

Вот такие результаты:

Подробнее..

Что такое JSON

02.05.2021 16:16:26 | Автор: admin

Если вы тестируете API, то должны знать про два основных формата передачи данных:

  • XML используется в SOAP(всегда)и REST-запросах(реже);

  • JSON используется в REST-запросах.

Сегодня я расскажу вам про JSON.

JSON (англ. JavaScript Object Notation) текстовый формат обмена данными, основанный на JavaScript. Но при этом формат независим от JS и может использоваться в любом языке программирования.

JSON используется в REST API. По крайней мере, тестировщик скорее всего столкнется с ним именно там.

См также:

Что такое API общее знакомство с API

Что такое XML второй популярный формат

Введение в SOAP и REST: что это и с чем едят видео про разницу между SOAP и REST

В SOAP API возможен только формат XML, а вот REST API поддерживает как XML, так и JSON. Разработчики предпочитают JSON он легче читается человеком и меньше весит. Так что давайте разберемся, как он выглядит, как его читать, и как ломать!

Содержание

Как устроен JSON

В качестве значений в JSON могут быть использованы:

  • JSON-объект

  • Массив

  • Число (целое или вещественное)

  • Литералы true (логическое значение истина), false (логическое значение ложь) и null

  • Строка

Я думаю, с простыми значениями вопросов не возникнет, поэтому разберем массивы и объекты. Ведь если говорить про REST API, то обычно вы будете отправлять / получать именно json-объекты.

JSON-объект

Как устроен

Возьмем пример из документации подсказок Дадаты по ФИО:

{  "query": "Виктор Иван",  "count": 7}

И разберемся, что означает эта запись.

Объект заключен в фигурные скобки {}

JSON-объект это неупорядоченное множество пар ключ:значение.

Ключ это название параметра, который мы передаем серверу. Он служит маркером для принимающей запрос системы: смотри, здесь у меня значение такого-то параметра!. А иначе как система поймет, где что? Ей нужна подсказка!

Вот, например, Виктор Иван это что? Ищем описание параметра query в документации ага, да это же запрос для подсказок!

Это как если бы мы вбили строку Виктор Иван в GUI (графическом интерфейсе пользователя):

Когда пользователь начинает вводить данные в формочку, то сразу видит результат появляется список подсказок. Это значит, что разработчик прописал в коде условие делать некое действие на каждый ввод символа в это поле. Какое действие? Можно увидеть через f12.

Открываем вкладку Network, вбиваем Виктор Иван и находим запрос, который при этом уходит на сервер. Ого, да это тот самый пример, что мы разбираем!

Клиент передает серверу запрос в JSON-формате. Внутри два параметра, две пары ключ-значение:

  • query строка, по которой ищем (то, что пользователь вбил в GUI);

  • count количество подсказок в ответе (в Дадате этот параметр зашит в форму, всегда возвращается 7 подсказок. Но если дергать подсказки напрямую, значение можно менять!)

Пары ключ-значение разделены запятыми:

Строки берем в кавычки, числа нет:

Конечно, внутри может быть не только строка или число. Это может быть и другой объект! Или массив... Или объект в массиве, массив в объекте... Любое количество уровней вложенности =))

Объект, массив, число, булево значение (true / false) если у нас НЕ строка, кавычки не нужны. Но в любом случае это будет значение какого-то ключа:

НЕТ

ДА

{

"a": 1,

{ x:1, y:2 }

}

{

"a": 1,

"inner_object": { "x":1, "y":2 }

}

{

"a": 1,

[2, 3, 4]

}

{

"a": 1,

"inner_array": [2, 3, 4]

}

Переносы строк делать необязательно. Вообще пробелы и переносы строк нужны только человеку для читабельности, система поймет и без них:

Так правильно

Так тоже правильно

{

"query": "Виктор Иван",

"count": 7

}

{ "query":"Виктор Иван", "count":7}

Ключ ВСЕГДА строка, поэтому можно не брать его в кавычки.

Так правильно

Так тоже правильно

{

"query": "Виктор Иван",

"count": 7

}

{

query: "Виктор Иван",

count: 7

}

По крайней мере, если вы работаете с простыми значениями ключей, а несколько слов записываете в верблюжьемРегистре или в змеином_регистре. Если вы хотите написать в ключе несколько слов через пробел, ключ нужно взять в кавычки.

НЕТ

ДА

{

my query: "Виктор Иван"

}

{

"my query": "Виктор Иван"

}

И все же я рекомендую использовать простые названия ключей, или использовать snake_case.

См также:

CamelCase, snake_case и другие регистры подробнее о разных регистрах

Писать ключи можно в любом порядке. Ведь JSON-объект это неупорядоченное множество пар ключ:значение.

Так правильно

Так тоже правильно

{

query: "Виктор Иван",

count: 7

}

{

count: 7,

query: "Виктор Иван"

}

Очень важно это понимать, и тестировать! Принимающая запрос система должна ориентировать на название ключей в запросе, а не на порядок их следования. Ключевое слово должна )) Хотя знаю примеры, когда от перестановки ключей местами всё ломалось, ведь первым должен идти запрос, а не count!.

Ключ или свойство?

Вот у нас есть JSON-объект:

{  "query": "Виктор Иван",  "count": 7}

Что такое query? Если я хочу к нему обратиться, как мне это сказать? Есть 2 варианта, и оба правильные:

Обратиться к свойству объекта;

Получить значение по ключу.

То есть query можно назвать как ключом, так и свойством. А как правильно то?

Правильно и так, и так! Просто есть разные определения объекта:

Объект

В JS объект это именно объект. У которого есть набор свойств и методов:

  • Свойства описывают, ЧТО мы создаем.

  • Методы что объект умеет ДЕЛАТЬ.

То есть если мы хотим создать машину, есть два пути:

  1. Перечислить 10 разных переменных модель, номер, цвет, пробег...

  2. Создать один объект, где будут все эти свойства.

Аналогично с кошечкой, собачкой, другом из записной книжки...

Объектно-ориентированное программирование (ООП) предлагает мыслить не набором переменных, а объектом. Хотя бы потому, что это логичнее. Переменных в коде будет много, как понять, какие из них взаимосвязаны?

Вот если я создаю машину, сколько переменных мне надо заполнить? А если меняю данные? А если удаляю? Когда переменные разбросаны по коду, можно забыть про какую-то и получить ошибку в интерфейсе. А если у нас есть цельный объект, всегда можно посмотреть, какие у него есть свойства и методы.

Например, создадим кошечку:

var cat = {name: Pussy,year: 1,sleep: function() {// sleeping code}}

В объекте cat есть:

  • Свойства name, year (что это за кошечка)

  • Функции sleep (что она умеет делать, описание поведения)

По коду сразу видно, что у кошечки есть имя и возраст, она умеет спать. Если разработчик решит добавить новые свойства или методы, он дополнит этот объект, и снова всё в одном месте.

Если потом нужно будет получить информацию по кошечке, разработчик сделает REST-метод getByID, searchKitty, или какой-то другой. А в нем будет возвращать свойства объекта.

То есть метод вернет

{name: Pussy,year: 1,}

И при использовании имени вполне уместно говорить обратиться к свойству объекта. Это ведь объект (кошечка), и его свойства!

Набор пар ключ:значение

Второе определение объекта неупорядоченное множество пар ключ:значение, заключенное в фигурные скобки {}.

Оно применимо тогда, когда внутри фигурных скобок приходит не конкретный целостный объект, а просто набор полей. Они могут быть связаны между собой, а могут относится к совершенно разным объектам внутри кода:

  • client_fio (в коде это свойство fio объекта client)

  • kitty_name (в коде это свойство name объекта cat)

  • car_model (в коде это свойство model объекта car)

В таком случае логично называть эти параметры именно ключами мы хотим получить значение по ключу.

Но в любом случае, и ключ, и свойство будет правильно. Не пугайтесь, если в одной книге / статье / видео увидели одно, в другой другое... Это просто разные трактовки \_()_/

Итого

Json-объект это неупорядоченное множество пар ключ:значение, заключённое в фигурные скобки { }. Ключ описывается строкой, между ним и значением стоит символ :. Пары ключ-значение отделяются друг от друга запятыми.

Значения ключа могут быть любыми:

  • число

  • строка

  • массив

  • другой объект

  • ...

И только строку мы берем в кавычки!

JSON-массив

Как устроен

Давайте снова начнем с примера. Это массив:

["MALE","FEMALE"]

Массив заключен в квадратные скобки []

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

Значения разделены запятыми:

Значения внутри

Внутри массива может быть все, что угодно:

Цифры

[1, 5, 10, 33]

Строки

["MALE","FEMALE"]

Смесь

[1, "Андрюшка", 10, 33]

Объекты

Да, а почему бы и нет:

[1, {a:1, b:2}, "такой вот массивчик"]

Или даже что-то более сложное. Вот пример ответа подсказок из Дадаты:

[        {            "value": "Иванов Виктор",            "unrestricted_value": "Иванов Виктор",            "data": {                "surname": "Иванов",                "name": "Виктор",                "patronymic": null,                "gender": "MALE"            }        },        {            "value": "Иванченко Виктор",            "unrestricted_value": "Иванченко Виктор",            "data": {                "surname": "Иванченко",                "name": "Виктор",                "patronymic": null,                "gender": "MALE"            }        },        {            "value": "Виктор Иванович",            "unrestricted_value": "Виктор Иванович",            "data": {                "surname": null,                "name": "Виктор",                "patronymic": "Иванович",                "gender": "MALE"            }        }]

Система возвращает массив подсказок. Сколько запросили в параметре count, столько и получили. Каждая подсказка объект, внутри которого еще один объект. И это далеко не сама сложная структура! Уровней вложенности может быть сколько угодно массив в массиве, который внутри объекта, который внутри массива, который внутри объекта...

Ну и, конечно, можно и наоборот, передать массив в объекте. Вот пример запроса в подсказки:

{"query": "Виктор Иван","count": 7,"parts": ["NAME", "SURNAME"]}

Это объект (так как в фигурных скобках и внутри набор пар ключ:значение). А значение ключа "parts" это массив элементов!

Итого

Массив это просто набор значений, разделенных запятыми. Находится внутри квадратных скобок [].

А вот внутри него может быть все, что угодно:

  • числа

  • строки

  • другие массивы

  • объекты

  • смесь из всего вышеназванного

JSON vs XML

В SOAP можно применять только XML, там без вариантов.

В REST можно применять как XML, так и JSON. Разработчики отдают предпочтение json-формату, потому что он проще воспринимается и меньше весит. В XML есть лишняя обвязка, название полей повторяется дважды (открывающий и закрывающий тег).

Сравните один и тот же запрос на обновление данных в карточке пользователя:

XML

<req><surname>Иванов</surname><name>Иван</name><patronymic>Иванович</patronymic><birthdate>01.01.1990</birthdate><birthplace>Москва</birthplace><phone>8 926 766 48 48</phone></req>

JSON

{"surname": "Иванов","name": "Иван","patronymic": "Иванович","birthdate": "01.01.1990","birthplace": "Москва","phone": "8 926 766 48 48"}

За счет того, что мы не дублируем название поля каждый раз surname surname, читать JSON проще. И за счет этого же запрос меньше весит, что при плохом интернете бывает важно. Или при большой нагрузке.

См также:

Инфографика REST vs SOAP

Well Formed JSON

Разработчик сам решает, какой JSON будет считаться правильным, а какой нет. Но есть общие правила, которые нельзя нарушать. Наш JSON должен быть well formed, то есть синтаксически корректный.

Чтобы проверить JSON на синтаксис, можно использовать любой JSON Validator (так и гуглите). Я рекомендую сайт w3schools. Там есть сам валидатор + описание типичных ошибок с примерами.

Но учтите, что парсеры внутри кода работают не по википедии или w3schools, а по RFC, стандарту. Так что если хотите изучить каким должен быть JSON, то правильнее открывать RFC и искать там JSON Grammar. Однако простому тестировщику хватит набора типовых правил с w3schools, их и разберем.

Правила well formed JSON:

  1. Данные написаны в виде пар ключ:значение

  2. Данные разделены запятыми

  3. Объект находится внутри фигурных скобок {}

  4. Массив внутри квадратных []

1. Данные написаны в виде пар ключ:значение

Например, так:

"name":"Ольга"

В JSON название ключа нужно брать в кавычки, в JavaScript не обязательно он и так знает, что это строка. Если мы тестируем API, то там будет именно JSON, так что кавычки обычно нужны.

Но учтите, что это правило касается JSON-объекта. Потому что json может быть и числом, и строкой. То есть:

123

Или

"Ольга"

Это тоже корректный json, хоть и не в виде пар ключ:значение.

И вот если у вас по ТЗ именно json-объект на входе, попробуйте его сломать, не передав ключ. Ещё можно не передать значение, но это не совсем негативный тест система может воспринимать это нормально, как пустой ввод.

2. Данные разделены запятыми

Пары ключ:значение в объекте разделяются запятыми. После последней пары запятая не нужна!

Типичная ошибка: поставили запятую в конце объекта:

{  "query": "Виктор Иван",  "count": 7,}

Это последствия копипасты. Взяли пример из документации, подставили в постман (ну или разработчик API подставил в код, который будет вызывать систему), а потом решили поменять поля местами.

В итоге было так:

{  "count": 7,  "query": "Виктор Иван"}

Смотрим на запрос ну, query то важнее чем count, надо поменять их местами! Копипастим всю строку "count": 7,, вставляем ниже. Перед ней запятую добавляем, а лишнюю убрать забываем. По крайней мере у меня это частая ошибка, когда я кручу-верчу, местами поменять хочу.

Другой пример когда мы добавляем в запрос новое поле. Примерный сценарий:

  1. У меня уже есть работающий запрос в Postman-е. Но в нем минимум полей.

  2. Я его клонирую

  3. Копирую из документации нужное мне поле. Оно в примере не последнее, так что идёт с запятой на конце.

  4. Вставляю себе в конце запроса в текущий конец добавляю запятую, потом вставляю новую строку.

  5. Отправляю запрос ой, ошибка! Из копипасты то запятую не убрала!

Я на этот сценарий постоянно напарываюсь при тестировании перестановки полей. А ведь это нужно проверять! Хороший запрос должен быть как в математической присказке: от перемены мест слагаемых сумма не меняется.

Не зря же определение json-объекта гласит, что это неупорядоченное множество пар ключ:значение. Раз неупорядоченное я могу передавать ключи в любом порядке. И сервер должен искать по запросу название ключа, а не обращаться к индексу элемента.

Разработчик, который будет взаимодействовать с API, тоже человек, который может ошибиться. И если система будет выдавать невразумительное сообщение об ошибке, можно долго думать, где конкретно ты налажал. Поэтому ошибки тоже тестируем.

Чтобы протестировать, как система обрабатывает плохой json, замените запятую на точку с запятой:

{  "count": 7;  "query": "Виктор Иван"}

Или добавьте лишнюю запятую в конце запроса эта ошибка будет встречаться чаще!

{  "count": 7,  "query": "Виктор Иван",}

Или пропустите запятую там, где она нужна:

{"count": 7"query": "Виктор Иван"}

Аналогично с массивом. Данные внутри разделяются через запятую. Хотите попробовать сломать? Замените запятую на точку с запятой! Тогда система будет считать, что у вас не 5 значений, а 1 большое:

[1, 2, 3, 4, 5] <!-- корректный массив на 5 элементов -->[1; 2; 3; 4; 5] <!-- некорректный массив, так как такого разделителя быть не должно. Это может быть простой строкой, но тогда нужны кавычки -->!

3. Объект находится внутри фигурных скобок {}

Это объект:

{a: 1, b: 2}

Чтобы сломать это условие, уберите одну фигурную скобку:

{a: 1, b: 2
a: 1, b: 2}

Или попробуйте передать объект как массив:

[ a: 1, b: 2 ]

Ведь если система ждет от вас в запросе объект, то она будет искать фигурные скобки.

4. Массив внутри квадратных []

Это массив:

[1, 2]

Чтобы сломать это условие, уберите одну квадратную скобку:

[1, 2
1, 2]

Или попробуйте передать массив как объект, в фигурных скобках:

{ 1, 2 }

Ведь если система ждет от вас в запросе массив, то она будет искать квадратные скобки.

Итого

JSON (JavaScript Object Notation) текстовый формат обмена данными, основанный на JavaScript. Легко читается человеком и машиной. Часто используется в REST API (чаще, чем XML).

  • JSON-объект неупорядоченное множество пар ключ:значение, заключённое в фигурные скобки { }.

  • Массив упорядоченный набор значений, разделенных запятыми. Находится внутри квадратных скобок [].

  • Число (целое или вещественное).

  • Литералы true (логическое значение истина), false (логическое значение ложь) и null.

  • Строка

При тестировании REST API чаще всего мы будем работать именно с объектами, что в запросе, что в ответе. Массивы тоже будут, но обычно внутри объектов.

Правила well formed JSON:

  1. Данные в объекте написаны в виде пар ключ:значение

  2. Данные в объекте или массиве разделены запятыми

  3. Объект находится внутри фигурных скобок {}

  4. Массив внутри квадратных []

См также:

Introducing JSON

RFC (стандарт)

Что такое XML

PS больше полезных статей ищитев моем блоге по метке полезное. А полезные видео намоем youtube-канале

Подробнее..

Перевод Использование JSON в Kibana поиске

24.04.2021 02:16:28 | Автор: admin

При поиске из Kibana вы обычно вводите фактическую строку запроса в верхнюю панель, как мы видели это в уроке. Если строки запроса недостаточно для того, что вам нужно, у вас также есть возможность написать JSON в этой строке.

Вы можете записать JSON-объект, который вы бы прикрепили к ключу "query (запрос)" при взаимодействии с Elasticsearch в этом поле, например:

{ "range": { "numeric": { "gte": 10 } } }

Это было бы эквивалентно записи numeric:>=10 в это поле. Чаще всего это имеет смысл только в том случае, если вам нужен доступ к опциям, которые доступны только в JSON-запросе, но не в строке запроса.

Предупреждение: если вы впишете JSON query_string в это поле (например, потому что хотите иметь доступ к lowercase_expanded_terms), Kibana сохранит правильный JSON для запроса, но снова покажет вам (после нажатия клавиши enter) только часть запроса вашего JSON. Это может быть очень запутанным и, конечно, если Вы сейчас введете текст и нажмете enter еще раз, он также потеряет параметры, которые Вы установили через JSON, так что это действительно должно быть использовано с осторожностью.

Особые случаи

В этом разделе должны быть затронуты еще несколько особых случаев, о которых можно подумать: "Я прочитал весь учебник, я все понял, но все равно мой запрос не находит тех данных, которые я ожидал от него получить".

Elasticseach не находит термины в длинных полях.

Это, по моему опыту, довольно распространенная проблема, и ее непросто решить, если вы не знаете, что ищете.

Elasticsearch имеет параметр ignore_above, который вы можете установить в отображении для каждого поля. Это числовое значение, которое приведет к тому, что Elasticsearch НЕ будет индексировать значения дольше, чем задано значением ignore_above, когда будет вставлен документ. Значение все равно будет сохранено, поэтому при просмотре документа вы его увидите, но не сможете найти.

Как проверить, установлено ли это значение в поле? Вам нужно получить отображение из Elasticsearch, вызвав <your-elasticsearch-domain>/<your-index-name>/_mapping. В возвращаемом JSON где-то будет отображение для искомого поля, которое может выглядеть следующим образом:

"fieldName": {  "type": "string",  "ignore_above": 15}

В этом случае значения выше 15 символов не индексируются, и их поиск невозможен.

Пример: Используя вышеприведенное отображение, давайте вставим два документа в этот Elasticsearch:

{ "fieldName": "short string" }{ "fieldName": "a string longer as ignore_above" }

Если вы теперь перечислите все документы (в Kibana или Elasticsearch), то увидите, что оба документа находятся там и значение обоих полей - это то, что вы вставили в строку. Но если вы теперь будете искать fieldName:longer, вы не получите никаких результатов (в то время как fieldName:short вернет первый документ). Elasticsearch обнаружил, что значение "строка длиннее чем ignore_above" длиннее 15 символов, и поэтому оно сохраняет его только в документе, но не индексирует его, поэтому вы не сможете искать в нем ничего, так как в инвертированном индексе для этого поля не будет содержимого этого значения.

Поиск требует определенного поля, без которого он не работает.

Если вы можете выполнить поиск, например, для author:foo, но не для foo, то, скорее всего, это "проблема" с вашим default_field. Elasticsearch предваряет поле по умолчанию перед foo. Это поле можно настроить так, чтобы оно отличалось от _all.

Возможно, настройка поля index.query.default_field была установлена на что-то другое, и Elasticsearch не использует поле _all, что может привести к проблеме.

Также возможно, что поле _all ведет себя не так, как вы ожидали, потому что оно было настроено каким-то другим образом. Вы можете исключить конкретные поля из поля _all (например, в приведенном выше примере fieldName могло быть исключено из индексации в поле _all) или были изменены опции анализа/индексации в отображении поля _all.


Уже сейчас в OTUS открыт набор на новый поток курса "DevOps практики и инструменты". Перевод данного фрагмента статьи был подготовлен в рамках набора на курс.

Также приглашаем всех желающих посетить бесплатный вебинар, на котором эксперты OTUS расскажут о ситуации на рынке DevOps и карьерных перспективах.

ЗАПИСАТЬСЯ НА ВЕБИНАР

Подробнее..
Категории: Devops , Json , Kibana , Блог компании otus

Кастомная (де) сериализация даты и времени в Spring

24.02.2021 18:11:35 | Автор: admin

Представьте довольно распространённую ситуацию: ваше приложение взаимодействует с клиентами, которые находятся в разных часовых поясах. Вам часто приходится работать с датами, а для корректной работы системы, они передаются с указанием часового пояса отправителя. При этом, вам нужно:

  1. При получении запроса привести дату к серверному времени и работать с ней, а также сохранять в базу данных в таком виде

  2. В ответ возвращать дату и время с указанием серверного часового пояса

Для решения этой задачи Spring предоставляет удобный механизм для написания кастомной сериализации и десериализации. Главным его преимуществом является возможность вынести преобразования дат (и других типов данных) в отдельный конфигурационный класс и не вызывать методы преобразования каждый раз в исходном коде.

Десериализация

Для того, чтобы Spring понимал, что именно наш класс нужно использовать для (де)сериализации, его необходимо пометить аннотацией @JsonComponent

Ну а чтобы код был максимально кратким, я буду использовать внутренний статический класс, который должен быть унаследован от JsonDeserializerи параметризован нужным нам типом данных. Поскольку JsonDeserializerэто абстрактный класс, нам необходимо переопределить его абстрактный метод deserialize()

@JsonComponentpublic class CustomDateSerializer {        public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {            @Override        public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {            return null;        }    }}

Из параметров метода получаем переданную клиентом строку, проверяем её на null и получаем из неё объект класса ZonedDateTime

public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {    String date = jsonParser.getText();    if (date.isEmpty() || isNull(date) {        return null;    }    ZonedDateTime userDateTime = ZonedDateTime.parse(date);}

Для получения разницы во времени, у переменной userDateTime нужно вызвать метод withZoneSameInstant() и передать в него текущую серверную таймзону. Нам остаётся лишь преобразовать полученную дату кLocalDateTime

Для надежности, предположим, что нам может прийти дата без таймзоны, либо вообще некорректные данные. Для этого случая, добавим обработку исключений. Полностью десериализатор будет выглядеть так

public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {    @Override    public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {        String date = jsonParser.getText();        if (date.isEmpty()) {            return null;        }        try {            ZonedDateTime userDateTime = ZonedDateTime.parse(date);            ZonedDateTime serverTime = userDateTime.withZoneSameInstant(ZoneId.systemDefault());            return serverTime.toLocalDateTime();        } catch (DateTimeParseException e) {            try {                return LocalDateTime.parse(date);            } catch (DateTimeParseException ex) {                throw new IllegalArgumentException("Error while parsing date", ex);            }        }    }}

Предположим, что серверное времяUTC+03. Таким образом, когда клиент передаёт дату 2021-01-21T22:00:00+07:00, в нашем контроллере мы уже можем работать с серверным временем

public class Subscription {    private LocalDateTime startDate;      // standart getters and setters}
@RestController public class TestController {    @PostMapping  public void process(@RequestBody Subscription subscription) {    // к этому моменту поле startDate объекта subscription будет равно 2021-01-21T18:00  }}

Сериализация

С сериализацией алгоритм действий похожий. Нам нужно унаследовать класс от JsonSerializer, параметризовать его и переопределить абстрактный метод serialize()

Внутри метода мы проверим нашу дату на null, добавим к ней серверную таймзону и получим из неё строку. Остаётся лишь отдать её клиенту

public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {    @Override    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {        if (isNull(localDateTime)) {            return;        }        OffsetDateTime timeUtc = localDateTime.atOffset(ZoneOffset.systemDefault().getRules().getOffset(LocalDateTime.now()));        jsonGenerator.writeString(timeUtc.toString());    }}

Круто? Можно в прод? Не совсем. В целом, этот код будет работать, но могут начаться проблемы, если серверная таймзона будет равна UTC+00. Дело в том, что конкретно для этого часового пояса id таймзоны отличается от стандартного формата. Посмотрим в документацию класса ZoneOffset

Таким образом, имея серверную таймзону UTC+03, на выходе мы получим строку следующего вида: 2021-02-21T18:00+03:00.Но если же оно UTC+00, то получим 2021-02-21T18:00Z

Поскольку мы работаем со строкой, нам не составит труда немного изменить код, дабы на выходе мы всегда получали дату в одном формате. Объявим две константы одна из них будет равна дефолтному id UTC+00, а вторая которую мы хотим отдавать клиенту, и добавим проверку - если серверное время находится в нулевой таймзоне, то заменим Z на +00:00. В итоге наш сериализотор будет выглядеть следующим образом

public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {    private static final String UTC_0_OFFSET_ID = "Z";    private static final String UTC_0_TIMEZONE = "+00:00";    @Override    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {        if (!isNull(localDateTime)) {            String date;            OffsetDateTime timeUtc = localDateTime.atOffset(ZoneOffset.systemDefault().getRules().getOffset(LocalDateTime.now()));            if (UTC_0_OFFSET_ID.equals(timeUtc.getOffset().getId())) {                date = timeUtc.toString().replace(UTC_0_OFFSET_ID, UTC_0_TIMEZONE);            } else {                date = timeUtc.toString();            }            jsonGenerator.writeString(date);        }    }}

Итого

Благодаря встроенным механизмам спринга мы получили возможность автоматически конвертировать дату и время в требуемом формате, без каких-либо явных вызовов методов в коде

Полностью исходный код можно посмотреть здесь

Подробнее..
Категории: Java , Spring , Json , Maven , Spring-boot , Serialization , Date , Deserialization

Что нам стоит загрузить JSON в Data Platform

16.06.2021 16:13:28 | Автор: admin

Всем привет!

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

То есть, если возвращаться к финальной картинке из упомянутой выше статьи (специально дублирую ее, чтобы уважаемым читателям было удобнее), то сегодня мы будем более углубленно говорить о реализации правой части схемы той, что лежит после Apache NiFi.

Схема из прошлой нашей статьи.Схема из прошлой нашей статьи.

Напомним, что в нашей компании более 350 реляционных баз данных. Естественно, не все они уникальны и многие представляют собой по сути разные экземпляры одной и той же системы, установленной во всех магазинах торговой сети, но все же зоопарк разнообразия присутствует. Поэтому без какого-либо Frameworkа, упрощающего и ускоряющего интеграцию источников в Data Platform, не обойтись.

Общая схема доставки данных из источников в ODS-слой Greenplum посредством разработанного нами frameworkа приведена ниже:

Общая схема доставки данных в ODS-слой Greenplum Общая схема доставки данных в ODS-слой Greenplum
  1. Данные из систем-источников пишутся в Kafka в AVRO-формате, обрабатываются в режиме реального времени Apache NiFi, который сохраняет их в формате parquet на S3.

  2. Затем эти файлы с сырыми данными с помощью Sparkа обрабатываются в два этапа:

    1. Compaction на данном этапе выполняется объединение для снижения количества выходных файлов с целью оптимизации записи и последующего чтения (то есть несколько более мелких файлов объединяются в несколько файлов побольше), а также производится дедубликация данных: простой distinct() и затем coalesce(). Результат сохраняется на S3. Эти файлы используются затем для parsing'а , а также являются своеобразным архивом сырых необработанных данных в формате как есть;

    2. Parsing на этой фазе производится разбор входных данных и сохранение их в плоские структуры согласно маппингу, описанному в метаданных. В общем случае из одного входного файла можно получить на выходе несколько плоских структур, которые в виде сжатых (как правило gzip) CSV-файлов сохраняются на S3.

  3. Заключительный этап загрузка данных CSV-файлов в ODS-слой хранилища: создается временная external table над данными в S3 через PXF S3 connector, после чего данные уже простым pgsql переливаются в таблицы ODS-слоя Greenplum

  4. Все это оркестрируется с помощью Airflow.

DAGи для Airflow у нас генерируются динамически на основании метаданных. Parsing файлов и разложение их в плоские структуры также производится с использованием метаданных. Это приводит к упрощению интеграции нового источника, так как, для этого необходимо всего лишь:

  • создать в ODS-слое Хранилища таблицы-приемники данных;

  • в репозитории метаданных в Git согласно принятым стандартам прописать в виде отдельных YAML-файлов:

    • общие настройки источника (такие как: расписание загрузки, входной и выходной формат файлов с данными, S3-бакет, имя сервисного пользователя, имя и email владельца для нотификации в случае сбоя загрузки и т.п.);

    • маппинг данных объектов источника на таблицы слоя ODS (при этом поддерживаются как плоские, так и вложенные структуры и массивы, а также есть возможность данные из одного объекта раскладывать в ODS-слое по нескольким таблицам). То есть описать, как необходимо сложную вложенную структуру разложить в плоские таблицы;

До недавнего времени такой подход удовлетворял текущие наши потребности, но количество и разнообразие источников данных растет. У нас стали появляться источники, которые не являются реляционными базами данных, а генерируют данные в виде потока JSON-объектов. Кроме того на горизонте уже маячила интеграция источника, который под собой имел MongoDB и поэтому будет использовать MongoDB Kafka source connector для записи данных в Kafka. Поэтому остро встала необходимость доработки нашего frameworkа для поддержки такого сценария. Хотелось, чтобы данные источника сразу попадали на S3 в формате JSON - то есть в формате "как есть", без лишнего шага конвертации в parquet посредством Apache NiFi.

В первую очередь необходимо было доработать шаг Compaction. Его код, если убрать всю обвязку и выделить только главное, очень простой:

df = spark.read.format(in_format) \               .options(**in_options) \               .load(path) \               .distinct()    new_df = df.coalesce(div)new_df.write.mode("overwrite") \             .format(out_format) \            .options(**out_options) \            .save(path)

Но если мы проделаем все те же манипуляции с JSON-данными, то волей-неволей внесем изменения во входные данные, так как при чтении JSONов Spark автоматом определит и сделает mergeSchema, т.е. мы тем самым можем исказить входные данные, чего не хотелось бы. Ведь наша задача на этом шаге только укрупнить файлы и дедублицировать данные, без какого-либо вмешательства в их структуру и наполнение. То есть сохранить их как есть.

По-хорошему, нам надо было просто прочитать данные как обычный текст, дедублицировать строки, укрупнить файлы и положить обратно на S3. Для этого был предложен достаточно изящный способ:

рассматривать файлы с JSON-объектами как DataFrame с одной колонкой, содержащей весь JSON-объект.

Попробуем сделать это. Допустим, мы имеем следующий файл данных:

file1:

{productId: 1, productName: ProductName 1, tags: [tag 1, tag 2], dimensions: {length: 10, width: 12, height: 12.5}}{productId: 2, price: 10.01, tags: [tag 1, tag 2], dimensions: {length: 10, width: 12, height: 12.5}}

Обратите внимание на формат этого файла. Это файл с JSON-объектами, где 1 строка = 1 объект. Оставаясь, по сути, JSON-ом, он при этом не пройдет синтаксическую JSON-валидацию. Именно в таком виде мы сохраняем JSON-данные на S3 (есть специальная "галочка в процессоре Apache NiFi).

Прочитаем файл предлагаемым способом:

# Читаем данныеdf = spark.read \          .format("csv") \          .option("sep", "\a") \          .load("file1.json")# Схема получившегося DataFramedf.printSchema()root |-- _c0: string (nullable = true)# Сами данныеdf.show()+--------------------+|                 _c0|+--------------------+|{"productId": 1, ...||{"productId": 2, ...|+--------------------+

То есть мы тут читаем JSON как обычный CSV, указывая разделитель, который никогда заведомо не встретится в наших данных. Например, Bell character. В итоге мы получим DataFrame из одного поля, к которому можно будет также применить dicstinct() и затем coalesce(), то есть менять существующий код не потребуется. Нам остается только определить опции в зависимости от формата:

# Для parquetin_format = "parquet"in_options = {}# Для JSONin_format = "csv"in_options = {"sep": "\a"}

Ну и при сохранении этого же DataFrame обратно на S3 в зависимости от формата данных опять применяем разные опции:

df.write.mode("overwrite") \           .format(out_format) \.options(**out_options) \  .save(path)  # для JSON     out_format = "text" out_options = {"compression": "gzip"}  # для parquet   out_format = input_format out_options = {"compression": "snappy"}

Следующей точкой доработки был шаг Parsing. В принципе, ничего сложного, если бы задача при этом упиралась в одну маленькую деталь: JSON -файл, в отличии от parquet, не содержит в себе схему данных. Для разовой загрузки это не является проблемой, так как при чтении JSON-файла Spark умеет сам определять схему, и даже в случае, если файл содержит несколько JSON-объектов с немного отличающимся набором полей, корректно выполнит mergeSchema. Но для регулярного процесса мы не могли уповать на это. Банально может случиться так, что во всех записях какого-то файла с данными может не оказаться некоего поля field_1, так как, например, в источнике оно заполняется не во всех случаях. Тогда в получившемся Spark DataFrame вообще не окажется этого поля, и наш Parsing, построенный на метаданных, просто-напросто упадет с ошибкой из-за того, что не найдет прописанное в маппинге поле.

Проиллюстрирую. Допустим,у нас есть два файла из одного источника со следующим наполнением:

file1 (тот же что и в примере выше):

{productId: 1, productName: ProductName 1, tags: [tag 1, tag 2], dimensions: {length: 10, width: 12, height: 12.5}}{productId: 2, price: 10.01, tags: [tag 1, tag 2], dimensions: {length: 10, width: 12, height: 12.5}}

file2:

{productId: 3, productName: ProductName 3, dimensions: {length: 10, width: 12, height: 12.5, package: [10, 20.5, 30]}}

Теперь прочитаем Sparkом их и посмотрим данные и схемы получившихся DataFrame:

df = spark.read \          .format("json") \          .option("multiline", "false") \          .load(path)df.printSchema()df.show()

Первый файл (схема и данные):

root |-- dimensions: struct (nullable = true) |    |-- height: double (nullable = true) |    |-- length: long (nullable = true) |    |-- width: long (nullable = true) |-- price: double (nullable = true) |-- productId: long (nullable = true) |-- productName: string (nullable = true) |-- tags: array (nullable = true) |    |-- element: string (containsNull = true)+--------------+-----+---------+-------------+--------------+|    dimensions|price|productId|  productName|          tags|+--------------+-----+---------+-------------+--------------+|[12.5, 10, 12]| null|        1|ProductName 1|[tag 1, tag 2]||[12.5, 10, 12]|10.01|        2|         null|[tag 1, tag 2]|+--------------+-----+---------+-------------+--------------+

Второй файл (схема и данные):

root |-- dimensions: struct (nullable = true) |    |-- height: double (nullable = true) |    |-- length: long (nullable = true) |    |-- package: array (nullable = true) |    |    |-- element: double (containsNull = true) |    |-- width: long (nullable = true) |-- productId: long (nullable = true) |-- productName: string (nullable = true)+--------------------+---------+-------------+|          dimensions|productId|  productName|+--------------------+---------+-------------+|[12.5, 10, [10.0,...|        3|ProductName 3|+--------------------+---------+-------------+

Как видно, Spark корректно выстроил схему отдельно для каждого файла. Если в какой-либо записи не было обнаружено поля, имеющегося в другой, то в DataFrame мы видим корректное проставление null (поля price и productName для первого файла).

Но в целом схемы получились разные, и если у нас в маппинге прописано, что нам нужно распарсить эти данные (то есть оба файла) в следующую плоскую структуру,

root |-- price: double (nullable = true) |-- productId: long (nullable = true) |-- productName: string (nullable = true)

а во входных данных у нас присутствуют только файлы а-ля file2, где поля price нет ни у одной записи, то Spark упадет с ошибкой, так как не найдет поля price для формирования выходного DataFrame. С parquet-файлами такой проблемы как правило не возникает, так как сам parquet-файл генерируется из AVRO, который уже содержит полную схему данных и, соответственно, эта полная схема есть и в parquet-файле.

Еще надо отметить, что, естественно, мы хотели по максимум переиспользовать уже существующий и зарекомендовавший себя код нашего frameworkа, а не городить какую-то полностью отдельную ветку для обработки JSONов то есть все изменения хотелось сделать на этапе чтения JSON-файлов с S3.

Таким образом очевидно, что для корректной загрузки данных из JSON-файлов необходимо предопределить схему JSON-файла с данными и читать файлы уже с применением этой схемы. Тогда у нас даже если в JSONе нет какого-то поля, то в самом DataFrame это поле будет, но его значение подставится как null:

df = spark.read \          .format("json") \          .option("multiline","false") \          .schema(df_schema) \          .load(path)

Первая мысль была использовать для хранения схемы имеющийся сервис метаданных - то есть описать схему в YAML-формате и сохранить в имеющемся репозитории. Но с учетом того, что все данные источников у нас проходят через Kafka, решили, что логично для хранения схем использовать имеющийся Kafka Schema Registry, а схему хранить в стандартном для JSON формате (другой формат, кстати говоря, Kafka Schema Registry не позволил бы хранить).

В общем, вырисовывалась следующая реализация:

  • Читаем из Kafka Schema Registry схему

  • Импортируем ее в pyspark.sql.types.StructType что-то типа такого:

# 1. получаем через Kafka Schema Registry REST API схему данных # 2. записываем ее в переменную schema и далее:df_schema = StructType.fromJson(schema)
  • Ну и с помощью полученной схемы читаем JSON-файлы

Звучит хорошо, если бы Давайте посмотрим на формат JSON-схемы, понятной Sparkу. Пусть имеем простой JSON из file2 выше. Посмотреть его схему в формате JSON можно, выполнив:

df.schema.json()  
Получившаяся схема
{    "fields":    [        {            "metadata": {},            "name": "dimensions",            "nullable": true,            "type":            {                "fields":                [                    {"metadata":{},"name":"height","nullable":true,"type":"double"},                    {"metadata":{},"name":"length","nullable":true,"type":"long"},                    {"metadata":{},"name":"width","nullable":true,"type":"long"}                ],                "type": "struct"            }        },        {            "metadata": {},            "name": "price",            "nullable": true,            "type": "double"        },        {            "metadata": {},            "name": "productId",            "nullable": true,            "type": "long"        },        {            "metadata": {},            "name": "productName",            "nullable": true,            "type": "string"        },        {            "metadata": {},            "name": "tags",            "nullable": true,            "type":            {                "containsNull": true,                "elementType": "string",                "type": "array"            }        }    ],    "type": "struct"}

Как видно, это совсем не стандартный формат JSON-схемы.

Но мы же наверняка не первые, у кого встала задача конвертировать стандартную JSON-схему в формат, понятный Sparkу - подумали мы и В принципе, не ошиблись, но несколько часов поиска упорно выводили исключительно на примеры, из серии:

как сохранить схему уже прочитанного DataFrame в JSON, затем использовать повторно

либо на репозиторий https://github.com/zalando-incubator/spark-json-schema, который нам бы подошел, если мы использовали Scala, а не pySpark

В общем, на горизонте маячила перспектива писать SchemaConverter. Сперва решили отделаться малой кровью и написать простенький конвертер никаких ссылок и прочих сложных конструкций. Конвертер был успешно протестирован на синтетических данных, после чего захотелось скормить ему что-то приближенное к реальности.

К счастью, у нас уже был один источник, генерирующий данные в формате JSON. Как временное решение схема его интеграции в DataPlatform была незамысловата: NiFi читал данные из Kafka, преобразовывал их в parquet, использую прибитую гвоздями в NiFi схему в формате AVRO-schema, и складывал на S3. Схема данных была действительно непростой и с кучей вложенных структур и нескольких десятков полей - неплохой тест-кейс в общем-то:

Посмотреть длинную портянку, если кому интересно :)
root |-- taskId: string (nullable = true) |-- extOrderId: string (nullable = true) |-- taskStatus: string (nullable = true) |-- taskControlStatus: string (nullable = true) |-- documentVersion: long (nullable = true) |-- buId: long (nullable = true) |-- storeId: long (nullable = true) |-- priority: string (nullable = true) |-- created: struct (nullable = true) |    |-- createdBy: string (nullable = true) |    |-- created: string (nullable = true) |-- lastUpdateInformation: struct (nullable = true) |    |-- updatedBy: string (nullable = true) |    |-- updated: string (nullable = true) |-- customerId: string (nullable = true) |-- employeeId: string (nullable = true) |-- pointOfGiveAway: struct (nullable = true) |    |-- selected: string (nullable = true) |    |-- available: array (nullable = true) |    |    |-- element: string (containsNull = true) |-- dateOfGiveAway: string (nullable = true) |-- dateOfGiveAwayEnd: string (nullable = true) |-- pickingDeadline: string (nullable = true) |-- storageLocation: string (nullable = true) |-- currentStorageLocations: array (nullable = true) |    |-- element: string (containsNull = true) |-- customerType: string (nullable = true) |-- comment: string (nullable = true) |-- totalAmount: double (nullable = true) |-- currency: string (nullable = true) |-- stockDecrease: boolean (nullable = true) |-- offline: boolean (nullable = true) |-- trackId: string (nullable = true) |-- transportationType: string (nullable = true) |-- stockRebook: boolean (nullable = true) |-- notificationStatus: string (nullable = true) |-- lines: array (nullable = true) |    |-- element: struct (containsNull = true) |    |    |-- lineId: string (nullable = true) |    |    |-- extOrderLineId: string (nullable = true) |    |    |-- productId: string (nullable = true) |    |    |-- lineStatus: string (nullable = true) |    |    |-- lineControlStatus: string (nullable = true) |    |    |-- orderedQuantity: double (nullable = true) |    |    |-- confirmedQuantity: double (nullable = true) |    |    |-- assignedQuantity: double (nullable = true) |    |    |-- pickedQuantity: double (nullable = true) |    |    |-- controlledQuantity: double (nullable = true) |    |    |-- allowedForGiveAwayQuantity: double (nullable = true) |    |    |-- givenAwayQuantity: double (nullable = true) |    |    |-- returnedQuantity: double (nullable = true) |    |    |-- sellingScheme: string (nullable = true) |    |    |-- stockSource: string (nullable = true) |    |    |-- productPrice: double (nullable = true) |    |    |-- lineAmount: double (nullable = true) |    |    |-- currency: string (nullable = true) |    |    |-- markingFlag: string (nullable = true) |    |    |-- operations: array (nullable = true) |    |    |    |-- element: struct (containsNull = true) |    |    |    |    |-- operationId: string (nullable = true) |    |    |    |    |-- type: string (nullable = true) |    |    |    |    |-- reason: string (nullable = true) |    |    |    |    |-- quantity: double (nullable = true) |    |    |    |    |-- dmCodes: array (nullable = true) |    |    |    |    |    |-- element: string (containsNull = true) |    |    |    |    |-- timeStamp: string (nullable = true) |    |    |    |    |-- updatedBy: string (nullable = true) |    |    |-- source: array (nullable = true) |    |    |    |-- element: struct (containsNull = true) |    |    |    |    |-- type: string (nullable = true) |    |    |    |    |-- items: array (nullable = true) |    |    |    |    |    |-- element: struct (containsNull = true) |    |    |    |    |    |    |-- assignedQuantity: double (nullable = true) |-- linkedObjects: array (nullable = true) |    |-- element: struct (containsNull = true) |    |    |-- objectType: string (nullable = true) |    |    |-- objectId: string (nullable = true) |    |    |-- objectStatus: string (nullable = true) |    |    |-- objectLines: array (nullable = true) |    |    |    |-- element: struct (containsNull = true) |    |    |    |    |-- objectLineId: string (nullable = true) |    |    |    |    |-- taskLineId: string (nullable = true)

Естественно, я не захотел перебивать руками захардкоженную схему, а воспользовался одним из многочисленных онлайн-конвертеров, позволяющих из Avro-схемы сделать JSON-схему. И тут меня ждал неприятный сюрприз: все перепробованные мною конвертеры на выходе использовали гораздо больше синтаксических конструкций, чем понимала первая версия конвертера. Дополнительно пришло осознание, что также как и я, наши пользователи (а для нас пользователями в данном контексте являются владельцы источников данных) с большой вероятностью могут использовать подобные конвертеры для того, чтобы получить JSON-схему, которую надо зарегистрировать в Kafka Schema Registry, из того, что у них есть.

В результате наш SparkJsonSchemaConverter был доработан появилась поддержка более сложных конструкций, таких как definitions, refs (только внутренние) и oneOf. Сам же парсер был оформлен уже в отдельный класс, который сразу собирал на основании JSON-схемы объект pyspark.sql.types.StructType

У нас почти сразу же родилась мысль, что хорошо бы было поделиться им с сообществом, так как мы в Леруа Мерлен сами активно используем продукты Open Source, созданные и поддерживаемые энтузиастами со всего мира, и хотим не просто их использовать, но и контрибьютить обратно, участвуя в разработке Open Source продуктов и поддерживая сообщество. В настоящий момент мы решаем внутренние орг.вопросы по схеме выкладывания данного конвертера в Open Source и, уверен, что в ближайшее время поделимся с сообществом этой наработкой!

В итоге благодаря написанному SparkJsonSchemaConverterу доработка шага Parsing свелась только к небольшому тюнингу чтения данных с S3: в зависимости от формата входных данных источника (получаем из сервиса метаданных) читаем файлы с S3 немного по-разному:

# Для JSONdf = spark.read.format(in_format)\            .option("multiline", "false")\            .schema(json_schema) \            .load(path)# Для parquet:df = spark.read.format(in_format)\            .load(path)

А дальше отрабатывает уже существующий код, раскрывающий все вложенные структуры согласно маппингу и сохраняющий данные DataFrameа в несколько плоских CSV-файлов.

В итоге мы смогли при относительном минимуме внесенных изменений в код текущего frameworkа добавить в него поддержку интеграции в нашу Data Platform JSON-источников данных. И результат нашей работы уже заметен:

  • Всего через месяц после внедрения доработки у нас на ПРОДе проинтегрировано 4 новых JSON-источника!

  • Все текущие интеграции даже не заметили расширения функционала frameworkа, а значит, мы точно ничего не сломали произведенной доработкой.

Подробнее..

Демистификация JWT

09.12.2020 18:14:11 | Автор: admin

Вокруг JWT сложилась много распространенных заблуждений. Одно из них, например, что JWT зашифровано (на самом деле только подписано и закодировано base64). На практике, я часто встречаюсь с довольно странными решениям, когда в JWT хранится идентификатор сессии (на самом деле если вы работаете с сессией то JWT вам не нужно). Или же в JWT хранится идентификатор пользователя, профайл которого запрашивается при каждом запросе из базы данных (на самом деле JWT имеет смысл, если Вы хотите уменьшить количество запросов в базу данных). Или, что еще более странно сами JWT хранятся в базе данных.


Одной из объективных причин такого недопонимания роли и места JWT является то, что спецификация JWT описывает формат JWT, и ничего не говорит о том как JWT нужно применять.


Желание написать об этом у меня возникло уже давно. И после просмотра очередного проекта с немного странным использованием JWT я все же решился.


Начнем с самой главной демистификации. JWT может выглядеть вот так:


eyJhbGciOiJSUzI1NiJ9.eyJpcCI6IjE3Mi4yMS4wLjUiLCJqdGkiOiIwNzlkZDMwMGFiODRlM2MzNGJjNWVkMTlkMjg1ZmRmZWEzNWJjYzExMmYxNDJiNmQ5M2Y3YmIxZWFmZTY4MmY1IiwiZXhwIjoxNjA3NTE0NjgxLCJjb3VudCI6MiwidHRsIjoxMH0.gH7dPMvf2TQaZ5uKVcm7DF4glIQNP01Dys7ADgsd6xcxOjpZ7yGhrgd3rMTHKbFyTOf9_EB5NEtNrtgaIsWTtCd3yWq21JhzbmoVXldJKDxjF841Qm4T6JfSth4vvDF5Ex56p7jgL3rkqk6WQCFigwwO2EJfc2ITWh3zO5CG05LWlCEOIJvJErZMwjt9EhmmGlj9B6hSsEGucCm6EDHVlof6DHsvbN2LM3Z9CyiCLNkGNViqr-jkDKbn8UwIuapJOrAT_dumeCWD1RYDL-WNHObaD3owX4iqwHss2yOFrUfdEynahX3jgzHrC36XSRZeEqmRnHZliczz99KeiuHfc56EF11AoxH-3ytOB1sMivj9LID-JV3ihaUj-cDwbPqiaFv0sL-pFVZ9d9KVUBRrkkrwTLVErFVx9UH9mHmIRiO3wdcimBrKpkMIZDTcU9ukAyaYbBlqYVEoTIGpom29u17-b05wY3y12lCA2n4ZqOceYiw3kyd46IYTGeiNmouG5Rb5ld1HJzyqsNDQJhwdibCImdCGhRuKQCa6aANIqFXM-XSvABpzhr1UmxDijzs30ei3AD8tAzkYe2cVhv3AyG63AcFybjFOU8cvchxZ97jCV32jYy6PFphajjHkq1JuZYjEY6kj7L-tBAFUUtjNiy_e0QSSu5ykJaimBsNzYFQ

Если его декодировать base64 миф о "секретности" сразу же разрушается:


{"alg":"RS256"}{"ip":"172.21.0.5","jti":"079dd300ab84e3c34bc5ed19d285fdfea35bcc112f142b6d93f7bb1eafe682f5","exp":1607514681,"count":2,"ttl":10} O2Mrn%!OPzN{hk11l\9Mkd    Z&WJP%^D8*X|!C&D0Di?Aknue7bB 6AV*9)S.jNv    `EcG9*6kQDv_xzEdgbs<wP("?K ?WxiHp<>,/EU]T-Q+\}Pfbu7ZTJ jhon-v 6j9:!z#fEewQ*44    bl"&t!F    *s>]+U&8z-@Fap2p\S}0hy*b1H/AU3bA$)   j)

Первая часть в фигурных скобках называется JOSE Header и описана https://tools.ietf.org/html/rfc7515. Может содержать поля, из которых наиболее важное alg. Если задать {"alg":"none"} токен считается валидным без подписи. И таким образом к Вашему API может получить доступ любой с токеном, сформированным вручную без подписи. В настоящее время большинство библиотек отвергают такие токены по умолчанию, но все же проверьте свои API на всякий случай.


Вторая часть в фигурных скобках это полезная нагрузка. Немного неудобно, что в этой части размещаются и пользовательские данные, и стандартные заголовки, например jti идентификатор токена и exp срок окончания действия токена. Такие поля как срок окончания действия токена, срок начала действия токена могут проверяться или не проверяться библитеками для работы с JWT в последнем случае придется в явном виде при проверять срок действия на уровне приложения.


И, наконец, подпись. Подпись может быть сформирована при помощи разных алгоритмов. Наиболее удобно, если алгоритмы основаны на паре ключей (публичном и приватном). Тогда проверку подписи можно проводить публичным ключом, который можно без опасения распространять между серверами, принимающими JWT.


Таким образом JWT это просто текст JSON, имеющий криптографическую подпись.


"Открыв" этот факт, можно поставить вопрос, в каких случаях рационально или не рационально использовать JWT. Можно предположить два основных кейса (если кто-то найдет еще готов добавить в текст).


  1. Микросервисы. Данные (любые не обязательно авторизация, а например "корзина" с товарами и зафиксированными ценами товаров) формируются и подписываются на одном микросервисе, а используются на другом микросервисе, который проверяет подпись токена публичным ключом.


  2. Авторизация. Этот кейс может быть полезен и для монолита, если нужно сократить количество запросов в базу данных. При реализации "традиционной" сессии каждый запрос API генерирует дополнительный запрос к базе данных. С JWT все, что берется в базе данных помещается в JWT и подписывается.



Поняв это, мы поймем на какое время нужно выпускать JWT на то время, которое не будет чувствительно, если в исходной информации что-то поменялось, а токен еще действует.


Все же есть случаи, когда информация слишком чувствительна, и нужно иметь возможность аннулировать JWT еще до окончания его срока действия. Для этого формируется реестр (например в Redis) аннулированных токенов. Поскольку время действия токена небольшое реестр будет также небольшим, если удалять из реестра просроченные токены.


Следующий вопрос. Если время жизни JWT небольшое, то как обновлять JWT после окончания срока действия (это актуально для авторизационных токенов)? Для этого предлагается использовать второй, "условно-постоянный" токен с более продолжительным сроком действия. По истечении срока действия авторизационного токена его обновляют с использованием "условно-постоянного" токена.


Есть рекомендации обновлять "условно-постоянный" токен вместе с выдачей нового авторизационного токена. В этом случае некоторые рекомендуют аннулировать "условно-постоянный" токен сразу после его использвания, делая такое использование однократным. В последнем варианте, сессия может прерваться, если сервер зафиксирует выдачу нового токена, но из-за сетевых проблем этот токен не будет получен клиентом.


Есть одна довольно интересная проблема с аннулированием "условно-постоянных" токенов. Если их выпустить бессрочными то в случае аннулирования их придется хранить также бессрочно. Если выпускать их на какой-то период то будет происходить разлогин из "вечной" сессии, если за время действия токена не будет обращения к API.


apapacy@gmail.com
9 декабря 2020 года

Подробнее..
Категории: Api , Authentication , Json , Jwt , Authorization , Passport.js

Сериализация в JSON и иммутабельный объект. О пакете built_value для Flutter

07.11.2020 14:06:38 | Автор: admin


Иногда JSON от API необходимо конвертировать в объект и желательно в иммутабельное значение. На Dart это возможно, но для этого необходимо много кодить для каждого из объектов. К счастью, существует пакет, который поможет Вам все это выполнить, и в этой статье я Вам расскажу об этом способе.

Наша цель:

1. Сериализация

final user = User.formJson({"name": "Maks"});final json = user.toJson();

2. Использование как значения

final user1 = User.formJson({"name": "Maks"});final user2 = User((b) => b..name='Maks');if (user1 == user2) print('Один и тот же пользователь');

3. Иммутабельность

user.name = 'Alex'; // Неверноfinal newUser = user.rebuild((b) => b..name='Alex'); // Верно


Устанавливаем пакеты


Открываем файл pubspec.yaml на нашем Flutter проекте и добавляем пакет built_value на dependencies:

  ...  built_value: ^7.1.0

А также добавляем пакеты built_value_generator и build_runner на dev_dependencies. Эти пакеты помогут генерировать необходимые коды.

dev_dependencies:

 ...  build_runner: ^1.10.2  built_value_generator: ^7.1.0

Сохраняем файл pubspec.yaml и запускаем flutter pub get чтобы получить все необходимые пакеты.

Создаем built_value


Давайте создадим простой класс чтобы увидеть как все работает.

Создаем новый файл user.dart:

import 'package:built_value/built_value.dart';part 'user.g.dart';abstract class User implements Built<User, UserBuilder> {  String get name;  User._();  factory User([void Function(UserBuilder) updates]) = _$User;}

Итак, мы создали простой абстрактный класс User с одним полем name, указали, что наш класс является частью user.g.dart и основная имплементация находится там, в том числе и UserBuilder. Чтобы автоматически создать этот файл, необходимо запустить это в командной строке:

flutter packages pub run build_runner watch

или

flutter packages pub run build_runner build

Запускаем с watch чтобы не перезапускать каждый раз, когда что-то меняется в классе.

После этого мы видим что появился новый файл user.g.dart. Можно посмотреть что внутри и увидеть сколько времени мы сэкономим автоматизируя этот процесс. Когда добавим еще поля и сериализацию этот файл станет еще больше.

Давайте проверим что у нас получилось:

final user = User((b) => b..name = "Max");print(user);print(user == User((b) => b..name = "Max")); // trueprint(user == User((b) => b..name = "Alex")); // false

nullable


Добавляем новое поле surname на User класс:

abstract class User implements Built<User, UserBuilder> {  String get name;  String get surname;...}

Если попробовать вот так:

final user = User((b) => b..name = 'Max');

То мы получим ошибку:

Tried to construct class "User" with null field "surname".

Чтобы surname сделать опциональным нужно использовать nullable:

@nullableString get surname;

или же нужно каждый раз давать surname:

final user = User((b) => b  ..name = 'Max'  ..surname = 'Madov');print(user);

Built Collection


Давайте используем массивы. Для этого нам поможет BuiltList:

import 'package:built_collection/built_collection.dart';...abstract class User implements Built<User, UserBuilder> {  ...  @nullable  BuiltList<String> get rights;...

final user = User((b) => b  ..name = 'Max'  ..rights.addAll(['read', 'write']));print(user);

Enum


Необходимо ограничить rights, так чтобы кроме read, write и delete не принимал другие значения. Для этого в новом файле под именем right.dart создаем новый EnumClass:

import 'package:built_collection/built_collection.dart';import 'package:built_value/built_value.dart';part 'right.g.dart';class Right extends EnumClass {  static const Right read = _$read;  static const Right write = _$write;  static const Right delete = _$delete;  const Right._(String name) : super(name);  static BuiltSet<Right> get values => _$rightValues;  static Right valueOf(String name) => _$rightValueOf(name);}

User:

@nullableBuiltList<Right> get rights;

Теперь rights принимает только тип Right:

final user = User((b) => b  ..name = 'Max'  ..rights.addAll([Right.read, Right.write]));print(user);

Сериализация


Чтобы эти объекты можно было легко конвертировать в JSON и обратно нам нужно добавить к нашим классам еще пару методов:

...import 'package:built_value/serializer.dart';import 'serializers.dart';...abstract class User implements Built<User, UserBuilder> {...  Map<String, dynamic> toJson() => serializers.serializeWith(User.serializer, this);  static User fromJson(Map<String, dynamic> json) =>serializers.deserializeWith(User.serializer, json);  static Serializer<User> get serializer => _$userSerializer;}

В принципе для сериализации хватит и этого:

static Serializer<User> get serializer => _$userSerializer;

Но для удобства добавим методы toJson и fromJson.

Также добавляем одну строку в класс Right:

import 'package:built_value/serializer.dart';,,,class Right extends EnumClass {...  static Serializer<Right> get serializer => _$rightSerializer;}

И нужно создать еще один файл с именем serializers.dart:

import 'package:built_collection/built_collection.dart';import 'package:built_value/serializer.dart';import 'package:built_value/standard_json_plugin.dart';import 'package:built_value_demo/right.dart';import 'package:built_value_demo/user.dart';part 'serializers.g.dart';@SerializersFor([Right, User])final Serializers serializers =(_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

Каждый новый Built класс необходимо добавить в @SerializersFor([...]) чтобы сериализация работала как надо.

Теперь можно проверять что у нас получилось:

final user = User.fromJson({  "name": "Max",  "rights": ["read", "write"]});print(user);print(user.toJson());

final user2 = User((b) => b  ..name = 'Max'  ..rights.addAll([Right.read, Right.write]));print(user == user2); // true

Давайте поменяем значения:

final user3 = user.rebuild((b) => b  ..surname = "Madov"  ..rights.replace([Right.read]));print(user3);

Дополнительно


По итогу найдутся и те кто скажут, что все равно необходимо писать довольно много. Но если Вы пользуетесь Visual Studio Code рекомендую установить снипет под названием Built Value Snippets и тогда можно все это генерировать автоматически. Для этого поищите в Marketplace или пройдите по этой ссылке.

После установки напишите в Dart файле bv и вы сможете увидеть какие опции существуют.

Если Вы не хотите чтобы Visual Studio Code показывал сгенерированные *.g.dart файлы, нужно открыть Settings и поискать Files: Exclude, после чего нажмите на Add Pattern и добавьте **/*.g.dart.

Что дальше?


На первый взгляд, может показаться, что такое количество приложенных усилий не стоит того, но если у вас много таких классов это в разы облегчит и ускорит весь процесс.

P.S. Буду очень рад и благодарен Вам, если поделитесь своими методами, которые посчитаете практичнее и эффективнее предложенного мной.

GitHub проект

Пакеты:
pub.dev/packages/built_value
pub.dev/packages/built_value_generator
pub.dev/packages/build_runner
Подробнее..
Категории: Dart , Flutter , Json , Serialization , Immutable

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru