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

Apache nifi

Чем заняты сотрудники? Анализируем Jira Software

17.11.2020 18:17:45 | Автор: admin
Таск-треккер как исправный источник данных для стратегического управления. Звучит красиво. А в нашей компании это даже работает и приносит пользу.

Данная статья является углублением к предыдущей: Автоматизация аналитики Jira средствами Apache NiFi. Теперь хочу подробнее раскрыть наш взгляд на отчетность по Jira Software и опыт ее реализации при помощи R. Язык тут, конечно же, не догма. Сегодня наше все это концепция.

image

Картинка позаимствована тут.



Давайте представим себе таск-треккер. Какие данные я, как аналитик, могу из него добыть? Или любая готовая аналитическая платформа? Количество задач за период, статистику логирования, базовую разбивку на проекты, картинки какие-нить о продуктивности сотрудников да и все, навскидку.

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

Таки да, нам это удалось. Правда, не обошлось без помощи PM-a попервой.

Реорганизация таск-треккера


Разумеется каждая конкретная Jira существенно отличается от какой-либо другой. И наше решение вполне может не быть для вас самым эффективным или же вообще применимым.

Прошу расценивать это лишь как идею организации таск-треккера, на примере нашей аутсорсинговой компании.

Мы сделали всего два движения.

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

О втором чуть подробнее. Нам удасться увидеть общую картину и ответить на стратегические вопросы, если рабочий процесс будет представлен в виде некого потока, в который каждый сотрудник делает свой вклад. Для этого, в нашем случае, идеально подходит концепция IT4IT, со своей операционной моделью, базирующейся на четырехпотоковой цепочке создания ценности:
image
Собственно, что мы сделали. Воспользовавшись IT4IT, добавили такое понятие как компонента задачи в Jira. У нас они следующие:

  • Service to Portfolio (Demand and Selection) стадия раскурки, поиска, выбора сервиса, технологии.
  • Request to Deploy (Plan and Design) обсуждение, планирование разработки, развития услуг, сервисов.
  • Request to Deploy (Develop) разработка услуги, сервиса чего либо.
  • Request to Deploy (Deploy) развертывание чего либо.
  • Request to Deploy (Test) тестирование сервиса, услуги.
  • Request to Fulfill этап эксплуатации разработанных сервисов, предоставление услуг.
  • Detect to Correct (Correct) исправление, доработка внутренних сервисов и услуг.
  • Detect to Correct (Monitor&Feedback) тоже самое, только + общение с клиентом.

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

Аналитика данных в R


Теперь препятствий для реализации отчетности быть не должно. Сформулирую подобие ТЗ.

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

Зная что нам нужно добыть идем выгружать данные. Через Jira API запрашиваем все таски (issue), обновленные на прошедшей неделе. Добываем из них ключи и к каждой таске догружаем историю логирования (worklog) и историю изменений (changelog). Извращения с догрузкой необходимы, чтобы обойти ограничения апишки.

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

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

Ну и, наконец-то мы подобрались к аналитике.

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

# Открыто тасокthis_week_opened <- jira_changelog_data %>%         filter(issue_type != "Epic") %>%         filter(as.Date(issue_created) >= start_date) %>%         filter(as.Date(issue_created) <= end_date) %>%   select(key, issue_created) %>% unique() %>% nrow()# В работеthis_week_processed <- jira_worklog_data %>% filter(as.Date(started) >= start_date) %>%         filter(as.Date(started) <= end_date) %>%   select(key) %>% unique() %>% nrow()# Закрытоthis_week_closed <- jira_changelog_data %>%         filter(issue_type != "Epic") %>%         filter(as.Date(issue_resolutiondate) >= start_date) %>%         filter(as.Date(issue_resolutiondate) <= end_date) %>%  select(key, issue_created) %>% unique() %>% nrow()# Отправлено в холд / бэклог this_week_holded <- issue_history %>% filter(change_date >= start_date) %>%         filter(change_date <= end_date) %>%filter(toString == "Hold" | toString == "Backlog") %>%   select(key) %>% unique() %>% nrow()


Не напоминает ли вам это псевдокод? А если я скажу что оператор '%>%' передает данные от предыдущей функции к следующей. А последняя модификация во всей цепочке будет сохранена в переменную. Представьте себе, мы только что поднялись на порог вхождения в R!

Вы еще не влюбились в него? Тогда, если позволите, я насыплю еще немного инфы.

Словеса из Википедии:
В целом, как язык программирования, R довольно прост и даже примитивен. Его наиболее сильная сторона возможность неограниченного расширения с помощью пакетов.

В базовую поставку R включен основной набор пакетов, а всего по состоянию на 2019 год доступно более 15 316 пакетов.

И последнее на сегодня. В этом году R ворвался в десятку самых популярных языков в мире (пруф). Горжусь им.

Прошу простить мне сие отступление. Об R я могу говорить часами. Просто он насквозь окутан мифами, а я люблю их разрушать хобби, знаете ли.

Вернемся к отчету. Имея искомые цифры, визуализируем их. После этого оформляем логирование сотрудников. Вот как выглядит данная часть у нас:

image

Продолжу показывать вам итоговые картинки из этого же реального отчета.

Направление нашей деятельности отражает следующий график. Он также позволяет оценить загруженость сотрудников операционной деятельностью.

image

А вот и разрез всех задач по компонентам. Он и дает ответ на вопрос чем мы занимаемся. Картину дополняю цифрами.

image

Ну и обещанный вклад каждого сотрудника в общую картину, приведенную выше.

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

image

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

Генерация отчета


Настроить автоматическую генерацию отчета, например, по понедельникам из R скрипта можно с помощью пакета cronR, это исключительно просто.

У нас же все сложнее и изящнее. Еженедельную выгрузку данных из Jira API, запуск скрипта генерации отчета и отправку отчета всем сотрудникам по Email мы реализовали с помощью Apache NiFi. Эта тема насколько обширная, что вполне себе заслужила отдельную статью.

Заключение


Количество реализаций Jira Software, как и количество компаний где она используется уйма. Каждый босс, при этом, нуждается в своей уникальньной базе метрик для тактически верного управления. Да, существует eazyBI и другие плагины для Jira аналитики, но в результате это как покупка костюма в магазине, вместо пошитого на заказ Bespoke.

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

Спасибо.
Подробнее..

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

27.11.2020 18:11:38 | Автор: admin

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


В предыдущей статье было описано, как получить файлы XML с данными ЕГРЮЛ, которые требуется импортировать.


В данной статье описан способ преобразования XML в JSON.



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


Для преобразования XML в JSON используется процессор ConvertRecord. В котором используются два контроллера: FnsEgrulXmlReader типа XMLReader для чтения данных из XML и FnsEgrulJsonWriter типа JsonRecordSetWriter для записи этих данных в JSON.





Для работы контроллерам XMLReader и JsonRecordSetWriter требуются сведения о структуре читаемых и записываемых данных, т.е. схема данных. Я использовал схему AVRO. Для ее хранения в NiFi используется контроллер AvroSchemaRegistry. В нем задается имя схемы в поле Property и ее содержимое в поле Value



AVRO схема должна описывать структуру данных, соответствующую XSD-схеме, опубликованной на сайте ФНС. Не обязательно описывать всю структуру. Достаточно лишь в части тех данных, которые требуется импортировать.


Пример исходного XML
<?xml version="1.0" encoding="windows-1251" ?><EGRUL ДатаВыг="2020-05-20"><СвЮЛ ДатаВып="2020-05-20" ОГРН="1234567890123" ДатаОГРН="2002-12-30" ИНН="1234567890" КПП="123456789" СпрОПФ="ОКОПФ" КодОПФ="12300" ПолнНаимОПФ="Общества с ограниченной ответственностью">  <СвНаимЮЛ НаимЮЛПолн="ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ" НаимЮЛСокр="ООО">    <ГРНДата ГРН="1234567890123" ДатаЗаписи="2002-12-30" />  </СвНаимЮЛ>  <СвАдресЮЛ>    <АдресРФ Индекс="143500" КодРегион="50" КодАдрКладр="500000570000011">      <Регион ТипРегион="ОБЛАСТЬ" НаимРегион="МОСКОВСКАЯ" />      <Город ТипГород="ГОРОД" НаимГород="ИСТРА" />      <Улица ТипУлица="ПЕРЕУЛОК" НаимУлица="ВОЛОКОЛАМСКИЙ" />      <ГРНДата ГРН="1234567890123" ДатаЗаписи="2016-02-22" />      <ГРНДатаИспр ГРН="1234567890123" ДатаЗаписи="2019-03-08" />    </АдресРФ>  </СвАдресЮЛ>  <СвОбрЮЛ ОГРН="1234567890123" ДатаОГРН="2002-12-30" РегНом="12:12:12345" ДатаРег="1997-12-24" НаимРО="Московская областная регистрационная палата">    <СпОбрЮЛ КодСпОбрЮЛ="01" НаимСпОбрЮЛ="Создание юридического лица до 01.07.2002" />    <ГРНДата ГРН="1234567890123" ДатаЗаписи="2002-12-30" />  </СвОбрЮЛ>  <СвРегОрг КодНО="5081" НаимНО="Межрайонная инспекция Федеральной налоговой службы" АдрРО="144000,РОССИЯ,МОСКОВСКАЯ ОБЛ,,ЭЛЕКТРОСТАЛЬ Г,,СОВЕТСКАЯ УЛ,26А,,">    <ГРНДата ГРН="1234567890123" ДатаЗаписи="2019-01-31" />  </СвРегОрг>  <СвСтатус>    <СвСтатус КодСтатусЮЛ="105" НаимСтатусЮЛ="Регистрирующим органом принято решение о предстоящем исключении юридического лица из ЕГРЮЛ (недействующее юридическое лицо)" />    <СвРешИсклЮЛ ДатаРеш="2020-05-18" НомерРеш="12345" ДатаПубликации="2020-05-20" НомерЖурнала="1" />    <ГРНДата ГРН="1234567890123" ДатаЗаписи="2020-05-20" />  </СвСтатус>  <СвУчетНО ИНН="1234567890" КПП="123456789" ДатаПостУч="1998-01-20">    <СвНО КодНО="5017" НаимНО="Инспекция Федеральной налоговой службы" />    <ГРНДата ГРН="1234567890123" ДатаЗаписи="2007-11-01" />  </СвУчетНО>  <СвРегПФ РегНомПФ="123456789012" ДатаРег="1998-01-15">    <СвОргПФ КодПФ="060010" НаимПФ="Государственное учреждение - Управление Пенсионного фонда РФ" />    <ГРНДата ГРН="1234567890123" ДатаЗаписи="2006-05-05" />  </СвРегПФ>  <СвРегФСС РегНомФСС="123456789012345" ДатаРег="1998-01-15">    <СвОргФСС КодФСС="5023" НаимФСС="Филиал 23 Государственного учреждения - Московского областного регионального отделения Фонда социального страхования Российской Федерации" />    <ГРНДата ГРН="1234567890123" ДатаЗаписи="2016-11-01" />  </СвРегФСС>  <СведДолжнФЛ>    <ГРНДатаПерв ГРН="1234567890123" ДатаЗаписи="2005-07-20" />    <СвФЛ Фамилия="ИВАНОВ" Имя="ИВАН" Отчество="ИВАНОВИЧ" ИННФЛ="123456789012">      <ГРНДата ГРН="1234567890123" ДатаЗаписи="2020-03-18" />    </СвФЛ>    <СвДолжн ВидДолжн="02" НаимВидДолжн="Руководитель юридического лица" НаимДолжн="ГЕНЕРАЛЬНЙ ДИРЕКТОР">      <ГРНДата ГРН="1234567890123" ДатаЗаписи="2020-03-18" />    </СвДолжн>  </СведДолжнФЛ>  <СвУчредит>    <УчрФЛ>      <ГРНДатаПерв ГРН="1234567890123" ДатаЗаписи="2005-07-20" />      <СвФЛ Фамилия="ИВАНОВ" Имя="ИВАН" Отчество="ИВАНОВИЧ" ИННФЛ="123456789012">        <ГРНДата ГРН="1234567890123" ДатаЗаписи="2020-03-18" />      </СвФЛ>      <ДоляУстКап НоминСтоим="20000">        <РазмерДоли>          <Процент>50</Процент>        </РазмерДоли>        <ГРНДата ГРН="1234567890123" ДатаЗаписи="2005-07-20" />        <ГРНДатаИспр ГРН="1234567890123" ДатаЗаписи="2018-08-30" />      </ДоляУстКап>    </УчрФЛ><УчрФЛ>      <ГРНДатаПерв ГРН="1234567890123" ДатаЗаписи="2005-07-20" />      <СвФЛ Фамилия="ПЕТРОВ" Имя="ПЕТР" Отчество="ПЕТРОВИЧ" ИННФЛ="123456789021">        <ГРНДата ГРН="1234567890123" ДатаЗаписи="2020-03-18" />      </СвФЛ>      <ДоляУстКап НоминСтоим="20000">        <РазмерДоли>          <Процент>50</Процент>        </РазмерДоли>        <ГРНДата ГРН="1234567890123" ДатаЗаписи="2005-07-20" />        <ГРНДатаИспр ГРН="1234567890123" ДатаЗаписи="2018-08-30" />      </ДоляУстКап>    </УчрФЛ>  </СвУчредит>  <СвОКВЭД>    <СвОКВЭДОсн КодОКВЭД="47.11" НаимОКВЭД="Торговля розничная преимущественно пищевыми продуктами, включая напитки, и табачными изделиями в неспециализированных магазинах" ПрВерсОКВЭД="2014">      <ГРНДата ГРН="1234567890123" ДатаЗаписи="2005-07-20" />      <ГРНДатаИспр ГРН="1234567890123" ДатаЗаписи="2018-08-30" />    </СвОКВЭДОсн>  </СвОКВЭД>  <СвЗапЕГРЮЛ ИдЗап="1234567890" ГРН="1234567890123" ДатаЗап="2002-12-30">    <ВидЗап КодСПВЗ="11101" НаимВидЗап="Внесение в Единый государственный реестр юридических лиц сведений о юридическом лице, зарегистрированном до 1 июля 2002 года" />    <СвРегОрг КодНО="5017" НаимНО="Инспекция МНС России по г.Истре Московской области" />    <СвСвид Серия="12" Номер="123456789" ДатаВыдСвид="2002-12-30" />  </СвЗапЕГРЮЛ>  <СвЗапЕГРЮЛ ИдЗап="1234567891" ГРН="1234567890123" ДатаЗап="2005-07-20">    <ВидЗап КодСПВЗ="12101" НаимВидЗап="Государственная регистрация изменений, внесенных в учредительные документы юридического лица, связанных с внесением изменений в сведения о юридическом лице, содержащиеся в Едином государственном реестре юридических лиц, на основании заявления" />    <СвРегОрг КодНО="5017" НаимНО="Инспекция Федеральной налоговой службы по г.Истре Московской области" />    <СведПредДок>      <НаимДок>ЗАЯВЛЕНИЕ О ГОСУДАРСТВЕННОЙ РЕГИСТРАЦИИ ИЗМЕНЕНИЙ, ВНОСИМХ В УЧРЕДИТЕЛЬНЕ ДОКУМЕНТ  ЮРИДИЧЕСКОГО ЛИЦА</НаимДок>      <НомДок>1</НомДок>      <ДатаДок>2005-07-14</ДатаДок>    </СведПредДок>    <СведПредДок>      <НаимДок>УСТАВ</НаимДок>      <НомДок>2</НомДок>      <ДатаДок>2005-07-14</ДатаДок>    </СведПредДок>    <СведПредДок>      <НаимДок>РЕШЕНИЕ</НаимДок>      <НомДок>3</НомДок>      <ДатаДок>2005-07-14</ДатаДок>    </СведПредДок>    <СведПредДок>      <НаимДок>КВИТАНЦИЯ</НаимДок>      <НомДок>4</НомДок>      <ДатаДок>2005-07-14</ДатаДок>    </СведПредДок>    <СвСвид Серия="12" Номер="123456789" ДатаВыдСвид="2005-07-20" />  </СвЗапЕГРЮЛ></СвЮЛ></EGRUL>

Пример получаемого 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  }} ]

AVRO схема
{ "type": "record", "name": "СвЮЛ",  "fields": [    { "name": "reportDate", "aliases": [ "ДатаВып" ], "type": "string" },    { "name": "ogrn", "aliases": [ "ОГРН" ], "type": "string" },    { "name": "ogrnDate", "aliases": [ "ДатаОГРН" ], "type": "string" },    { "name": "inn", "aliases": [ "ИНН" ], "type": "string" },    { "name": "kpp", "aliases": [ "КПП" ], "type": "string" },    { "name": "opfCode", "aliases": [ "КодОПФ" ], "type": "string" },    { "name": "opfName", "aliases": [ "ПолнНаимОПФ" ], "type": "string" },    { "name": "name", "aliases": [ "СвНаимЮЛ" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвНаимЮЛ.СвЮЛ",        "fields": [          { "name": "fullName", "aliases": [ "НаимЮЛПолн" ], "type": "string" },          { "name": "shortName", "aliases": [ "НаимЮЛСокр" ], "type": "string" }        ]      }    },    { "name": "address", "aliases": [ "СвАдресЮЛ" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвАдресЮЛ.СвЮЛ",        "fields": [          { "name": "addressRF", "aliases": [ "АдресРФ" ], "namespace": "СвАдресЮЛ.СвЮЛ",            "type": { "type": "record", "name": "АдресРФ.СвАдресЮЛ.СвЮЛ",              "fields": [                { "name": "region", "aliases": [ "Регион" ], "namespace": "АдресРФ.СвАдресЮЛ.СвЮЛ",                  "type": { "type": "record", "name": "Регион.АдресРФ.СвАдресЮЛ.СвЮЛ",                    "fields": [                      { "name": "type", "aliases": [ "ТипРегион" ], "type": "string" },                      { "name": "name", "aliases": [ "НаимРегион" ], "type": "string" }                    ]                  }                },                { "name": "district", "aliases": [ "Район" ], "namespace": "АдресРФ.СвАдресЮЛ.СвЮЛ",                  "type": { "type": "record", "name": "Район.АдресРФ.СвАдресЮЛ.СвЮЛ",                    "fields": [                      { "name": "type", "aliases": [ "ТипРайон" ], "type": "string" },                      { "name": "name", "aliases": [ "НаимРайон" ], "type": "string" }                    ]                  }                },                { "name": "town", "aliases": [ "Город" ], "namespace": "АдресРФ.СвАдресЮЛ.СвЮЛ",                  "type": { "type": "record", "name": "Город.АдресРФ.СвАдресЮЛ.СвЮЛ",                    "fields": [                      { "name": "type", "aliases": [ "ТипГород" ], "type": "string" },                      { "name": "name", "aliases": [ "НаимГород" ], "type": "string" }                    ]                  }                },                { "name": "settlement", "aliases": [ "НаселПункт" ], "namespace": "АдресРФ.СвАдресЮЛ.СвЮЛ",                  "type": { "type": "record", "name": "НаселПункт.АдресРФ.СвАдресЮЛ.СвЮЛ",                    "fields": [                      { "name": "type", "aliases": [ "ТипНаселПункт" ], "type": "string" },                      { "name": "name", "aliases": [ "НаимНаселПункт" ], "type": "string" }                    ]                  }                },                { "name": "street", "aliases": [ "Улица" ], "namespace": "АдресРФ.СвАдресЮЛ.СвЮЛ",                  "type": { "type": "record", "name": "Улица.АдресРФ.СвАдресЮЛ.СвЮЛ",                    "fields": [                      { "name": "type", "aliases": [ "ТипУлица" ], "type": "string" },                      { "name": "name", "aliases": [ "НаимУлица" ], "type": "string" }                    ]                  }                },                { "name": "index", "aliases": [ "Индекс" ], "type": "string" },                { "name": "regionCode", "aliases": [ "КодРегион" ], "type": "string" },                { "name": "kladr", "aliases": [ "КодАдрКладр" ], "type": "string" },                { "name": "house", "aliases": [ "Дом" ], "type": "string" },                { "name": "building", "aliases": [ "Корпус" ], "type": "string" },                { "name": "apartment", "aliases": [ "Кварт" ], "type": "string" }              ]            }          }        ]      }    },    { "name": "termination", "aliases": [ "СвПрекрЮЛ" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвПрекрЮЛ.СвЮЛ",        "fields": [          { "name": "method", "aliases": [ "СпПрекрЮЛ" ], "namespace": "СвПрекрЮЛ.СвЮЛ",            "type": { "type": "record", "name": "СпПрекрЮЛ.СвПрекрЮЛ.СвЮЛ",              "fields": [                { "name": "code", "aliases": [ "КодСпПрекрЮЛ" ], "type": "string" },                { "name": "name", "aliases": [ "НаимСпПрекрЮЛ" ], "type": "string" }              ]            }          },          { "name": "date", "aliases": [ "ДатаПрекрЮЛ" ], "type": "string" }        ]      }    },    { "name": "capital", "aliases": [ "СвУстКап" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвУстКап.СвЮЛ",        "fields": [          { "name": "type", "aliases": [ "НаимВидКап" ], "type": "string" },          { "name": "amount", "aliases": [ "СумКап" ], "type": "double" },          { "name": "partRUR", "aliases": [ "ДоляРубля" ], "namespace": "СвУстКап.СвЮЛ",            "type": { "type": "record", "name": "ДоляРубля.СвУстКап.СвЮЛ",              "fields": [                { "name": "num", "aliases": [ "Числит" ], "type": "long" },                { "name": "denom", "aliases": [ "Знаменат" ], "type": "long" }              ]            }          }        ]      }    },    { "name": "manageOrg", "aliases": [ "СвУпрОрг" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвУпрОрг.СвЮЛ",        "fields": [          { "name": "egrulData", "aliases": [ "НаимИННЮЛ" ], "namespace": "СвУпрОрг.СвЮЛ",            "type": { "type": "record", "name": "НаимИННЮЛ.СвУпрОрг.СвЮЛ",              "fields": [                { "name": "ogrn", "aliases": [ "ОГРН" ], "type": "string" },                { "name": "inn", "aliases": [ "ИНН" ], "type": "string" },                { "name": "fullName", "aliases": [ "НаимЮЛПолн" ], "type": "string" }              ]            }          }        ]      }    },    { "name": "director", "aliases": [ "СведДолжнФЛ" ], "namespace": "СвЮЛ",      "type": { "type": "array",        "items": [          { "name": "СведДолжнФЛ.СвЮЛ", "type": "record",            "fields": [              { "name": "fl", "aliases": [ "СвФЛ" ], "namespace": "СведДолжнФЛ.СвЮЛ",                "type": { "type": "record", "name": "СвФЛ.СведДолжнФЛ.СвЮЛ",                  "fields": [                    { "name": "lastName", "aliases": [ "Фамилия" ], "type": "string" },                    { "name": "firstName", "aliases": [ "Имя" ], "type": "string" },                    { "name": "patronymic", "aliases": [ "Отчество" ], "type": "string" },                    { "name": "inn", "aliases": [ "ИННФЛ" ], "type": "string" }                  ]                }              },              { "name": "position", "aliases": [ "СвДолжн" ], "namespace": "СведДолжнФЛ.СвЮЛ",                "type": { "type": "record", "name": "СвДолжн.СведДолжнФЛ.СвЮЛ",                  "fields": [                    { "name": "ogrnip", "aliases": [ "ОГРНИП" ], "type": "string" },                    { "name": "typeCode", "aliases": [ "ВидДолжн" ], "type": "string" },                    { "name": "typeName", "aliases": [ "НаимВидДолжн" ], "type": "string" },                    { "name": "name", "aliases": [ "НаимДолжн" ], "type": "string" }                  ]                }              },              { "name": "disqualification", "aliases": [ "СвДискв" ], "namespace": "СведДолжнФЛ.СвЮЛ",                "type": { "type": "record", "name": "СвДискв.СведДолжнФЛ.СвЮЛ",                  "fields": [                    { "name": "startDate", "aliases": [ "ДатаНачДискв" ], "type": "string" },                    { "name": "endDate", "aliases": [ "ДатаОкончДискв" ], "type": "string" },                    { "name": "decisionDate", "aliases": [ "ДатаРеш" ], "type": "string" }                  ]                }              }            ]          }        ]      }    },    { "name": "founders", "aliases": [ "СвУчредит" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвУчредит.СвЮЛ",        "fields": [          { "name": "founderULRF", "aliases": [ "УчрЮЛРос" ], "namespace": "СвУчредит.СвЮЛ",            "type": { "type": "array",              "items": [                { "name": "УчрЮЛРос.СвУчредит.СвЮЛ", "type": "record",                  "fields": [                    { "name": "egrulData", "aliases": [ "НаимИННЮЛ" ], "namespace": "УчрЮЛРос.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "НаимИННЮЛ.УчрЮЛРос.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "ogrn", "aliases": [ "ОГРН" ], "type": "string" },                          { "name": "inn", "aliases": [ "ИНН" ], "type": "string" },                          { "name": "fullName", "aliases": [ "НаимЮЛПолн" ], "type": "string" }                        ]                      }                    },                    { "name": "oldRegData", "aliases": [ "СвРегСтарые" ], "namespace": "УчрЮЛРос.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "СвРегСтарые.УчрЮЛРос.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "regNumber", "aliases": [ "РегНом" ], "type": "string" },                          { "name": "regDate", "aliases": [ "ДатаРег" ], "type": "string" },                          { "name": "regOrg", "aliases": [ "НаимРО" ], "type": "string" }                        ]                      }                    },                    { "name": "capitalPart", "aliases": [ "ДоляУстКап" ], "namespace": "УчрЮЛРос.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "ДоляУстКап.УчрЮЛРос.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "nominal", "aliases": [ "НоминСтоим" ], "type": "double" },                          { "name": "size", "aliases": [ "РазмерДоли" ], "namespace": "ДоляУстКап.УчрЮЛРос.СвУчредит.СвЮЛ",                            "type": { "type": "record", "name": "РазмерДоли.ДоляУстКап.УчрЮЛРос.СвУчредит.СвЮЛ",                              "fields": [                                { "name": "percent", "aliases": [ "Процент" ], "type": "double" },                                { "name": "decimalPart", "aliases": [ "ДробДесят" ], "type": "double" },                                { "name": "simplePart", "aliases": [ "ДробПрост" ], "namespace": "РазмерДоли.ДоляУстКап.УчрЮЛРос.СвУчредит.СвЮЛ",                                  "type": { "type": "record", "name": "ДробПрост.РазмерДоли.ДоляУстКап.УчрЮЛРос.СвУчредит.СвЮЛ",                                    "fields": [                                      { "name": "num", "aliases": [ "Числит" ], "type": "long" },                                      { "name": "denom", "aliases": [ "Знаменат" ], "type": "long" }                                    ]                                  }                                }                              ]                            }                          }                        ]                      }                    }                  ]                }              ]            }          },          { "name": "founderULForeign", "aliases": [ "УчрЮЛИн" ], "namespace": "СвУчредит.СвЮЛ",            "type": { "type": "array",              "items": [                { "name": "УчрЮЛИн.СвУчредит.СвЮЛ", "type": "record",                  "fields": [                    { "name": "egrulData", "aliases": [ "НаимИННЮЛ" ], "namespace": "УчрЮЛИн.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "НаимИННЮЛ.УчрЮЛИн.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "ogrn", "aliases": [ "ОГРН" ], "type": "string" },                          { "name": "inn", "aliases": [ "ИНН" ], "type": "string" },                          { "name": "fullName", "aliases": [ "НаимЮЛПолн" ], "type": "string" }                        ]                      }                    },                    { "name": "foreignReg", "aliases": [ "СвРегИн" ], "namespace": "УчрЮЛИн.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "СвРегИн.УчрЮЛИн.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "oksm", "aliases": [ "ОКСМ" ], "type": "string" },                          { "name": "country", "aliases": [ "НаимСтран" ], "type": "string" },                          { "name": "regDate", "aliases": [ "ДатаРег" ], "type": "string" },                          { "name": "regNumber", "aliases": [ "РегНомер" ], "type": "string" },                          { "name": "regOrg", "aliases": [ "НаимРегОрг" ], "type": "string" },                          { "name": "address", "aliases": [ "АдрСтр" ], "type": "string" }                        ]                      }                    },                    { "name": "capitalPart", "aliases": [ "ДоляУстКап" ], "namespace": "УчрЮЛИн.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "ДоляУстКап.УчрЮЛИн.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "nominal", "aliases": [ "НоминСтоим" ], "type": "double" },                          { "name": "size", "aliases": [ "РазмерДоли" ], "namespace": "ДоляУстКап.УчрЮЛИн.СвУчредит.СвЮЛ",                            "type": { "type": "record", "name": "РазмерДоли.ДоляУстКап.УчрЮЛИн.СвУчредит.СвЮЛ",                              "fields": [                                { "name": "percent", "aliases": [ "Процент" ], "type": "double" },                                { "name": "decimalPart", "aliases": [ "ДробДесят" ], "type": "double" },                                { "name": "simplePart", "aliases": [ "ДробПрост" ], "namespace": "РазмерДоли.ДоляУстКап.УчрЮЛИн.СвУчредит.СвЮЛ",                                  "type": { "type": "record", "name": "ДробПрост.РазмерДоли.ДоляУстКап.УчрЮЛИн.СвУчредит.СвЮЛ",                                    "fields": [                                      { "name": "num", "aliases": [ "Числит" ], "type": "long" },                                      { "name": "denom", "aliases": [ "Знаменат" ], "type": "long" }                                    ]                                  }                                }                              ]                            }                          }                        ]                      }                    }                  ]                }              ]            }          },          { "name": "founderFL", "aliases": [ "УчрФЛ" ], "namespace": "СвУчредит.СвЮЛ",            "type": { "type": "array",              "items": [                { "name": "УчрФЛ.СвУчредит.СвЮЛ", "type": "record",                  "fields": [                    { "name": "fl", "aliases": [ "СвФЛ" ], "namespace": "УчрФЛ.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "СвФЛ.УчрФЛ.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "lastName", "aliases": [ "Фамилия" ], "type": "string" },                          { "name": "firstName", "aliases": [ "Имя" ], "type": "string" },                          { "name": "patronymic", "aliases": [ "Отчество" ], "type": "string" },                          { "name": "inn", "aliases": [ "ИННФЛ" ], "type": "string" }                        ]                      }                    },                    { "name": "capitalPart", "aliases": [ "ДоляУстКап" ], "namespace": "УчрФЛ.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "ДоляУстКап.УчрФЛ.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "nominal", "aliases": [ "НоминСтоим" ], "type": "double" },                          { "name": "size", "aliases": [ "РазмерДоли" ], "namespace": "ДоляУстКап.УчрФЛ.СвУчредит.СвЮЛ",                            "type": { "type": "record", "name": "РазмерДоли.ДоляУстКап.УчрФЛ.СвУчредит.СвЮЛ",                              "fields": [                                { "name": "percent", "aliases": [ "Процент" ], "type": "double" },                                { "name": "decimalPart", "aliases": [ "ДробДесят" ], "type": "double" },                                { "name": "simplePart", "aliases": [ "ДробПрост" ], "namespace": "РазмерДоли.ДоляУстКап.УчрФЛ.СвУчредит.СвЮЛ",                                  "type": { "type": "record", "name": "ДробПрост.РазмерДоли.ДоляУстКап.УчрФЛ.СвУчредит.СвЮЛ",                                    "fields": [                                      { "name": "num", "aliases": [ "Числит" ], "type": "long" },                                      { "name": "denom", "aliases": [ "Знаменат" ], "type": "long" }                                    ]                                  }                                }                              ]                            }                          }                        ]                      }                    }                  ]                }              ]            }          },          { "name": "founderGov", "aliases": [ "УчрРФСубМО" ], "namespace": "СвУчредит.СвЮЛ",            "type": { "type": "array",              "items": [                { "name": "УчрРФСубМО.СвУчредит.СвЮЛ", "type": "record",                  "fields": [                    { "name": "govOrg", "aliases": [ "ВидНаимУчр" ], "namespace": "УчрРФСубМО.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "ВидНаимУчр.УчрРФСубМО.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "code", "aliases": [ "КодУчрРФСубМО" ], "type": "string" },                          { "name": "name", "aliases": [ "НаимМО" ], "type": "string" },                          { "name": "regionCode", "aliases": [ "КодРегион" ], "type": "string" },                          { "name": "regionName", "aliases": [ "НаимРегион" ], "type": "string" }                        ]                      }                    },                    { "name": "capitalPart", "aliases": [ "ДоляУстКап" ], "namespace": "УчрРФСубМО.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "ДоляУстКап.УчрРФСубМО.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "nominal", "aliases": [ "НоминСтоим" ], "type": "double" },                          { "name": "size", "aliases": [ "РазмерДоли" ], "namespace": "ДоляУстКап.УчрРФСубМО.СвУчредит.СвЮЛ",                            "type": { "type": "record", "name": "РазмерДоли.ДоляУстКап.УчрРФСубМО.СвУчредит.СвЮЛ",                              "fields": [                                { "name": "percent", "aliases": [ "Процент" ], "type": "double" },                                { "name": "decimalPart", "aliases": [ "ДробДесят" ], "type": "double" },                                { "name": "simplePart", "aliases": [ "ДробПрост" ], "namespace": "РазмерДоли.ДоляУстКап.УчрРФСубМО.СвУчредит.СвЮЛ",                                  "type": { "type": "record", "name": "ДробПрост.РазмерДоли.ДоляУстКап.УчрРФСубМО.СвУчредит.СвЮЛ",                                    "fields": [                                      { "name": "num", "aliases": [ "Числит" ], "type": "long" },                                      { "name": "denom", "aliases": [ "Знаменат" ], "type": "long" }                                    ]                                  }                                }                              ]                            }                          }                        ]                      }                    },                    { "name": "founderImplUL", "aliases": [ "СвОргОсущПр" ], "namespace": "УчрРФСубМО.СвУчредит.СвЮЛ",                      "type": { "type": "array",                        "items": [                          { "name": "СвОргОсущПр.УчрРФСубМО.СвУчредит.СвЮЛ", "type": "record",                            "fields": [                              { "name": "egrulData", "aliases": [ "НаимИННЮЛ" ], "namespace": "СвОргОсущПр.УчрРФСубМО.СвУчредит.СвЮЛ",                                "type": { "type": "record", "name": "НаимИННЮЛ.СвОргОсущПр.УчрРФСубМО.СвУчредит.СвЮЛ",                                  "fields": [                                    { "name": "ogrn", "aliases": [ "ОГРН" ], "type": "string" },                                    { "name": "inn", "aliases": [ "ИНН" ], "type": "string" },                                    { "name": "fullName", "aliases": [ "НаимЮЛПолн" ], "type": "string" }                                  ]                                }                              }                            ]                          }                        ]                      }                    },                    { "name": "founderImplFL", "aliases": [ "СвФЛОсущПр" ], "namespace": "УчрРФСубМО.СвУчредит.СвЮЛ",                      "type": { "type": "array",                        "items": [                          { "name": "СвФЛОсущПр.УчрРФСубМО.СвУчредит.СвЮЛ", "type": "record",                            "fields": [                              { "name": "fl", "aliases": [ "СвФЛ" ], "namespace": "СвФЛОсущПр.УчрРФСубМО.СвУчредит.СвЮЛ",                                "type": { "type": "record", "name": "СвФЛ.СвФЛОсущПр.УчрРФСубМО.СвУчредит.СвЮЛ",                                  "fields": [                                    { "name": "lastName", "aliases": [ "Фамилия" ], "type": "string" },                                    { "name": "firstName", "aliases": [ "Имя" ], "type": "string" },                                    { "name": "patronymic", "aliases": [ "Отчество" ], "type": "string" },                                    { "name": "inn", "aliases": [ "ИННФЛ" ], "type": "string" }                                  ]                                }                              }                            ]                          }                        ]                      }                    }                  ]                }              ]            }          },          { "name": "founderPIF", "aliases": [ "УчрПИФ" ], "namespace": "СвУчредит.СвЮЛ",            "type": { "type": "array",              "items": [                { "name": "УчрПИФ.СвУчредит.СвЮЛ", "type": "record",                  "fields": [                    { "name": "PIFName", "aliases": [ "СвНаимПИФ" ], "namespace": "УчрПИФ.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "СвНаимПИФ.УчрПИФ.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "name", "aliases": [ "НаимПИФ" ], "type": "string" }                        ]                      }                    },                    { "name": "manageOrg", "aliases": [ "СвУпрКомпПИФ" ], "namespace": "УчрПИФ.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "СвУпрКомпПИФ.УчрПИФ.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "egrulData", "aliases": [ "УпрКомпПиф" ], "namespace": "УчрПИФ.СвУчредит.СвЮЛ",                            "type": { "type": "record", "name": "УпрКомпПиф.УчрПИФ.СвУчредит.СвЮЛ",                              "fields": [                                { "name": "ogrn", "aliases": [ "ОГРН" ], "type": "string" },                                { "name": "inn", "aliases": [ "ИНН" ], "type": "string" },                                { "name": "fullName", "aliases": [ "НаимЮЛПолн" ], "type": "string" }                              ]                            }                          }                        ]                      }                    },                    { "name": "capitalPart", "aliases": [ "ДоляУстКап" ], "namespace": "УчрПИФ.СвУчредит.СвЮЛ",                      "type": { "type": "record", "name": "ДоляУстКап.УчрПИФ.СвУчредит.СвЮЛ",                        "fields": [                          { "name": "nominal", "aliases": [ "НоминСтоим" ], "type": "double" },                          { "name": "size", "aliases": [ "РазмерДоли" ], "namespace": "ДоляУстКап.УчрПИФ.СвУчредит.СвЮЛ",                            "type": { "type": "record", "name": "РазмерДоли.ДоляУстКап.УчрПИФ.СвУчредит.СвЮЛ",                              "fields": [                                { "name": "percent", "aliases": [ "Процент" ], "type": "double" },                                { "name": "decimalPart", "aliases": [ "ДробДесят" ], "type": "double" },                                { "name": "simplePart", "aliases": [ "ДробПрост" ], "namespace": "РазмерДоли.ДоляУстКап.УчрПИФ.СвУчредит.СвЮЛ",                                  "type": { "type": "record", "name": "ДробПрост.РазмерДоли.ДоляУстКап.УчрПИФ.СвУчредит.СвЮЛ",                                    "fields": [                                      { "name": "num", "aliases": [ "Числит" ], "type": "long" },                                      { "name": "denom", "aliases": [ "Знаменат" ], "type": "long" }                                    ]                                  }                                }                              ]                            }                          }                        ]                      }                    }                  ]                }              ]            }          }        ]      }    },    { "name": "capitalPart", "aliases": [ "СвДоляООО" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвДоляООО.СвЮЛ",        "fields": [          { "name": "nominal", "aliases": [ "НоминСтоим" ], "type": "double" },          { "name": "size", "aliases": [ "РазмерДоли" ], "namespace": "СвДоляООО.СвЮЛ",            "type": { "type": "record", "name": "РазмерДоли.СвДоляООО.СвЮЛ",              "fields": [                { "name": "percent", "aliases": [ "Процент" ], "type": "double" },                { "name": "decimalPart", "aliases": [ "ДробДесят" ], "type": "double" },                { "name": "simplePart", "aliases": [ "ДробПрост" ], "namespace": "РазмерДоли.СвДоляООО.СвЮЛ",                  "type": { "type": "record", "name": "ДробПрост.РазмерДоли.СвДоляООО.СвЮЛ",                    "fields": [                      { "name": "num", "aliases": [ "Числит" ], "type": "long" },                      { "name": "denom", "aliases": [ "Знаменат" ], "type": "long" }                    ]                  }                }              ]            }          }        ]      }    },    { "name": "holderReestrAO", "aliases": [ "СвДержРеестрАО" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвДержРеестрАО.СвЮЛ",        "fields": [          { "name": "egrulData", "aliases": [ "ДержРеестрАО" ], "namespace": "СвДержРеестрАО.СвЮЛ",            "type": { "type": "record", "name": "ДержРеестрАО.СвДержРеестрАО.СвЮЛ",              "fields": [                { "name": "ogrn", "aliases": [ "ОГРН" ], "type": "string" },                { "name": "inn", "aliases": [ "ИНН" ], "type": "string" },                { "name": "fullName", "aliases": [ "НаимЮЛПолн" ], "type": "string" }              ]            }          }        ]      }    },    { "name": "okved", "aliases": [ "СвОКВЭД" ], "namespace": "СвЮЛ",      "type": { "type": "record", "name": "СвОКВЭД.СвЮЛ",        "fields": [          { "name": "mainOkved", "aliases": [ "СвОКВЭДОсн" ], "namespace": "СвОКВЭД.СвЮЛ",            "type": { "type": "record", "name": "СвОКВЭДОсн.СвОКВЭД.СвЮЛ",              "fields": [                { "name": "code", "aliases": [ "КодОКВЭД" ], "type": "string" },                { "name": "name", "aliases": [ "НаимОКВЭД" ], "type": "string" }              ]            }          },          { "name": "addOkved", "aliases": [ "СвОКВЭДДоп" ], "namespace": "СвОКВЭД.СвЮЛ",            "type": { "type": "array",              "items": [                { "name": "СвОКВЭДДоп.СвОКВЭД.СвЮЛ", "type": "record",                  "fields": [                    { "name": "code", "aliases": [ "КодОКВЭД" ], "type": "string" },                    { "name": "name", "aliases": [ "НаимОКВЭД" ], "type": "string" }                  ]                }              ]            }          }        ]      }    }  ]}

Разработка схемы AVRO


Схема должна начинаться с того элемента, содержимое которого должно попасть в JSON. Но не сам этот элемент. В данном случае это СвЮЛ.


Если в исходном XML этот элемент встречается несколько раз, то результирующий JSON будет представлять массив. Каждый элемент этого массива соответствует содержимому корневого элемента схемы AVRO


Необходимо указать тип элемента record, и описать составляющие его элементы в блоке fields.


{ "type": "record", "name": "СвЮЛ",  "fields": []}

Примитивные элементы


Примитивные элементы это атрибуты элемента и вложенные элементы без атрибутов. Для них указывается наименование name, псевдоним aliases и тип type.

{ "name": "reportDate", "aliases": [ "ДатаВып" ], "type": "string" }

Наименование элемента указывает его имя в результирующем JSON. Наименование долно состоять только из букв. Псевдоним его имя в исходном XML. Псевдоним можно не использовать, тогда имена элементов в XML и JSON будут совпадать. Тип элемента в схеме AVRO может быть примитивным или логическим. Однако с ходу добиться работы логических типов в NiFi мне не удалось. Валидатор не пропускал такую схему.

Сложные элементы


Сложные элементы описываются типом record. Описание типа должно быть вложенным.

{ "name": "name", "aliases": [ "СвНаимЮЛ" ], "namespace": "СвЮЛ",  "type": { "type": "record", "name": "СвНаимЮЛ.СвЮЛ",    "fields": [      { "name": "fullName", "aliases": [ "НаимЮЛПолн" ], "type": "string" },      { "name": "shortName", "aliases": [ "НаимЮЛСокр" ], "type": "string" }    ]  }}

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


В описании типа указывается его имя name. Я в качестве имени типа использовал, скажем так, пространство имен. Этим обеспечивается уникальность наименования типа.


Блок fields содержит описание атрибутов и вложенных элементов. Они в свою очередь могут быть как примитивного так и составного типа.


Массивы


Массивы описываются типом array. Описание типа должно быть вложенным. Блок items должен содержать описание элемента массива. В name указывается путь к элементу XML, содержащему элемент массива.

{ "name": "addOkved", "aliases": [ "СвОКВЭДДоп" ], "namespace": "СвОКВЭД.СвЮЛ",  "type": { "type": "array",    "items": [      { "name": "СвОКВЭДДоп.СвОКВЭД.СвЮЛ", "type": "record",        "fields": [          { "name": "code", "aliases": [ "КодОКВЭД" ], "type": "string" },          { "name": "name", "aliases": [ "НаимОКВЭД" ], "type": "string" }        ]      }    ]  }}

Стоит обратить внимание, что здесь описание типа record не должно быть вложенным.

Настройка процессоров NiFi


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


В процессоре ConvertRecord необходимо указать контроллеры для чтения данных из XML и записи данных в JSON.

Настройки XMLReader




  • Schema Access Strategy = Use 'Schema Name' Property поиск схемы по наименованию
  • Schema Registry контроллер AvroSchemaRegistry, в котором зарегистрирована схема AVRO
  • Schema Name атрибут в FlowFile, который содержит наименование схемы. Я использовал атрибут scheme.name. Установка атрибута в FlowFile выполняется процессором UpdateAttribute. Хотя можно использовать и другой атрибут, который присутствует в FlowFile и позволяет точно выбрать схему AVRO
  • Expect Records as Array = true в XML ожидается массив искомых элементов
  • Field Name for Content = EGRUL имя элемента в XML, внутри которого содержится структура описываемая в схеме AVRO

Настройки JsonRecordSetWriter


  • Schema Write Strategy записывать ли схему AVRO в FlowFile
  • Schema Access Strategy = Use 'Schema Name' Property поиск схемы по наименованию
  • Schema Registry контроллер AvroSchemaRegistry, в котором зарегистрирована схема AVRO
  • Schema Name атрибут в FlowFile, который содержит наименование схемы
  • Pretty Print JSON выполнять ли форматирование JSON
  • Suppress Null Values что делать с пустыми элементами
  • Output Grouping способ группировки записей в JSON

На выходе процессор ConvertRecord создаст FlowFile, который содержит результирующий JSON.


Далее...


Далее каждый JSON можно разбить по организациям и, навести красоту убрать избыточные уровни иерархии, объединить некоторые поля (например ФИО, поля адреса).


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

Подробнее..

Импорт ЕГРЮЛ ФНС средствами 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 следует куда-то разместить для хранения и дальнейшего использования. Но это выходит за рамки повествования. Тут уж кому что удобно.

Подробнее..

Из песочницы Побег от скуки процессы ETL

28.06.2020 18:15:43 | Автор: admin

В конце зимы и начале весны, появилась возможность поработать с новым для меня инструментом потоковой доставки данных Apache NiFi. При изучении инструмента, все время не покидало ощущение, что помимо официальной документации, нелишним были бы материалы "for dummies", с практическими примерами.


После выполнении задачи, решил попробовать облегчить вхождение в мир NiFi.


Предыстория, почти не связанная со статьей


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


Система Apache NiFi была выбрана на удачу. Прототип был построен и сдан заказчику.


Первоначально заказчик хотел монолитное приложение, а использование NiFi рассматривал просто как инструмент прототипирование (где-то прочитал). Но после знакомства в вблизи NiFi остался в продукте.


А теперь собственно история


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


Когда я начал разбираться с Apache NiFi выяснилось, что более-менее подробная информация есть только на сайте проекта. Русские статьи во многом просто переводы вводных частей официальной документации. Основной недостаток часто не понятно в каком формате параметры вводить. Гугл спасает, но не всегда.


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


И так, задача получать данные о распространении вируса, обрабатывать строить графики.
Источниками данных будет сайты стопкороновирус.рф и covid19.who.int. Первый сайт содержит данные по регионам России, сайт ВОЗ данные по странам мира.


Данные на сайте стопкороновирус.рф находятся прямо в html-коде страницы в виде почти готового json-массива. Нужно его только обработать. Но об этом в следующей статье.
Данные ВОЗ находятся в csv-файле, который не получилось автоматически скачивать (либо я был недостаточно настойчив). Поэтому, когда нужно было, файл сохранялся из браузера в специальную папку.


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


Каждый процессор может находиться в состоянии "Running" или "Stopped". Данные накапливаются в очереди перед остановленным процессором, после старта процессора, данные будут переданы в процессор. Очень удобно, можно изменять параметры процессора, без потери данных на работающем процессе.


Данные в системе называются FlowFile, потоковый файл. Процесс начинает работать с процессора, который умеет генерировать FlowFile. Не каждый процессор умеет генерировать потоковый файл в этом случае в начало ставится процессор "GenerateFlowFile", задача которого создать пустой потоковый файл и тем самым запустить процесс. Файл может создаваться по расписанию или событию. Файл может быть бинарным или текстовым потоком.


Файл состоит из контента и метаданных. Метаданные называются атрибутами. Например, атрибутом является имя файл, id-файла, имя схемы и т.д. Система позволяет добавлять собственные атрибуты, записывать значения. Значения атрибутов используются в работе процессоров.


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


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


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


Итак практическое применение вольного изложения документации чтение данных ВОЗ и сохранение в БД. Данные будем хранить в СУБД PostgreSQL.


Создаем таблицу:


CREATE TABLE public.who_outbreak (    dt timestamp NULL,    country_code varchar NULL,    country varchar NULL,    region_code varchar NULL,    died int4 NULL,    died_delta int4 NULL,    infected int4 NULL,    infected_delta int4 NULL);

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


Источником данных будет cvs-файл с сайта https://covid19.who.int/ по кнопке Download Map Data. Файл содержит информацию по заболевшим и погибшим по всем странам на каждый день примерно с конца января. Оперативная информация там задерживается на 1-2 дня. За это время в файле менялись наименования полей (были даже наименования с пробелами), менялся формат даты.


Файл сохраняется из браузера в определенный каталог, откуда NiFi забирает его на обработку.


image
Общий вид визуализации процесса в интерфейсе Apache NiFI


На рисунке большие прямоугольники процессы, прямоугольники поменьше очереди между процессами.


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


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


Визуализация очереди показывает ее имя, и объем данных в очереди. По умолчанию имя очереди это тип связи success, failed и другие.


Тип первого процесса "GetFile". Этот процессор создает flowfile и запускает процесс. Контентом потокового файла будет содержимое файла если он будет найден. В настройках процессора на вкладке Scheduling указываем расписание запуска процесса 20 секунд.



Вкладка Scheduling запуск каждые 20 сек.


После старта процессора, каждые 20 секунд процессор будет запускаться. Если файл будет найден FlowFile будет создан и процесс запустится.


Как видно из рисунка, указываем каталог и имя файла. В имени файла можно использовать символы подстановки. Например *.csv приведет к обработке всех csv-файлов в каталоге. Указываем также, что после обработки файл можно удалить ("Keep Source File"). Также есть возможность указать максимальные и минимальные значения возраста и размера файла. Это позволяет обрабатывать, например, только не пустые файл, созданные за последний час.
На вкладке Settings указываются базовые параметры процесса, такие как имя процесса, максимальное время работы процесса, время между запусками, типы связей.


Результатом работы первого процесса "GetFile" с именем "Read WHO datafile" будет просто поток данных из файла. Поток будет передан в следующий процесс "ReplaceText".



Процессор поиска подстроки


В этом процессе обратим внимание сразу на вкладку параметров. Данный процессор ищет regex-выражение "Search Value" в входном потоке и заменяет на новое значение "Replacement Value". Обработка ведется построчно ("Evaluation Mode"). В данном случае идет замена в строке даты. Во входном файле, в какой-то момент дата формата YYYY-MM-DD стала указываться как YYYY-MM-DDThh:mm:ssZ, причем время было всегда 00:00:00, а временная зона не указывалась.
Простого способа преобразования в даты уже в записи не нашел, поэтому к проблеме подошел в лоб просто через процессор "ReplaceText" убрал символы T и Z. После этого строка стала конвертироваться в timestamp в avro-схеме без ошибок.


На выходе процессора будет поток текстовых данных, в которых уже поправили подстроку даты. Но пока это просто поток байтов без какой-то структуры.


Следующий процессор "Rename fields" читает поток уже как структурированные данные.



Переименование полей


Процессор содержит ссылку на Reader специальный объект-контроллер, который умеет читать из потока структурированные данные и в таком виде уже передает процессору на обработку. В данном случае "WHO CVS Reader просто читает поток и преобразует каждую строку cvs-файла в запись (record) которая содержит поля со значениями из строки. Имена полей берутся из заголовка cvs-файла.



Контроллер чтения записей из cvs-файла


Параметр "Schema Access Strategy" указывают, что структура записи формируется из заголовка cvs-файла. Если заголовков нет, то можно изменить стратегию доступа к схеме и в реестре схем данных создать схему, указать ее имя в параметре "Schema Name" или еще проще указать саму схему в параметре "Schema Text".


Но так как у нас есть заголовки в файле читаем по ним.


Итак, в процессоре данные уже структурированные их можно представить как таблицу базы данных поля и строки. И к этой таблице мы выполняем SQL запрос :


select    Date_reported dt,    Country_code country_code,    Country country,    WHO_region region_code,    New_deaths died_delta,    Cumulative_deaths died,    New_cases infected_delta,    Cumulative_cases infectedfrom FLOWFILE

Поля в запросе такие же как заголовки cvs-файла. Имя таблицы служебное FLOWFILE обозначает чтение структурированных данных их контента файла. Язык запроса SQL довольно гибкий, есть функции преобразований, агрегаций и т.д. В данном случае запрос выводит все данные, только имена полей результата будут другие они соответствуют полям таблицы who_outbreak в целевой БД.


Поток записей с новыми именами полей передается в контроллер RecordSetWriter, ссылка на который также указана в параметра контроллера "WHO AvroRecordSetWriter".



Контроллер RecordSetWriter


Контролер RecordSetWriter уже использует предопределенную схему данных. Схема находится в отдельном объекте регистре схем ("Schema Registry"). В контроллере есть только ссылка на реестр схем и имя схемы.



Регистр схем


Работать с регистром схем довольно просто. Добавляем новый параметр. Его имя будет именем схемы. Значение параметр определение схемы.


В регистре схем создана схема who_outbreak, определение схемы:


{"type" : "record","name" : "who_outbreak","fields": [  {"name": "dt", "type": { "type" : "long", "logicalType": "timestamp-millis"}},  {"name": "country_code", "type" : ["null", "string"], "default": "-"},  {"name": "country", "type" : ["null", "string"], "default": ""},  {"name": "region_code", "type" : ["null", "string"], "default": ""},  {"name" : "died", "type" : "int", "default": 0},  {"name" : "died_delta", "type" : "int", "default": 0},  {"name" : "infected", "type" : "int", "default": 0},  {"name" : "infected_delta", "type" : "int", "default": 0} ]}

Имена и типы атрибутов схемы соответствуют именам и типам полей записи, сформированной sql-запросом.


После выполнения контроллером sql-запроса и передачи данных на выход контроллера в формате схемы данных, структурированный поток передается в контроллер "Delete all records". Это контроллер типа "PutSQL", который может передавать на выполнение sql-команды.


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



delete from who_outbreak;


В параметрах контроллера указываем SQL Statement delete from who_outbreak; и ссылку на пул соединений "JDBC Connection Pool". Параметры JDBC стандартные. Пул содержит настройки подключения к конкретной БД, поэтому его можно использовать во всех контроллерах, которые будут работать с этой БД.


Данные или атрибуты FlowFile не обрабатываются в процессоре, поэтому вход и выход процессора идентичен.


Последний процессор "PutDatabaseRecord".



Запись в БД


В этом процессоре указываем Reader, в котором используется определенная схема who_outbreak. Так как мы удалили все записи в предыдущем процессоре, используем простой INSERT для добавления записей в таблицу. Указываем пул соединений DBCPConnectionPool, далее указываем БД и имя таблицы. Имена полей в схеме данных и БД совпадают, то больше никакой дополнительной настройки проводить не нужно.


Все процессоры, контроллеры и регистры схем нужно перевести в состояние Running (Start).
Процесс доставки данных готов. Если положить файл WHO-COVID-19-global-data.csv в каталог D:\input, то в течении 20 секунд он будет удален, а данные пройдя через процесс доставки данных будут сохранены в БД.


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


На рисунке изображение в интерфейсе Apache NiFi описанного процесса (справа) и, для затравки, процесса для второй статьи (слева).


Подробнее..
Категории: Big data , Bigdata , Apache nifi , Etl

Автоматизация аналитики Jira средствами Apache NiFi

12.11.2020 00:07:35 | Автор: admin
Приветствую, господа. Я Маша, мне 23, и я уже полгода изучаю и внедряю на практике Apache NiFi.

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

В тот час, когда технически Apache NiFi мощное связующее звено между различными сервисами (осуществляет обмен данными между ними, по пути позволяя их обогащать и модифицировать), смотрю я на него с точки зрения аналитика. А все потому, что NiFi весьма удобный инструмент для ETL. В часности, в команде мы ориентируемся на построение им SaaS архитектуры.

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

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

Концепция Apache NiFi кратко.


Apache NiFi opensource продукт для автоматизации и управления потоками данных между системами. Приступая к нему важно сразу осознать две вещи.

Первое это зона Low Code. Что я имею ввиду? Предполагается, что все манипуляции с данными с момента их попадания в NiFi вплоть до извлечения можно выполнить его стандартными инструментами (процессорами). Для особых случаев существует процессор для запуска скриптов из bash-а.

Это говорит о том, что сделать что-то в NiFi неправильно довольно сложно (но мне удалось! об этом второй пункт). Сложно потому, что любой процессор будет прямо таки пинать тебя А куда отправлять ошибки? А что с ними делать? А сколько ждать? А тут ты выделил мне маловато места! А ты докумментацию точно внимательно читал? и т.д.

image

Второе (ключевое) это концепция потокового программирования, и только. Тут, я лично, не сразу врубилась (прошу, не судите). Имея опыт функционального программирования на R, я неосознанно формировала функции и в NiFi. В конечном счете переделывай сказали мне коллеги, когда увидели мои тщетные попытки эти функции подружить.

Думаю, хватит на сегодня теории, лучше узнаем все из практики. Давайте сформулируем подобие ТЗ для недельной аналитики Jira.

  • Достать из жиры ворклог и историю изменений за неделю.
  • Вывести базовую статистику за этот период и дать ответ на вопрос: чем же занималась команда?
  • Отправить отчет боссу и коллегам.

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

Давайте же разбираться.

Первые шаги. Забор данных из API


В Apache NiFi нету такого понятия как отдельный проект. У нас есть только общее рабочее пространство и возможность формирования в нем групп процессов. Этого вполне достаточно.

Находим в панели инструментов Process Group и создаем группу Jira_report.



Идем в группу и начинаем строить поток (workflow). Большинство процессоров из которых его можно собрать требуют Upstream Connection. Простыми словами это триггер, по которому процессор будет срабатывать. Потому логично, что и весь поток будет начинаться с обычного триггера в NiFi это процессор GenerateFlowFile.

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

Контент обычный файл, набор байтов. Представьте что контент это аттач к FlowFile.

Делаем Add Processor GenerateFlowFile. В настройках, в первую очередь, настоятельно рекомендую задать имя процессора (это хороший тон) вкладка Settings. Еще момент: по умолчанию GenerateFlowFile генерит потоковые файлы непрерывно. Вряд ли это вам когда-нибуть понадобится. Сразу увеличиваем Run Schedule, к примеру до 60 sec вкладка Scheduling.



Также на вкладке Properties укажем дату начала отчетного периода атрибут report_from со значением в формате yyyy/mm/dd.

Согласно документации Jira API, у нас есть ограничение на выгрузку issues не больше 1000. Потому, чтобы получить все таски, мы должны будем сформировать JQL запрос, в котором указываются параметры пагинации: startAt и maxResults.

Зададим их атрибутами с помощью процессора UpdateAttribute. Заодно прикрутим и дату генерации отчета. Она понадобится нам позже.





Вы наверняка обратили внимание на атрибут actual_date. Его значение задано с помощью Expression Language. Ловите крутую шпаргалку по нему.

Все, можем формировать JQL к жире укажем параметры пагинации и нужные поля. В последующем он будет телом HTTP запроса, следовательно, отправим его в контент. Для этого используем процессор ReplaceText и укажем его Replacement Value примерно таким:

{"startAt": ${startAt}, "maxResults": ${maxResults}, "jql": "updated >= '2020/11/02'", "fields":["summary", "project", "issuetype", "timespent", "priority", "created", "resolutiondate",  "status", "customfield_10100", "aggregatetimespent", "timeoriginalestimate", "description", "assignee", "parent", "components"]}


Обратите внимание как прописываются ссылки на атрибуты.

Поздравляю, мы готовы делать HTTP запрос. Тут впору будет процессор InvokeHTTP. Кстати он может по всякому Я имею ввиду методы GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Модифицируем его свойства следующим образом:

HTTP Method у нас POST.

Remote URL нашей жиры включает IP, порт и приставочку /rest/api/2/search?jql=.

Basic Authentication Username и Basic Authentication Password это креды к жире.

Меняем Content-Type на application/json b ставим true в Send Message Body, что значит переслать JSON, который прийдет из предыдущего процессора в теле запроса.

APPLY.



Ответом апишки будет JSON файл, который попадет в контент. В нем нам интересны две вещи: поле total cодержащее общее количество тасок в системе и массив issues, в котором уже лежит часть из них. Распарсим же ответочку и познакомимся с процессором EvaluateJsonPath.

В случае, когда JsonPath указывает на один обьект, результат парсинга будет записан в атрибут флоу файла. Тут пример поле total и следующий скрин.



В случае же, когда JsonPath указывает на массив обьектов, в результате парсинга флоу файл будет разбит на множество с контентом соответствующим каждому обьекту. Тут пример поле issue. Ставим еще один EvaluateJsonPath и прописываем: Property issue, Value $.issue.

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

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

Для этого увеличим номер стартовой таски на maxResults. Снова заюзаем UpdateAttribute: укажем атрибут startAt и пропишем ему новое значение ${startAt:plus(${maxResults})}.

Ну и без проверки на достижение максимума тасок не обойдемся процессор RouteOnAttribute. Настройки следующие:



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



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

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

Галопом по Европам. Выгрузка ворклога и др.


Ну, что, ускоримся. Как говорится, найдите отличия:



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



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

Нам удобно будет оформить worklog и changelog по всем таскам в виде отдельных документов. Поэтому, воспользуемся процессором MergeContent и склеим им содержимое всех флоу файлов.

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

Заключительный этап. Генерация отчета и отправка по Email


Окей. Тасочки все выгрузились и отправились двумя путями: в группу для выгрузки ворклога и к скрипту для генерации отчета. К последнему у нас STDIN один, поэтому нам необходимо собрать все задачи в одну кучу. Сделаем это в MergeContent, но перед этим чуть подправим контент, чтобы итоговый json получился корректным.



Перед квадратиком генерации скрипта (ExecuteStreamCommand) присутствует интересный процессор Wait. Он ожидает сигнала от процессора Notify, который находиться в группе выгрузки ворклога, о том что там уже все готово и можно идти дальше. Дальше запускаем скрипт из bash-a ExecuteStreamCommand. Ии отправляем отчетик с помощью PutEmail всей комманде.

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

Кратко скажу, что разработанная нами отчетность дает стратегическое представление о том чем занимается подразделение или команда. А это бесценно для любого босса, согласитесь.

Послесловие


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

Apache NiFi не упрощает процесс разработки, он упрощает процесс эксплуатации. Мы можем в любой момент остановить любой поток, внести правку и запустить заново.

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

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

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



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


Гениальная статья, которая прямо на пальчиках и по буковкам освещает что такое Apache NiFi.

Краткое руководство на русском языке.

Крутая шпаргалка по Expression Language.

Англоязычное комьюнити Apache NiFi открыто к вопросам.

Русскоязычное сообщество Apache NiFi в Telegram живее всех живых, заходите.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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