skriptorium 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1211 @@
1
+ [
2
+ {
3
+ "type_of": "article",
4
+ "id": 4198697,
5
+ "title": "Syncerman: от bash-скриптов к Go cli для пяти облаков одновременно",
6
+ "description": "Иногда, чтобы решить свою проблему, приходится написать инструмент. А иногда — чтобы написать...",
7
+ "published": true,
8
+ "published_at": "2026-07-21T15:46:22.000Z",
9
+ "slug": "syncerman-ot-bash-skriptov-k-go-cli-dlia-piati-oblakov-odnovriemienno-hn7",
10
+ "path": "/kinnalru/syncerman-ot-bash-skriptov-k-go-cli-dlia-piati-oblakov-odnovriemienno-hn7",
11
+ "url": "https://dev.to/kinnalru/syncerman-ot-bash-skriptov-k-go-cli-dlia-piati-oblakov-odnovriemienno-hn7",
12
+ "comments_count": 0,
13
+ "public_reactions_count": 0,
14
+ "page_views_count": 0,
15
+ "published_timestamp": "2026-07-21T16:24:04Z",
16
+ "body_markdown": "---\ntitle: Syncerman: от bash-скриптов к Go cli для пяти облаков одновременно\npublished: true\ndate: 2026-07-21 15:46:22 UTC\ntags: \ncanonical_url: https://teletype.in/@jerry_ru/NpCFHTA1uZQ?utm_source=teletype&utm_medium=feed_rss&utm_campaign=jerry_ru\n---\n\n![](https://img3.teletype.in/files/a3/78/a378884e-5416-4669-b2fb-a001bce9d848.png)\n\nИногда, чтобы решить свою проблему, приходится написать инструмент. А иногда — чтобы написать инструмент, приходится решить свою проблему. Короче, как обычно, всё началось не с идеи сделать утилиту, а с того, что добавилась пара важных документов (по иппотке) к существующему зоопарку данных, которые надо хранить.\n\n### Как я к этому пришел\n\nУ меня давно назрела проблема: синхронизировать разные файлы с разных рабочих мест — по крайней мере дома и на работе. Документы (важные), сохранения из игр, книги для читалки. Ну и надёжный архив всего этого добра, потому что терять даже что-то незначительное это боль 🤕.\n\nПросто положить всё в одно облако — хорошо, но нужен ещё постоянный процесс актуализации. `Яндекс Диск` работает, `Google Drive` может изменить условия, `Dropbox` может внезапно ограничить доступ. Поэтому идея была простая: **хранить копии везде**. `Яндекс Диск`, `Google Drive`, `Mail.ru Cloud`, `Dropbox` — чем больше, тем надёжнее. Только файлы, только хардкор!\n\nРаньше я обходился консольным демоном `yandex-disk` — он работал, но только с Яндексом. Потом перешёл на самописные bash-скрипты, которые вызывали [rclone](https://rclone.org/) напрямую. Это работало… до поры до времени. Конфигурация разрасталась, порядок синхронизации был важен (нельзя синкать из пустого облака в другое до того, как туда прилетели файлы) и всякое подобное. Проблема!\n\nКогда появились нормальные ИИ-ассистенты для кодинга, я решил, что пора пробовать, а мне нужен нормальный инструмент. Declarative configuration, predictable order, automatic error handling и пр. Так родился [Syncerman](https://gitlab.com/kinnalru/syncerman).\n\n### ~~Из говна и палок~~ архитектура и технологии\n\n`Syncerman` написан на **Go** — выбрал его только за простоту распространения (один бинарник).\n\nВ основе лежит [rclone bisync](https://rclone.org/bisync/) — двусторонняя синхронизация с сохранением метаданных. `Syncerman` не пытается переизобрести колесо синхронизации, а выступает оркестратором над `rclone`, предоставляя удобный CLI и YAML-конфигурацию. Никаких серверов и БД.\n\n### YAML-конфигурация\n\nВместо папок с ссылками на bash-скрипты — чёткая структура:\n\n```yaml\njobs:\n important:\n name: \"Важное\"\n priority: 10\n tasks:\n - from: \"local:/home/jerry/cloud/mirror/Documents\"\n to:\n - path: \"gdrive:folders/Documents\"\n - path: \"ydisk:folders/Documents\"\n - from: local:Наследие\n to:\n - path: gdrive:Наследие/\n args: [\"--force\"]\n - path: ydisk:Наследие/\n args: [\"--force\"]\n - path: mailru:Наследие/\n args: [\"--force\"]\n\n games:\n name: \"игрули\"\n priority: 20\n tasks:\n - from: \"local:games_sync\"\n to:\n - path: \"ydisk:folders/games_sync\"\n```\n\nПорядок **tasks** и **to** внутри массивов сохраняется строго. Это критично для цепочек: `local → Google Drive → Yandex Disk`. Без этого — катастрофа.\n\n### Проблемасики\n\n`rclone bisync` требует state-файлы для работы (они лежат где-то в папке пользователя). При первом запуске на новой паре путей он падает с ошибкой _\"cannot find prior Path1 or Path2 listings\"_. Syncerman детектирует это по двум паттернам в выводе и автоматически перезапускает синхронизацию с флагом `--resync`. Пользователь даже не замечает проблемы. Раньше делал руками.\n\nНа ранних этапах обнаружился критический баг: из-за использования map в Go порядок выполнения целей был недетерминированным. Для линейных цепочек это катастрофа: если сначала синкать из пустого `Yandex` в `Google`, а потом из локальной папки в `Google` — быть беде. Проблема! Так появились массивы.\n\n### Результат\n\n[Syncerman](https://gitlab.com/kinnalru/syncerman) версии \\*\\*0.3.6\\*\\* сейчас работает в production — то есть на моих машинах :) Само собой нужны пруфы, и их есть у меня!\n\n- бинарники для Linux и Windows (сборка сразу подо всё)\n- GitLab CI как в лучших домах\n- Покрытие тестами - самому было бы лень писать а тут юнит-тесты для всех ключевых пакетов (config, rclone, sync), интеграционные тесты (не просто интеграционные а целые **bash-сценарии с десятком шагов** ).\n\nПубличные репозитории (это мой фетиш):\n\n- Gitlab [https://gitlab.com/kinnalru/syncerman](https://gitlab.com/kinnalru/syncerman)\n- Github [https://github.com/kinnalru/syncerman](https://github.com/kinnalru/syncerman)\n- Gitflic [https://gitflic.ru/project/kinnalru/syncerman](https://gitflic.ru/project/kinnalru/syncerman)\n- Gitverse [https://gitverse.ru/kinnalru/syncerman](https://gitverse.ru/kinnalru/syncerman)\n\n### Использование\n\nМой `crontab` выглядит так:\n\n```shell\n# Синхронизация документов и сохранений 2 рааз в час и лог для ручной проверки\n10,30 * * * * /bin/sh -lc \"cd /home/jerry/cloud/mirror && syncerman sync 2>&1 | tee ./lastrun.log\"\n```\n\nКонфигурация зеркалирует локальные папки во все облака (настроенные в `rclone`) одновременно, а затем синхронизирует облака между собой. Если одно облако отвалится — данные остаются в остальных. Если `rclone` выдаст `first-run` ошибку — `Syncerman` сам всё починит.\n\n### Вместо вывода\n\nПоявление ИИ-ассистентов снизило порог входа для написания таких инструментов. Раньше мне было лень писать то, что и так работает с помощью пары строк на bash. А сейчас это делаю не я :) — разбор выхлопов `rclone`, обработка edge cases и тестирование делаются очень легко. Я могу заняться решением проблем (архитектурой и требованиями), а рутина \"делается сама\".\n\n* * *\n\nВторой фетиш это ссылочки:\n\n- [rclone bisync](https://rclone.org/bisync/) - это база\n- [unison](https://github.com/bcpierce00/unison) - еще одна классная штука по теме синхронизации, она прочно заняла место в моём сердце, но об это в другой раз",
17
+ "positive_reactions_count": 0,
18
+ "cover_image": null,
19
+ "tag_list": [],
20
+ "canonical_url": "https://teletype.in/@jerry_ru/NpCFHTA1uZQ?utm_source=teletype&utm_medium=feed_rss&utm_campaign=jerry_ru",
21
+ "reading_time_minutes": 1,
22
+ "user": {
23
+ "name": "Samoilenko Yuri",
24
+ "username": "kinnalru",
25
+ "twitter_username": null,
26
+ "github_username": "kinnalru",
27
+ "user_id": 559891,
28
+ "website_url": "https://teletype.in/@jerry_ru",
29
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
30
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
31
+ }
32
+ },
33
+ {
34
+ "type_of": "article",
35
+ "id": 2275835,
36
+ "title": "DevSecOps подкрался незаметно, хотя заметен был издалека…",
37
+ "description": "DevSecOps уверенно шагает по нашей индустрии, и горе тому, кто попадёт под его поступь… Эта статья...",
38
+ "published": true,
39
+ "published_at": "2025-02-11T12:34:47.000Z",
40
+ "slug": "devsecops-podkralsia-niezamietno-khotia-zamietien-byl-izdalieka-3gjj",
41
+ "path": "/rnds/devsecops-podkralsia-niezamietno-khotia-zamietien-byl-izdalieka-3gjj",
42
+ "url": "https://dev.to/rnds/devsecops-podkralsia-niezamietno-khotia-zamietien-byl-izdalieka-3gjj",
43
+ "comments_count": 0,
44
+ "public_reactions_count": 0,
45
+ "page_views_count": 20,
46
+ "published_timestamp": "2026-07-21T07:45:15Z",
47
+ "body_markdown": "---\ntitle: DevSecOps подкрался незаметно, хотя заметен был издалека…\npublished: true\ndate: 2025-02-11 12:34:47 UTC\ntags: \ncanonical_url: https://blog.rnds.pro/064-devsecops1?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n![](https://img1.teletype.in/files/42/f0/42f0e9b3-d3f8-403f-ac95-bfd789c92278.jpeg)\n\nDevSecOps уверенно шагает по нашей индустрии, и горе тому, кто попадёт под его поступь… Эта статья про ~~ультимативную~~ сборку базовых образов для Ruby для удовлетворения самых параноидальных потребностей ИБ. Да, именно об этом мы и расскажем - что такое \"инсталляция ruby\", где, что, почему лежит и как с этим жить нашему пайплайну сборки и самому приложению.\n\n<section style=\"background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);\">\n <p id=\"CaKt\">Статья подойдёт тем, кто хочет более глубоко понимать, какие процессы происходят в системе, когда вызывается <code>gem install</code>, <code>bundle install</code> или (не дай Бог) <code>gem update –system</code></p>\n </section>\n## Как было до?\n\nС самого начала появления docker мы в RNDSOFT использовали парадигму \"всё включено\". Для нас это означало, что в образ мы включаем всё, что нам надо не только для продуктовой эксплуатации, но и для проведения всех тестов. При прохождении CI пайплайна на следующие стадии продвигался образ целиком и, в конце концов, выкатывался на прод.\n\nЭто было очень удобно и позволяло быть максимально (насколько это вообще возможно) уверенным в работоспособности, однако имело ряд серьёзных минусов, с которыми мы прекрасно жили достаточно долгое время:\n\n- размер образа был большим - от 1 ГБ;\n- образ включал большое количество неиспользуемого (в проде) ПО.\n\n## Сканеры сканировали, сканировали, да не высканировали…\n\nИ вот однажды один (на данный момент уже далеко не один) наш клиент захотел устранения всех замечаний, которые смог выявить сканнер [Trivy](https://github.com/aquasecurity/trivy). А их, как не трудно догадаться, было достаточно много. И главная проблема заключается в том, что большая часть замечаний никак не связана с непосредственными зависимостями вашего приложения (теми, которые фиксируются в Gemfile.lock). \nТак откуда же они берутся?\n\nЕсли коротко отвечать - отовсюду :) При сборке проекта необходимо поставить его зависимости - это план минимум. Кроме того, часто появляется необходимость установить bundle определенной версии или даже обновить ruby целиком, выполнив команду [gem update --system](https://guides.rubygems.org/command-reference/#gem-update). В результате этих действий в ваш образ в разнообразные папки ставятся разнообразные гемы, но\n\n<section style=\"background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);\">\n <p id=\"gt1O\">старые версии гемов и сама базовая \"инсталляция ruby\" остаётся в системе со всеми своими \"устаревшими\" и \"уязвимыми\" версиями. И эти версии очень нравятся сканерам для того, чтобы поднять тревогу. </p>\n </section>\n\nИ еще два слова о том, что же именно сканирует Trivy (касательно ruby конечно):\n\n- сканирует [.gemspec файлы](https://guides.rubygems.org/specification-reference/) на всей файловой системе и не важно, установлен гем или нет - если в .gemspec указана версия \"с уязвимостью\" - алярм;\n- сканирует .gem файлы на всей файловой системе и не важно, установлен гем, лежит просто в папке cache, используется или нет в вашем приложении.\n\nЗначит для удовлетворения хотелок сканера, надо сделать так, чтоб нигде не лежало ничего лишнего или не используемого, **включая** [stdlib](https://docs.ruby-lang.org/en/3.2/standard_library_rdoc.html) - стандартную библиотеку ruby.\n\nПроблема ясна, задача очевидна - можно приступать к решению и начинать надо с базовых образов. Их надо собрать так, чтобы сами базовые образы уже не содержали никаких уязвимостей.\n\n![](https://img3.teletype.in/files/e1/7a/e17a3417-a64b-4027-adf2-971f332cf9dd.png)\n_Немного удручающее зрелище, особенно для ИБ клиента_\n\n## Что такое инсталляция ruby?\n\nЕсли мы возьмём любой дистрибутив с установленным там ruby, то увидим, что само ruby будет находиться где-то в районе:\n\n- /usr/lib/ruby - например в alpine после `apk add ruby`\n- /usr/local/lib/ruby - например в `ruby:alpine`, где руби собирается отдельно от apk\n\nА дальше начинается интересное. Если посмотреть в финальный образ, в котором сделано много различных операций (обновление ruby, установка гемов через `gem install`, установка через `bundle install`), то в корневой папке ruby обнаружится несколько папок, по которым тем или иным способом будут распределены установленные вами гемы:\n\n```plaintext\n/usr/local/lib/ruby\n├── 3.2.0\n├── gems/3.2.0\n├── site_ruby/3.2.0\n└── vendor_ruby/3.2.0\n```\n\nСразу добавим к этому списку другие папки (которые можно посмотреть в выводе команды `gem env`):\n\n```yaml\n - INSTALLATION DIRECTORY: /usr/local/bundle\n - USER INSTALLATION DIRECTORY: /root/.local/share/gem/ruby/3.3.0\n - SPEC CACHE DIRECTORY: /root/.cache/gem/specs\n - GEM PATHS:\n - /usr/lib/ruby/gems/3.3.0\n - /root/.local/share/gem/ruby/3.3.0\n```\n\nИ вишенкой на торте будет настройка вашего bundle, если вы используете кеширование сборки ([bundle cache](https://bundler.io/man/bundle-cache.1.html) или `bundle config set cache_path vendor/cache`), и используемые в этом зоопарке переменные окружения `GEM_HOME`, `BUNDLE_CACHE_PATH`, `BUNDLE_PATH` и скорее всего еще какие-то скрытые в глубинах экосистемы ruby.\n\nНемного путано и в результате беспорядочных ~~связей~~ установок и обновлений во всех этих папках могут (и будут) появляться гемы. Надо исправить!\n\nПостараюсь дать верхнеуровневое описание, что же именно это за папочки, не углубляясь в подробности и исключения:\n\n### /usr/local/lib/ruby/3.3.0\n\nИменно в этой папке установлены основные файлы ruby и stdlib, а также компилируемые расширения в папке x86\\_64-linux-musl (в нашем случае собранные alpine для x86\\_64). Эти файлы принадлежат условной \"системе\" и никак не будут изменяться при дальнейших модификациях, например при установке или обновлении гемов через `gem install`. Вместо этого библиотеки будут ставиться в папку `/usr/local/lib/ruby/gems/3.3.0`\n\n### /usr/local/lib/ruby/gems/3.3.0\n\nТут собрано всё, что вы ставите (без модификации `GEM_PATH`) командами `gem install,` включая компилируемые расширения.\n\n### /usr/local/lib/ruby/vendor\\_ruby/3.3.0\n\nСюда **должны** ставиться дополнительные гемы и/или патчи от команды мейнтейнеров дистрибутива. Об этом сложно найти информацию, но несколько слов есть в книге `The Ruby Programming Language` или, что интереснее, в changelog для [NEWS for Ruby 1.8.7](https://docs.ruby-lang.org/en/2.3.0/NEWS-1_8_7.html) (да, окаменелое…):\n\n<section style=\"background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);\">\n <p id=\"VAZR\"><strong>vendor_ruby</strong> directory<br>A new library directory named <code>vendor_ruby</code> is introduced in addition to <code>site_ruby</code>. The idea is to separate libraries installed by the package system (<code>vendor</code>) from manually (<code>site</code>) installed libraries preventing the former from getting overwritten by the latter, while preserving the user option to override vendor libraries with site libraries. (<code>site_ruby</code> takes precedence over <code>vendor_ruby</code>)<br>If you are a package maintainer, make each library package configure the library passing the <code>--vendor</code> option to <code>extconf.rb</code> so that the library files will get installed under <code>vendor_ruby</code>.<br>You can change the directory locations using configure options such as<code> --with-sitedir=DIR</code> and <code>--with-vendordir=DIR</code>.</p>\n </section>\n### /usr/local/lib/ruby/site\\_ruby/3.3.0\n\nСюда будут ставиться \"системные\" файлы, но не от OS, а от самого ruby, например после выполнения `gem update –system`:\n\n```plaintext\nsite_ruby/\n└── 3.4.0\n ├── bundler\n ├── rubygems\n └── x86_64-linux-musl\n```\n\n### /usr/local/lib/ruby/gems/3.3.0/specifications/\n\n…а также INSTALLATION DIRECTORY `/usr/local/bundle/specifications`… \n…а также USER INSTALLATION DIRECTORY: `/root/.local/share/gem/ruby/3.3.0`… \n…а также SPEC CACHE DIRECTORY `/root/.cache/gem/specs`… \n…а также BUNDLE\\_CACHE\\_PATH …\n\nСюда ставятся .gemspec файлы, которые собственно и говорят пакетному менеджеру ruby (gem и bundle), какие именно версии каких гемов установлены.\n\n### /usr/local/lib/ruby/gems/3.3.0/specifications/default\n\nDefault, Карл… Default - это особое состояние гема, и эти гемы нельзя удалить. Если вы обновили гем до новой версии, то старая всё равно останется, и Trivy вам этого не простит. Весьма вредная папка с точки зрения сканирования.\n\n### /usr/local/lib/ruby/gems/3.3.0/cache/\n\n…а также INSTALLATION DIRECTORY `/usr/local/bundle/cache`… \n…а также BUNDLE\\_PATH …\n\nСюда пакетный менеджер ruby (gem или bundle) скачивает гемы (допустим вы ставите faraday.gem) перед установкой. Этот кеш очень часто используется для ускорения сборки, например [в вашем Gitlab](https://docs.gitlab.com/ee/ci/caching/#cache-ruby-dependencies).\n\n## Что здесь у вас происходит?!\n\nПосле того как мы в процессе исследования детально разобрались и увидели всё это многообразие мест, где могут находиться файлы, вызывающие панические атаки у Trivy, мы решили радикально решить эту проблему: свести все файлы, все гемы и все спеки (.gemspec) в одно место. Это позволит легко следить за всеми (всеми!) фактическими зависимостями и эффективно пользоваться командами `gem cleanup` и `bundle clean`. Тут надо отметить, что для вашей рабочей OS (системы общего назначения) такое решение приведёт к поломке системного пакетного менеджера ([Gentoo Portage](https://ru.wikipedia.org/wiki/Portage), [dpkg/apt](https://ru.wikipedia.org/wiki/Dpkg), [rpm/zypper](https://ru.wikipedia.org/wiki/RPM) и пр.), но мы ведь говорим о конкретной сборке ruby под ваш конкретный проект - и тут никаких проблем не будет.\n\n<section style=\"background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);\">\n <p id=\"ma0i\">Для того чтобы узнать что куда, когда и зачем ставится, мы использовали git прямо на корне файловой системы внутри контейнера (ruby:alpine, просто alpine, ruby:debian и другие образы для сравнения) и фиксировали изменения после различных команд ⏳</p>\n </section>\n\nНо это еще не всё. Остаётся еще проблема с default gemspec, но её относительно легко решить:\n\n```shell\nmv /usr/local/lib/ruby/gems/3.3.0/specifications/default/* /usr/local/lib/ruby/gems/3.3.0/specifications/\n```\n\nТеперь мы можем сформулировать План:\n\n- Все дороги ведут в Рим - делаем ссылочки для `vendor_ruby`, `site_ruby` и пр. Также явно прописываем системные переменные `GEM_HOME` и `BUNDLE_APP_CONFIG.`\n- Разбираемся с default gems.\n- Обновляем ruby (имеется в виду stdlib) до последней требуемой версии.\n- Обновляем bundle до нужной версии.\n- Удаляем лишние гемы (например rdoc).\n- Переставляем (!) системные (на текущий момент сборки базового образа - **все** ) гемы, потому что, как оказалось, `mv` для default gemspec имеет не очень хорошие последствия.\n- profit!\n\nСказано - сделано, и добро пожаловать под кат!\n\n```docker\nARG BASE_RUBY=3.2\nARG BASE_ALPINE=alpine3.16\nARG BASE_IMAGE=ruby:${BASE_RUBY}-${BASE_ALPINE}\n\nFROM ${BASE_IMAGE}\n\nARG BASE_RUBY=3.2\n\nARG RUBYGEMS_VERSION=3.5.20\nARG BUNDLER_VERSION=2.5.20\n\n# эта переменная есть в старом alpine но нет в debian и новом\n# добавляем потому что она очень нужна для работы с папочками\nENV RUBY_MAJOR=${BASE_RUBY}\n\nENV RUBYGEMS_VERSION=${RUBYGEMS_VERSION} \\\n BUNDLER_VERSION=${BUNDLER_VERSION}\n\nRUN apk update && apk upgrade\n\n# Это наш костыльный скрипт который удаляет всякие лишние кеши,\n# man-файлы и прочий мусор. \nCOPY common/scripts/cleanallbuilds.sh /usr/bin/\n\n# dumb-init всегда используем как PID-1 но это немного другая история\nRUN set -ex \\\n && apk add dumb-init \\\n && cleanallbuilds.sh\n\n###### \n# Надругиваемся над диструбутивом, чтоб иметь строго\n# одну версию руби в систему и управлять ею целиком через gem/bundle\n\n# все пути ведут в Рим\nENV GEM_HOME=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/\nENV BUNDLE_APP_CONFIG=/usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/\n\n# Сводим vendor_ruby, site_ruby и GEM_HOME в одно место\nRUN set -ex \\\n && rm -rf /usr/local/lib/ruby/site_ruby /usr/local/lib/ruby/vendor_ruby \\\n && ln -sf /usr/local/lib/ruby /usr/local/lib/ruby/site_ruby \\\n && ln -sf /usr/local/lib/ruby /usr/local/lib/ruby/vendor_ruby \\\n && mkdir -p /root/.local/share/gem/ruby/ \\\n && ln -sf ${GEM_HOME} /root/.local/share/gem/ruby/${RUBY_MAJOR}.0\n\n# Чутка тюним bundle config чтоб в дальнейшем не забыть\nRUN set -ex \\\n && bundle config --local disable_version_check true \\\n && bundle config --local clean false \\\n && bundle config --local no_prune false \\\n && bundle config --local disable_local_branch_check true \\\n && bundle config --local jobs 2 \\\n && bundle config --local allow_offline_install true\n\n# настраиваем .gemrc чтоб не было ничего лишнего, вклюячая rdoc\nRUN set -ex \\\n && echo 'gem: --no-document' > /usr/local/etc/gemrc \\\n && echo 'update_sources: false' >> /usr/local/etc/gemrc \\\n && echo 'verbose: false' >> /usr/local/etc/gemrc \\\n && echo 'update: --no-suggestions' >> /usr/local/etc/gemrc \\\n && echo 'install: --no-suggestions --conservative' >> /usr/local/etc/gemrc\n\n# пытаемся удалить ненужные гемы с самого начала - попытка не пытка\nRUN set -ex \\\n && gem uninstall -a -x --quiet --force `gem list | cut -f 1 -d \" \"` \\\n && gem cleanup\n\nRUN set -ex \\\n && apk add --virtual .build-deps \\\n autoconf \\\n bison \\\n bzip2 \\\n bzip2-dev \\\n coreutils \\\n curl-dev \\\n dpkg-dev dpkg \\\n g++ \\\n gcc \\\n gdbm-dev \\\n git \\\n glib-dev \\\n libc-dev \\\n libffi-dev \\\n libxml2-dev \\\n libxslt-dev \\\n linux-headers \\\n make \\\n ncurses-dev \\\n procps \\\n readline-dev \\\n tar \\\n xz \\\n yaml-dev \\\n zlib-dev \\\n shared-mime-info \\\n && mv /usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/specifications/default/* /usr/local/lib/ruby/gems/${RUBY_MAJOR}.0/specifications/ \\\n && gem cleanup \\\n && gem update --system \"${RUBYGEMS_VERSION}\" \\\n && gem uninstall bundler --all --silent || true \\\n && gem install bundler -v \"${BUNDLER_VERSION}\" \\\n && gem uninstall rdoc --all --silent || true \\\n && GMS=`gem list | sed s/default:\\ // | sed -E 's/\\ \\((.*)\\)/:\\1/' | sort` \\\n && DEBUG_FLAGS=\"-Wno-calloc-transposed-args\" gem pristine --all --extensions \\\n && commands=$(for x in $GMS; do \\\n g=$(echo \"$x\" | cut -f 1 -d \":\"); \\\n v=$(echo \"$x\" | cut -f 2 -d \":\"); \\\n echo gem pristine $g --version \"$v\"; \\\n done) \\\n && echo -e \"$commands\" | xargs -I CMD -P 3 bash -c CMD\\\n && gem cleanup \\\n && rm -rf root/.local/share/gem/specs \\\n && apk del .build-deps \\\n && cleanallbuilds.sh\n\nWORKDIR /home/app\nRUN set -ex \\\n && adduser -D -s /sbin/nologin app \\\n && chown -R app:app /home/app\n\nENTRYPOINT [\"/usr/bin/entrypoint.sh\"]\nSHELL [\"/bin/sh\", \"-c\"] \n```\n\nСамое интересное, конечно, находится в одном слое самого пухленького RUN, и по некоторым командам надо дать пояснения:\n\n- `GMS=`gem list | sed s/default:\\ // | sed -E 's/\\ \\((.*)\\)/:\\1/' | sort`` - получаем список установленных гемов с версиями в формате \"yaml:0.4.0 zlib:3.2.1\". Это нам потребуется дальше из-за странного поведения `gem pristine`\n- `gem pristine --all --extensions` - должен для всех установленных гемов сделать \"чистовую\" установку, включая скачивание и перекомпиляцию расширений. Но нет. На самом деле он делает не для всех и не всё :(\n- `DEBUG_FLAGS=\"-Wno-calloc-transposed-args\"` - мы используем один Dockerfile для сборки базовых образов ruby 2.7, 3.0, 3.1, 3.2, и по умолчанию все расширения должны компилироваться без единого ~~разрыва~~ ворнинга компиляции, но для некоторых версий руби (кажется 3.0) это не так, и именно этот ворнинг всё портит, поэтому его подавляем без затей.\n- `commands=$(for x in $GMS; do …` - а вы знали, что в bash тоже есть пул потоков? Строго говоря, он [пул процессов](https://stackoverflow.com/questions/6441509/how-to-write-a-process-pool-bash-shell), и не в баше, а в xargs, но это не важно. В общем тут список `$GMS` в виде \"yaml:0.4.0 zlib:3.2.1\" трансформируется в список `$commands`, потому что в такой форме `gem pristine` работает именно так как надо:\n\n```shell\ngem pristine yaml --version \"0.4.0\"\ngem pristine zlib --version \"3.2.1\"\n...\n```\n\n- `echo -e \"$commands\" | xargs -I CMD -P 3 bash -c CMD` - отправляем `$commands` в пул из трех процессов для одновременной установки гемов. \n- `cleanallbuilds.sh` - зовём в самом конце кустарный скрипт, который удаляет всякий мусор из системы:\n\n```shell\n#!/bin/sh\n\n# ruby (alpine + debian)\n# Скрипт единый идля debian и для alpine, поэтому ошибки,\n# возникающие из-за разницы дистрибутивов просто игнорируем\nrm -rf /usr/src/ruby/ || true\nrm -rf /root/.local/share/gem/specs/ || true\nrm -rf /root/.local/state/ || true\nrm -rf /root/.cache/gem/ || true\nrm -rf /usr/local/lib/ruby/gems/3.2.0/cache/ || true\nrm -rf /root/.bundle/cache/ || true\ngem cleanup || true\n\n# alpine\napk cache clean || true\n\n# debian\napt-get clean autoclean || true\napt-get autoclean --yes || true\napt-get autoremove --yes || true\n\nfind /var/log/ -type f -delete || true\nfind /var/lib/log/ -type f -delete || true\nfind /usr/share/doc/ -type f -delete || true\n```\n\n## Результат\n\nСтарый образ занимал 900MB, новый - 152MB (не спрашивайте...)\n\n![](https://img1.teletype.in/files/c3/95/c395d60f-18a0-45a0-9757-f7b7f779949c.png)\n_Значительно лучше. Особенно для базового образа_\n\n## Что дальше?\n\nТеперь эти базовые образы надо начать использовать непосредственно в CI пайплайнах боевых сервисов и, конечно, процедуру сборки и тестирования придётся поменять. Точнее, мы уже перешли на новые образы и сборку уже пару недель назад - дело теперь только за статьёй. А в качестве приманки приведу результаты перехода для одного из самых \"толстых\" наших сервисов:\n\n```plaintext\nimage:old size:1.49GB vulns:255\nimage:new-prod size:259MB vulns:1 (Вчерашняя!! СVE-2025-25186)\nimage:new-test size:269MB vulns:1 (СVE-2025-25186)\n```\n\nЕсли взглянуть ретроспективно, то совсем не весело - так что:\n\n<section style=\"background-color:hsl(hsl(24, 24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);\">\n <p id=\"tf2v\">профилируйте чаще не только ваш код и инфраструктуру сборки.</p>\n </section><tt-tags id=\"HMo0\">\n <tt-tag name=\"docker\">#docker</tt-tag>\n <tt-tag name=\"devsecops\">#devsecops</tt-tag>\n <tt-tag name=\"ruby\">#ruby</tt-tag>\n <tt-tag name=\"cicd\">#cicd</tt-tag>\n </tt-tags>",
48
+ "positive_reactions_count": 0,
49
+ "cover_image": null,
50
+ "tag_list": [],
51
+ "canonical_url": "https://blog.rnds.pro/064-devsecops1?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
52
+ "reading_time_minutes": 6,
53
+ "user": {
54
+ "name": "Samoilenko Yuri",
55
+ "username": "kinnalru",
56
+ "twitter_username": null,
57
+ "github_username": "kinnalru",
58
+ "user_id": 559891,
59
+ "website_url": "https://teletype.in/@jerry_ru",
60
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
61
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
62
+ },
63
+ "organization": {
64
+ "name": "RNDSOFT",
65
+ "username": "rnds",
66
+ "slug": "rnds",
67
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
68
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
69
+ }
70
+ },
71
+ {
72
+ "type_of": "article",
73
+ "id": 2204211,
74
+ "title": "Selenium. Как заставить браузер работать на вас",
75
+ "description": "Всем привет! Сегодня я расскажу, что такое Selenium, как его запустить, зачем он нужен, и какие у...",
76
+ "published": true,
77
+ "published_at": "2024-12-25T05:52:13.000Z",
78
+ "slug": "selenium-kak-zastavit-brauzier-rabotat-na-vas-1mim",
79
+ "path": "/rnds/selenium-kak-zastavit-brauzier-rabotat-na-vas-1mim",
80
+ "url": "https://dev.to/rnds/selenium-kak-zastavit-brauzier-rabotat-na-vas-1mim",
81
+ "comments_count": 0,
82
+ "public_reactions_count": 0,
83
+ "page_views_count": 36,
84
+ "published_timestamp": "2025-01-18T10:01:16Z",
85
+ "body_markdown": "---\ntitle: Selenium. Как заставить браузер работать на вас\npublished: true\ndate: 2024-12-25 05:52:13 UTC\ntags: ruby,rails\ncanonical_url: https://blog.rnds.pro/063-selenium?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n![](https://img4.teletype.in/files/ba/29/ba29aa5e-fa6b-4c40-96c7-8a0369fcce13.jpeg)\n\nВсем привет! Сегодня я расскажу, что такое Selenium, как его запустить, зачем он нужен, и какие у него есть плюсы и минусы.\n\n## Небольшая вводная\n\nSelenium — это инструмент для автоматизации веб-браузеров. Он позволяет разработчикам и тестировщикам писать скрипты, которые могут управлять браузером, имитируя действия пользователя, такие как клики, ввод текста и навигация по страницам.\n\nSelenium поддерживает множество языков программирования, включая Python, Java, Ruby и другие, и может работать с различными браузерам. Это делает его популярным выбором для автоматизации тестирования веб-приложений и выполнения рутинных задач в браузере.\n\n## Задачи, решаемые Selenium\n\nМожно разделить на 2 большие группы. Это тестирование и парсинг (скрейпинг). Парсинг - это процесс извлечения и обработки данных из целевых ресурсов.\n\nК тестированию можно отнести такие подгруппы как:\n\n- **Тестирование пользовательских интерфейсов.** Позволяет проверять элементы интерфейса, такие как кнопки, поля ввода и ссылки, чтобы убедиться, что они работают, как задумано.\n- **Кросс-браузерное тестирование.** Позволяет запускать тесты в различных браузерах, что помогает убедиться, что приложение работает корректно в разных средах.\n- **Регрессионное тестирование.** Позволяет повторно запускать тесты после внесения изменений в код, чтобы убедиться, что новые изменения не нарушили существующий функционал.\n\nВ простых случаях можно обойтись и без Selenium. Например получать html страницы с помощью простых инструментов, таких как curl.\n\nНо бывает более сложные случаи, когда Selenium становится нашим лучшим другом:\n\n- **Динамически загружаемые страницы.** SPA приложению требуется js.\n- **Обход ограничений.** Современные сайты часто содержат механизмы защиты от парсинга. Например, проверка cookie, user-agent и, конечно же, captcha.\n\n_Для обхода капчи часто используется комбинация Selenium, который позволяет выполнять js на странице и прогрузить капчу, и специального механизма для решения капчи. К таким механизмам относятся:_\n\n- _ **Сторонние платные сервисы.** Принимают изображение или аудиофайл через API и возвращают готовое решение._\n- _ **Обученные нейронные сети.** Запускаются локально и самостоятельно распознают капчи._\n\n_После получения ответа от выбранного механизма Selenium используется для ввода решения в поле или для выполнения требуемых действий, например, выбора объектов вроде \"светофоров\" или “мостов”._\n\n## Конфигурирование приложения для работы с Selenium\n\nРассмотрим конфигурирование на примере Ruby on Rails приложения.\n\nВ первую очередь нам необходимо поставить гемы. Для этого добавляем в Gemfile следующие строки:\n\n```\ngem 'selenium-webdriver'\ngem 'webdrivers'\n```\n\nИ устанавливаем гемы:\n\n```\nbundle install\n```\n\n## Работа с Selenium\n\nПример парсинга данных:\n\n```ruby\nrequire 'selenium-webdriver'\nrequire 'webdrivers'\n\ndriver = Selenium::WebDriver.for :chrome\ndriver.navigate.to 'http://example.com/products'\nproducts = driver.find_elements(class: 'product-item')\n\nproducts.each do |product|\n name = product.find_element(class: 'product-name').text\n price = product.find_element(class: 'product-price').text\n Rails.logger.info { \"Название: #{name}, Цена: #{price}\" }\nend\n\ndriver.quit\n\n```\n\nФункционально пример выше найдет названия и цену товаров на воображаемом сайте и напечатает их в логах. Но этот код можно улучшить.\n\nВо-первых, нужно учитывать то, что Selenium - это браузер, а значит нам стоит ждать загрузки страницы. Поэтому модифицируем код, и при попытке получить список товаров, ждем 5 секунд и только потом падаем с ошибкой.\n\n```\nproducts = Selenium::WebDriver::Wait.new(timeout: 5).until do\n driver.find_elements(class: 'product-item')\nend\n```\n\nВо-вторых, нужно хоть немного замаскироваться от систем сайта, которые ограничивают работу парсеров. Для этого добавим немного “человечности” нашему браузеру.\n\n```\noptions = Selenium::WebDriver::Chrome::Options.new\noptions.add_argument('--disable-blink-features=AutomationControlled')\noptions.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36')\noptions.add_argument('--headless=new')\noptions.add_argument('--disable-gpu')\noptions.add_argument('window-size=1200x800')\n\ndriver = Selenium::WebDriver.for(:chrome, options: options)\n```\n\nНебольшое пояснение к опциям:\n\n- **--disable-blink-features=AutomationControlled.** Отключает некоторые функции Blink, которые указывают на то, что браузер управляется автоматизированным инструментом. Может помочь избежать обнаружения автоматизации на некоторых сайтах.\n- **--user-agent=Mozilla/5.0 (X11; Linux x86\\_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36.** Устанавливает пользовательский агент (User-Agent) для браузера. Пользовательский агент сообщает веб-сайтам, какую версию браузера и операционной системы использует пользователь. Установка пользовательского агента может помочь в обходе блокировок или в получении контента, оптимизированного для определенного браузера.\n- **--headless=new.** Запускает браузер в \"безголовом\" режиме, что означает, что он будет работать без графического интерфейса. Нужно для автоматизации и тестирования.\n- **--disable-gpu.** Отключает использование графического процессора (GPU). Полезно в безголовом режиме, так как некоторые функции, зависящие от GPU, могут вызывать проблемы или не поддерживаться.\n\nПо итогу получится такой код:\n\n```\nrequire 'selenium-webdriver'\nrequire 'webdrivers'\n\ndef wait(time)\n Selenium::WebDriver::Wait.new(timeout: time)\nend\n\ndef parse\n options = Selenium::WebDriver::Chrome::Options.new\n options.add_argument('--disable-blink-features=AutomationControlled')\n options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36')\n options.add_argument('--headless=new')\n options.add_argument('--disable-gpu')\n\n driver = Selenium::WebDriver.for(:chrome, options: options)\n\n driver.navigate.to 'https://example.com/products'\n products = wait(5).until { driver.find_elements(class: 'product-item') }\n products.each do |product|\n name = product.find_element(class: 'product-name').text\n price = product.find_element(class: 'product-price').text\n Rails.logger.info { \"Название: #{name}, Цена: #{price}\" }\n end\nrescue Selenium::WebDriver::Error::TimeoutError\n 'Элемент с классом product-name не найден'\nensure\n driver.quit\nend\n\n```\n\nИ еще пара советов:\n\n- В браузер можно подгружать плагины:\n\n```\noptions.add_extension(Rails.root.join('plugin.crx'))\n```\n\n- Обязательно надо закрывать за собой браузер, чтобы избегать утечек памяти:\n\n```\ndriver.quit\n```\n\n- Иногда требуется работа с cookie. В этом помогут следующие команды:\n\n```\ndriver.manage.all_cookies # получить все куки\ndriver.manage.cookie_named # получить куку по названию\ndriver.manage.add_cookie # записать куку\n```\n\n## Альтернативы Selenium\n\nСамые популярные инструменты для работы с виртуальными браузерами это:\n\n- **Selenium.** Старейший представитель. Поддержка множества браузеров и библиотек на различных языках. Большое и активное сообщество. Более сложное api, чем у остальных.\n- **Puppeteer.** Работает только на Node.js. Поддержка Chrome и Firefox. Сообщество растет, но меньше по сравнению с Selenium. Простое и интуитивно понятное api.\n- **Playwright.** Самый молодой представитель в этом списке. Поддержка множества браузеров и есть библиотеки на различных языках. Удобное и современное api. Встроенная поддержка мобильных устройств. Сообщество активно развивается.\n\n## Вывод\n\nSelenium является одним из самых популярных инструментов для автоматизации браузеров и тестирования веб-приложений. Широкая поддержка различных браузеров и языков программирования упрощает процесс написания кода для реализации задач, будь то тестирование или парсинг.\n\nДолговечность и стабильность подтверждают его надежность и эффективность.\n\nС большой вероятностью проблема, которую вы пытаетесь решить, уже кем-то была решена, и в интернете можно найти гайд, который поможет.\n\nТаким образом, Selenium остается одним из лучших инструментов для автоматизации браузеров. Используем его у себя в работе и вам советуем 🙂",
86
+ "positive_reactions_count": 0,
87
+ "cover_image": null,
88
+ "tag_list": [
89
+ "ruby",
90
+ "rails"
91
+ ],
92
+ "canonical_url": "https://blog.rnds.pro/063-selenium?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
93
+ "reading_time_minutes": 2,
94
+ "user": {
95
+ "name": "Samoilenko Yuri",
96
+ "username": "kinnalru",
97
+ "twitter_username": null,
98
+ "github_username": "kinnalru",
99
+ "user_id": 559891,
100
+ "website_url": "https://teletype.in/@jerry_ru",
101
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
102
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
103
+ },
104
+ "organization": {
105
+ "name": "RNDSOFT",
106
+ "username": "rnds",
107
+ "slug": "rnds",
108
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
109
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
110
+ }
111
+ },
112
+ {
113
+ "type_of": "article",
114
+ "id": 2167133,
115
+ "title": "Простейший AI ассистент или Tools or not tools",
116
+ "description": "Нужно бежать со всех ног, чтобы только оставаться на месте, а чтобы куда-то попасть, надо бежать как...",
117
+ "published": true,
118
+ "published_at": "2024-12-12T15:43:22.000Z",
119
+ "slug": "prostieishii-ai-assistient-ili-tools-or-not-tools-4387",
120
+ "path": "/rnds/prostieishii-ai-assistient-ili-tools-or-not-tools-4387",
121
+ "url": "https://dev.to/rnds/prostieishii-ai-assistient-ili-tools-or-not-tools-4387",
122
+ "comments_count": 1,
123
+ "public_reactions_count": 0,
124
+ "page_views_count": 17,
125
+ "published_timestamp": "2025-01-18T09:50:09Z",
126
+ "body_markdown": "---\ntitle: Простейший AI ассистент или Tools or not tools\npublished: true\ndate: 2024-12-12 15:43:22 UTC\ntags: other\ncanonical_url: https://blog.rnds.pro/062-ai-assistant?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n> Нужно бежать со всех ног, чтобы только оставаться на месте, а чтобы куда-то попасть, надо бежать как минимум вдвое быстрее! _Льюис Кэролл Алиса в Cтране Чудес_\n\n![](https://img2.teletype.in/files/54/7f/547fc713-a025-45bd-bef5-914a420b1dca.jpeg)\n\n**Вступление**\n\nВ данной статье мы продемонстрируем, как можно построить простейшего AI ассистента. Давайте сперва определимся с терминами. Обычно под AI ассистентом подразумевают способности Больших языковых моделей (далее по тексту LLM) не просто выдавать готовый текстовый ответ, но и совершать какую-то автономную работу по вызову сторонних функций, отправку запросов в API и на основании полученной информации из сторонних сервисов (но иногда нужно получить именно точный ответ в заданном формате), промпта, а также запроса пользователя выдавать итоговый ответ. Так, с терминами определились. Теперь вкратце о чем будет статья: в статье мы покажем, как 2мя способами LLM заставить взаимодействовать с внешним миром и с информацией, полученной не от пользователя, а из внешнего мира. В данной статье мы будем обогащать вывод LLM информацией из поиска, т.к. основной проблемой LLM является то, что в них информация заморожена на определенный момент времени, и с течением времени она устаревает и требует переобучения модели. Переобучение модели является очень дорогостоящим мероприятием, т.о. чтобы актуализировать информацию можно делать запросы в интернет, чтобы получать свежую информацию, а LLM будет нам, используя информацию из своего пространства знаний, а также дополняя информацией из поиска, выдавать достаточно свежий результат. На основе скриптов из этой статьи можно будет уже делать первые попытки для построения собственных мини-ассистентов.\n\n### \n\n### **Инструменты**\n\nВ данной статье будут использованы следующие технологии:\n\n1. В качестве поискового движка будем использовать Tavily, т.к. у него простое API, а также они заявляют, что оптимизируют свой поиск как раз для использования с LLM (подробней можно почитать в документации к [tavily](https://docs.tavily.com/docs/welcome#tavily-search-api))\n2. LLM YandexGPT 4 Pro 32k RC\n3. Python, Gradio\n\nНужно получить [API ключи](https://yandex.cloud/ru/docs/foundation-models/api-ref/authentication#service-account_1) для YandexGPT API и [ключ](https://docs.tavily.com/docs/python-sdk/tavily-search/getting-started) для Tavily\n\n### **Промптинг**\n\nВ первом способе будем использовать результаты вызова функции в промпте для получения окончательного ответа от LLM как самый примитивный способ.\n\nНиже представлен код скрипта `yc-search-prompt.py`\n\n```\n#!/usr/bin/env python3\nimport httpx\nimport os\nimport gradio as gr\nfrom tavily import TavilyClient\n\nBASE_YC_GPT_URL = \"https://llm.api.cloud.yandex.net/foundationModels/v1/completion\"\n\ndef format_search_results(search_results):\n formatted_results = \"\\nRelevant search results:\\n\"\n for result in search_results['results']:\nformatted_results += f\"- {result['title']}: - URL: {result['url']} \\n {result['content'][:200]}...\\n\"\n\ndef create_prompt_with_search(user_message, search_results):\n search_context = format_search_results(search_results)\n prompt = f\"\"\"Here is some relevant context from a web search:\n {search_context}\n Using the above context, please answer the following question:\n {user_message}\n Please provide a comprehensive answer based on both the search results and your knowledge.\n And add at the end of final answer all titles and URL links at format Title - Url from above context.\"\"\"\n return prompt\n \ndef make_search_request(text):\n tavily_client = TavilyClient(api_key=os.environ[\"TAVILY_API_KEY\"] )\n response = tavily_client.search(text, max_results=8)\n return response\n \ndef make_request_yc_gpt(text, history):\n with httpx.Client() as client:\n headers = {'Authorization': \"Api-Key \" + os.environ['YC_API_KEY'], \n 'content-type':'application/json'}\n r = client.post(BASE_YC_GPT_URL, timeout=None, \n json={\"modelUri\": \"gpt://\"+os.environ['YC_FOLDER_ID'] + \"/yandexgpt-32k/rc\",\n \"completionOptions\": {\n \"stream\": False,\n \"temperature\": \"0.3\",\n \"maxTokens\": \"2000\"\n },\"messages\": [{\"role\": \"user\",\"text\": text}]}, headers=headers)\n return r.json()[\"result\"][\"alternatives\"][0][\"message\"][\"text\"]\n\ndef chatbot_function(message, chat_history, model_choice):\n try:\n if model_choice == \"YandexGPT+Tavily\":\n search_results = make_search_request(message)\n enhanced_prompt = create_prompt_with_search(message, search_results)\n print(enhanced_prompt)\n bot_message = f\"You selected the {model_choice} model.\\n\" + make_request_yc_gpt(enhanced_prompt, chat_history)\n chat_history.append((message, bot_message))\n else:\n bot_message = f\"You selected the {model_choice} model.\\n\" + make_request_yc_gpt(message, chat_history)\n chat_history.append((message, bot_message))\n return \"\", chat_history\n except Exception as e:\n error_message = f\"An error occurred: {str(e)}\"\n chat_history.append((message, error_message))\n return \"\", chat_history\nwith gr.Blocks() as demo:\n gr.Markdown(\"AI prompting with internet search\")\n with gr.Row():\n with gr.Column(scale=4):\n chatbot = gr.Chatbot()\n msg = gr.Textbox(label=\"Сообщение\")\n submit = gr.Button(\"Отправить\")\n clear = gr.Button(\"Очистить\")\n with gr.Column(scale=1):\n model = gr.Radio(\n [\"YandexGPT+Tavily\", \"YandexGPT\"],\n label=\"Выберите модель\",\n value=\"YandexGPT+Tavily\"\n )\n submit.click(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot])\n msg.submit(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot])\n clear.click(lambda: None, None, chatbot, queue=False)\ndemo.launch()\n \n```\n\n_Примечание:_ **Помните: при запросах к YandexGPT и Tavily могут списываться денежные средства. Перед запуском скрипта читайте актуальные правила использования сервисов.**\n\nЗапускать скрипт следующим образом\n\n```\nYC_FOLDER_ID=folder_id YC_API_KEY=YANDEX_API_KEY TAVILY_API_KEY=TAVILY_KEY python3 yc-search-prompt.py\n```\n\nПринцип работы скрипта: В UI Gradio на вход скрипт принимает текст от пользователя, в зависимости от того выбран ли вариант использования вместе с Tavily (YandexGPT+Tavily), тогда отправляется запрос в поиск Tavily, потом результат поиска отдается YandexGPT с промптом и просьбой сформировать окончательный ответ из собственных знаний, а также результатов поиска, а также в ответ добавить ссылки на источники из поиска.\n\n### **Function calling (Tools)**\n\nВторой способ также будет использовать промпт для получения окончательного ответа, но для получения результатов поиска мы будем использовать функционал function calling. Наверное стоит остановиться подробней, для чего это нужно, т.к. кода стало почти в 2 раза больше, а результат такой же. Function calling (Tools) - это способность LLM вызывать сторонние приложения, это могут быть скрипты, обращения к различным API. В большинстве случаев это необходимо, когда для LLM нужно получить конкретный ответ (конечно, пример с использованием поиска не очень подходящий, но хотелось сделать примеры максимально похожими, больше здесь подходит, например, вызов функции, которая использует калькулятор), что-то посчитать, а т.к. LLM не предназначены для конкретных вычислений, то для этого используется функционал function calling. При этом если Tools будет много, то LLM может и сама принимать решение, когда и какой Tool ей вызывать (у anthropic есть прямо [определение поведения](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#controlling-claudes-output) LLM для выбора Tools. Важный момент: нужно делать хорошее описание для tools. Вот примеры хороших и плохих описаний tools от одного из [лидеров индустрии](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#best-practices-for-tool-definitions).\n\n_Примечания:_\n\n1. _в YandexGPT API на момент написания статьи функционал Tools находился в режиме бета-тестирования, может быть непредвиденное поведение._\n2. _На момент написания статьи в скриптах использовалась версия релиз кандидат YandexGPT RC 32k, подробней про жизненный цикл моделей [читайте в документации](https://yandex.cloud/ru/docs/foundation-models/concepts/yandexgpt/models#model-lifecycle)_\n\nНиже представлен код скрипта `yc-search-tools.py`\n\n```\n#!/usr/bin/env python3\n\nimport httpx\nimport os\nimport json\nfrom tavily import TavilyClient\nimport gradio as gr\n\nBASE_YC_GPT_URL = \"https://llm.api.cloud.yandex.net/foundationModels/v1/completion\"\nsearch_tool = {\n \"function\": {\n \"name\": \"search_tavily\",\n \"description\": \"Search the web for current information\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\n \"type\": \"string\",\n \"description\": \"The search query\"\n }\n },\n \"required\": [\"query\"]\n }\n }\n}\n\ndef format_search_results(search_results):\n formatted_results = \"\\nRelevant search results:\\n\"\n for result in search_results['results']:\n formatted_results += f\"- {result['title']}: - URL: {result['url']} \\n {result['content'][:200]}...\\n\"\n return formatted_results\n\ndef create_prompt_with_search(user_message, search_results):\n search_context = format_search_results(search_results)\n prompt = f\"\"\"Here is some relevant context from a web search:\n{search_context}\n\nUsing the above context, please answer the following question:\n{user_message}\n\nPlease provide a comprehensive answer based on both the search results and your knowledge.\nAnd add at the end of final answer all titles and URL links at format Title - Url from above context.\"\"\"\n return prompt\n\ndef make_request_yc_gpt(text, is_tool_call=True):\n with httpx.Client() as client:\n headers = {'Authorization': \"Api-Key \" + os.environ['YC_API_KEY'], \n 'content-type':'application/json'}\n payload = {\n \"modelUri\": f\"gpt://{os.environ['YC_FOLDER_ID']}/yandexgpt-32k/rc\",\n \"completionOptions\": {\n \"stream\": False,\n \"temperature\": 0.0,\n \"maxTokens\": 8000\n },\n \"messages\": text\n }\n \n if is_tool_call:\n payload[\"tools\"] = [search_tool]\n\n r = client.post(\n BASE_YC_GPT_URL,\n timeout=None,\n json=payload,\n headers=headers\n )\n \n response = r.json()\n return response\n\ndef handle_tool_calls(toolCalls):\n results = []\n for tool_call in toolCalls:\n if toolCalls[0][\"functionCall\"][\"name\"] == \"search_tavily\":\n result = make_search_request(tool_call[\"functionCall\"][\"arguments\"][\"query\"])\n return result\ndef make_search_request(text):\n tavily_client = TavilyClient(api_key=os.environ[\"TAVILY_API_KEY\"] )\n response = tavily_client.search(text, max_results=8)\n return response\n\ndef process_conversation(user_input, history):\n conversation = [\n {\n \"role\": \"system\",\n \"text\": \"You are a helpful bot that helps the user. You can use tools at your discretion to generate answers, but you don't always need to use them.\"\n },\n {\n \"role\": \"user\",\n \"text\": user_input\n }\n ]\n initial_response = make_request_yc_gpt(conversation)\n \n if \"toolCalls\" in initial_response['result']['alternatives'][0]['message']['toolCallList']:\n tool_results = handle_tool_calls(\n initial_response['result']['alternatives'][0]['message']['toolCallList'][\"toolCalls\"]\n )\n \n enhanced_prompt = create_prompt_with_search(user_input, tool_results)\n \n final_conversation = [\n {\n \"role\": \"user\",\n \"text\": enhanced_prompt\n }\n ]\n \n final_response = make_request_yc_gpt(final_conversation, is_tool_call=False)\n return final_response['result']['alternatives'][0]['message']['text']\n else:\n return initial_response['result']['alternatives'][0]['message']['text']\n \ndef chatbot_function(message, chat_history, model_choice):\n try:\n response_list = process_conversation(message, chat_history)\n \n if isinstance(response_list, list):\n formatted_responses = [item['text'] for item in response_list if isinstance(item, dict) and 'text' in item] \n bot_message = f\"You selected the {model_choice} model.\\n\" + \"\\n\".join(formatted_responses)\n else:\n bot_message = f\"You selected the {model_choice} model.\\n\" + str(response_list)\n chat_history.append((message, bot_message))\n return \"\", chat_history \n except Exception as e: \n error_message = f\"An error occurred: {str(e)}\" \n chat_history.append((message, error_message)) \n return \"\", chat_history\nwith gr.Blocks() as demo:\n gr.Markdown(\"AI function calling tools internet search\") \n with gr.Row(): \n with gr.Column(scale=4): \n chatbot = gr.Chatbot() \n msg = gr.Textbox(label=\"Сообщение\") \n submit = gr.Button(\"Отправить\") \n clear = gr.Button(\"Очистить\") \n with gr.Column(scale=1):\n model = gr.Radio( \n [\"YandexGPT+Tavily\"], \n label=\"Модель\", \n value=\"YandexGPT+Tavily\" \n ) \n submit.click(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot]) \n msg.submit(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot]) \n clear.click(lambda: None, None, chatbot, queue=False) \ndemo.launch()\n```\n\n_Примечание:_ **Помните, при запросах к YandexGPT и Tavily могут списываться денежные средства. Перед запуском скрипта читайте актуальные правила использования сервисов.**\n\nЗапускать скрипт следующим образом\n\n`YC_FOLDER_ID=folder_id YC_API_KEY=YANDEX_API_KEY TAVILY_API_KEY=TAVILY_KEY python3 yc-search-tools.py`\n\nВкратце принцип работы скрипта: В UI Gradio на вход скрипт принимает текст от пользователя, вызывает tool tavily\\_search, после этого результаты поиска отдается YandexGPt с промптом и просьбой сформировать окончательный ответ из собственных знаний, результатов поиска, а также в ответ добавить ссылки на источники из поиска.\n\n### **Итоги и дополнительные материалы**\n\nВ итоге мы получили простенький аналог perplexity.ai, сделанный своими руками. Несколько скринов как это выглядит.\n\nОтвет YandexGPT, дополненный информацией из Tavily\n\n![](https://lh7-rt.googleusercontent.com/docsz/AD_4nXcJbJAI8kAVt__Oh7QxUEfbEdLVUd1PM_8QhHozgTxkYysnJy-TwswWc5LHTALoKykLIxo5NcPFQysdTYPf2aTgNv8O2jRi-MoS5bcvVwiprp_u5U8KOrEeYgf_MWVeC58PM1Oo1w?key=UhEGguG-LoZ_EPfcxuJK_gbd)\n_YandexGPT дополненный информацией из Tavily_\n\nИ ниже ответ дополняется ссылками (блок Titles and URLs) в поиске\n\n![](https://lh7-rt.googleusercontent.com/docsz/AD_4nXeGwTUa0egcd0d3Ul4ZnRlsdnRm4Oth5r9L2S8LUl7A-L-u0xdQlxQsNi9glUEygtrIvZlg_4LclUISlB7bppvD3f2h7j8c5lr-hbymRtHia2JjMgUIyWytv9pECVQ3SwmK5n71DA?key=UhEGguG-LoZ_EPfcxuJK_gbd)\n_YandexGPT дополненный информацией из Tavily_\n\nОтвет YandexGPT без дополнения ответа результатами из поиска, как видно внизу без ссылок на результаты поиска\n\n![](https://lh7-rt.googleusercontent.com/docsz/AD_4nXf1nEIXuj2_I2fENHgvryV7nbi4dMTKjjwdR7u3RnFDXz-NRaawv8-sSqoKSk0Y1SHgQnsjR5eRQn6fsXS5SUfbrF6pz0p5LqHUt5zR-SJzoQI9Zwq4pIrlq_l8bh-kypZOXUvl7Q?key=UhEGguG-LoZ_EPfcxuJK_gbd)\n_YandexGPT без дополненным ответом из поиска_\n\nПриводим аналогичные скрины с использованием функционала Tools\n\n![](https://lh7-rt.googleusercontent.com/docsz/AD_4nXdQxydh47aqy9Z0CUGYvsn57NT9l79qLo_Rdm2K94Xabl-AeI3JXCZSjsE_esTQ02RutkkCQU1dzAT9pdgGeG8haHNybUrLEylXEfHFf0I_HVthKogWFFTWl_d_H_FqYCHRkZwS?key=UhEGguG-LoZ_EPfcxuJK_gbd)\n_YandexGPT + Tavily + Function calling (Tools)_\n\n![](https://lh7-rt.googleusercontent.com/docsz/AD_4nXfS0QQxLYPTKE3p_QkRlrqBIsq2GHCmDhFnXQ8KwASXqkLOEJbVoiq4wztaGQ8CnfbcT0-Z2s8O1623EhOsa5VwSefNHimaJbikHIBJcFKIXplvQZcLf0nouWzggTRFklbyFmy3gA?key=UhEGguG-LoZ_EPfcxuJK_gbd)\n_YandexGPT + Tavily + Function calling (Tools)_\n\nОдним из самых частых препятствий в процессе промышленного внедрения LLM являются галлюцинации и проблемы получения конкретных ответов, и хочется сказать еще пару слов про библиотеки и фреймворки, которые могут быть полезны для решения этих проблем.\n\n1. Не пренебрегайте промптами, если у вас нет других инструментов для контроля LLM. Хороший материал на тему [prompt engineering](https://www.promptingguide.ai/) \n2. Кроме промптинга есть фреймворки для работы с промптами, например, [dspy](https://dspy.ai/), к сожалению, из коробки поддержки YandexGPT там нет, но можно пробовать использовать [адаптер для совместимости с OpenAI API](https://github.com/all-mute/openai2yandex_api_adapter) + dspy (сами, честно говоря, еще не пробовали) и [проголосовать за фичу](https://yandex.cloud/ru/features/4631) в Yandex Cloud\n3. Помимо коммерческих реализаций есть также уже много open source моделей, в которых реализован функционал tools, например, у [ollama](https://ollama.com/search?c=tools). Кажется, скоро эта фича станет стандартной в LLM.\n4. Кроме промптинга есть еще возможность заставить LLM четко следовать формату ответа, это т.н. structured output, вот [хорошая статья](https://simmering.dev/blog/structured_output/) с библиотеками, большинство библиотек в этом списке тоже не поддерживают YandexGPT (а также почти все нестабильных версий 0.x.x), но чтобы этот мир стал еще лучше, можете проголосовать [за эту фичу](https://yandex.cloud/ru/features/4627)\n5. Фреймворки для построения агентов и мультиагентских систем\n\n 1. [LangGraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/) кандидат, чтобы стать стандартом в индустрии (там готовится целая экосистема библиотек для работы с LLM LangChain, LangSmith, LangGraph), самая большая поддержка различных LLM. Очень нестабильно. Но в документации к LangGraph можно почерпнуть много хороших идей для построения агентов [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/) и [How-To](https://langchain-ai.github.io/langgraph/how-tos/), это прямо must read! Для построения MVP и быстрого прототипирования подходит отлично.\n 2. [Llamaindex](https://docs.llamaindex.ai/en/stable/use_cases/agents/) Тоже довольно большая библиотека для работы с LLM. Подробней что-то рассказать сложно, сами не пользовались.\n 3. [AutoGen](https://microsoft.github.io/autogen/0.2/) библиотека для построения мультиагентских систем. Огромное кол-во примеров под разные use cases.\n6. Совсем недавно в Yandex cloud появился новый функционал [AI assistant](https://yandex.cloud/ru/docs/foundation-models/concepts/assistant/), который с использованием их ML SDK тоже позволяет строить LLM приложения и прячет некоторые вещи “под капот”, которые обычно используются в LLM приложениях: RAG, сохранение контекста.\n7. Также совсем недавно anthropic выпустил [Model Context Protocol](https://modelcontextprotocol.io/introduction), для более глубокой интеграции LLM, tools и источников данных. Посмотрим, сможет ли MCP стать стандартом в будущем.\n8. Также аналогичный функционал, скорей всего, можно реализовать более простым способом, использовав лишь один инструмент [Search API](https://yandex.cloud/ru/docs/search-api/concepts/generative-response) в Yandex Cloud (на момент написания статьи функционал был в Preview режиме)",
127
+ "positive_reactions_count": 0,
128
+ "cover_image": null,
129
+ "tag_list": [
130
+ "other"
131
+ ],
132
+ "canonical_url": "https://blog.rnds.pro/062-ai-assistant?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
133
+ "reading_time_minutes": 5,
134
+ "user": {
135
+ "name": "Samoilenko Yuri",
136
+ "username": "kinnalru",
137
+ "twitter_username": null,
138
+ "github_username": "kinnalru",
139
+ "user_id": 559891,
140
+ "website_url": "https://teletype.in/@jerry_ru",
141
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
142
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
143
+ },
144
+ "organization": {
145
+ "name": "RNDSOFT",
146
+ "username": "rnds",
147
+ "slug": "rnds",
148
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
149
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
150
+ }
151
+ },
152
+ {
153
+ "type_of": "article",
154
+ "id": 1247886,
155
+ "title": "Централизованное логирование",
156
+ "description": "В данной статье мы рассмотрим вопрос централизованного логирования с использованием filebeat...",
157
+ "published": true,
158
+ "published_at": "2022-11-03T13:12:06.000Z",
159
+ "slug": "tsientralizovannoie-loghirovaniie-naa",
160
+ "path": "/rnds/tsientralizovannoie-loghirovaniie-naa",
161
+ "url": "https://dev.to/rnds/tsientralizovannoie-loghirovaniie-naa",
162
+ "comments_count": 0,
163
+ "public_reactions_count": 0,
164
+ "page_views_count": 293,
165
+ "published_timestamp": "2023-04-10T17:26:43Z",
166
+ "body_markdown": "---\ntitle: Централизованное логирование\npublished: true\ndate: 2023-03-10 13:12:06 UTC\ntags: graylog, infra, nomad, traefik\ncanonical_url: https://blog.rnds.pro/033-centralized-logging?utm_source=devto&utm_medium=post&utm_campaign=rnds\n---\n\n![](https://img1.teletype.in/files/03/12/0312a760-6c68-4a3d-bad1-4f9a6838e1eb.png)\n\nВ данной статье мы рассмотрим вопрос централизованного логирования с использованием filebeat (гребаный спойлер) и graylog. В какой-то момент мы заметили, что машин в нашей инфраструктуре стало достаточно много, и чтобы посмотреть логи, приходилось иногда заходить на несколько машин и мучительно искать по множеству контейнеров, в этот момент мы поняли, что дальше жить так нельзя.\n\nПостановка задачи стояла следующим образом:\n\n1. Чем собирать логи\n2. Где хранить\n3. Нужно не потерять логи при недоступности центрального хранилища логов\n\nРанее мы уже пробовали централизованно собирать логи vector ([https://vector.dev/](https://vector.dev/)), но нам хотелось иметь удобный веб-интерфейс для просмотра. А vector веб-интерфейса не имеет. В качестве веб-интерфейса, а заодно и местом централизованного хранения, мы выбрали graylog, фактически сейчас единственное open source решение, которое имеет обширный функционал и удобство использования. На момент написания статьи vector “из коробки” не умеет отправлять в gelf формате в graylog, но в одном из issues на github один энтузиаст смог сконфигурировать vector нужным образом ([https://github.com/vectordotdev/vector/issues/4868#issuecomment-782740639](https://github.com/vectordotdev/vector/issues/4868#issuecomment-782740639)). В общем vector в качестве решения для сбора логов отбросили.\n\nПрежде чем отвечать на вопрос “Чем собирать”, нужно немного сказать про инфраструктуру: в инфраструктуре у нас все приложения запускаются в docker контейнерах, машин на момент написания у нас уже 16, а самих контейнеров около 60, контейнеры в свою очередь находятся под управлением nomad. Также важным моментом является то, что несколько машин в нашей инфраструктуре являются прерываемыми, они один раз в сутки перезапускаются. С инфраструктурой разобрались, теперь нужно определяться, чем собирать. Сперва наш взгляд упал на fluentd, fluentbit, т.к. они умели отправлять в gelf формате. В процессе настройки fluentd почему-то не слал больше 30 сообщений, разобраться, почему, нам так и не удалось, при использовании fluentbit в какой-то момент использования docker logging driver gelf и настройки fluentbit c output gelf выяснилось, что при недоступности fluentbit в момент рестарта контейнеров (на прерываемых машинах контейнеры один раз в сутки перезапускаются) они не запустятся и становятся неработоспособными. Мы поняли, что чем проще - тем лучше, и решили, что нужно собирать из файлов. В свою очередь настроили fluentbit для сбора из файлов, но fluentbit почему-то тоже доставлял не все логи, аналогично с fluentd определить, почему так происходит, не удалось. И в итоге мы остановились на filebeat в качестве сборщика логов ([https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html](https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html)), а graylog в качестве хранилища логов.\n\nЕще один важный момент, который нужно учесть, это то, что graylog находится в другой инфраструктуре, т.е. прямой сетевой связности между graylog сервером и машинами, на которых запущен filebeat, нет. Т.о. мы связали это все через гейт, на котором у нас находится traefik (смотри диаграмму ниже). Теперь перейдем к настройке. Graylog мы запустили на отдельной машине в обычном docker-compose.yml файле.\n\n![](https://lh3.googleusercontent.com/Wzf3hrjZEtkmygD2Myb8HKG9vYQdSzdaCS8UdGvXwEfO8hBBqSO_OVuI80kjGd3L26CzZSVCDNPCvxLLeVeIK-0HZS3NNW2dOCEOEtTp8VmHwu0MX--123ayMNjPmAKWZ0IC-2LgrNoAFt2VrbzPi3U7G0EMlDhIgANWdpcAHFQJQOnCAlcXivE_WQ)\n\nGraylog docker-compose.yml\n\n```\nversion: \"2.3\"\n\nservices:\n\n es01:\n image: elasticsearch:7.10.1\n restart: unless-stopped\n network_mode: host\n ulimits:\n memlock:\n soft: -1\n hard: -1\n nofile:\n soft: 65536\n hard: 65536\n environment:\n - \"ES_JAVA_OPTS=-Xms5g -Xmx5g\"\n - ELASTIC_PASSWORD=test\n - discovery.type=single-node\n - cluster.max_shards_per_node=20000\n - search.max_open_scroll_context=9000\n volumes:\n - /etc/localtime:/etc/localtime:ro\n - /graylog/data/es01:/usr/share/elasticsearch/data\n labels:\n - \"SERVICE_CHECK_HTTP=/_cluster/health\"\n - \"SERVICE_CHECK_INTERVAL=40s\"\n - \"SERVICE_CHECK_TIMEOUT=3s\"\n - \"SERVICE_CHECK_DEREGISTER_AFTER=10m\"\n\n mongodb:\n image: mongo:4.2\n restart: unless-stopped\n network_mode: host\n volumes:\n - /graylog/data/mongo:/data/db\n\n graylog:\n image: graylog/graylog:4.3.3\n restart: unless-stopped\n depends_on:\n - mongodb\n - es01\n network_mode: host\n volumes:\n - /graylog/data/graylog:/usr/share/graylog/data\n environment:\n - GRAYLOG_PASSWORD_SECRET=test\n - GRAYLOG_ROOT_PASSWORD_SHA2=shashasha\n - GRAYLOG_HTTP_EXTERNAL_URI=http://192.168.2.2:9000/\n entrypoint: /usr/bin/tini -- wait-for-it 172.22.202.104:9200 -- /docker-entrypoint.sh\n labels:\n - \"SERVICE_TAGS=hostname=graylog,traefik.enable=true,\\\n traefik.http.routers.graylog.rule=Host(`graylog.dev`),\\\n traefik.http.routers.graylog.entrypoints=websecure,\\\n traefik.http.routers.graylog.tls=true\"\n - SERVICE_NAME=graylog\n - SERVICE_CHECK_DEREGISTER_AFTER=10m\n - SERVICE_9000_NAME=graylog\n - SERVICE_9000_CHECK_TCP=true\n - SERVICE_9000_CHECK_INTERVAL=40s\n - SERVICE_9000_CHECK_TIMEOUT=5s\n - SERVICE_9000_CHECK_DEREGISTER_AFTER=10m\n\n```\n\nПерейдем к конфигурации filebeat\n\nКонфиг filebeat.yml у нас простейший\n\n```\nfilebeat.inputs:\n- type: container\n paths:\n - /var/lib/docker/containers/*/*.log\n processors:\n - add_docker_metadata:\n match_source_index: 4 \noutput.logstash:\n hosts: \n - graylog_host:graylog_port\n\n```\n\nТут все просто, мы просто отправляем все логи всех контейнеров на хост graylog, в свою очередь graylog\\_host - это у нас хост с traefik, а в конфигурации traefik уже указан сам graylog, т.к. с гейта есть прямая сетевая связность с graylog.\n\nТ.к. у нас nomad, то filebeat мы деплоим тоже в nomad, далее конфигурация filebeat job для деплоя его в nomad. Единственное, что здесь стоит отметить, так это то, что в env параметры GRAYLOG\\_HOST, GRAYLOG\\_PORT мы передаем ip адрес и порт на traefik. Также стоит обратить внимание, что тип job у нас system, что запускает filebeat на всех машинах. Также мы монтируем в контейнер filebeat каталог /filebeat/registry, для сохранения registry файла. Это необходимо для того, чтобы при отказе output filebeat отслеживал последние изменения, чтобы после того, как output вернется в рабочее состояние, доставить туда логи. Более подробно про registry файл можно почитать в документации filebeat [https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html#\\_how\\_does\\_filebeat\\_keep\\_the\\_state\\_of\\_files](https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html#_how_does_filebeat_keep_the_state_of_files)\n\n```\njob \"filebeat\" {\n datacenters = [\"a\", \"b\", \"c\"]\n namespace = \"default\"\n update {\n max_parallel = 1\n auto_revert = true\n auto_promote = false\n canary = 0\n }\n type = \"system\"\n group \"logging\" {\n restart {\n mode = \"delay\"\n attempts = 2\n interval = \"1m\"\n delay = \"30s\"\n }\n task \"filebeat\" {\n driver = \"docker\"\n kill_timeout = \"30s\"\n leader = true\n user = \"root\"\n env {\n SERVICE_IGNORE = \"true\"\n GRAYLOG_HOST = \"GRAYLOG_HOST\"\n GRAYLOG_PORT = \"GRAYLOG_PORT\"\n }\n config {\n auth {\n username = \"test\"\n password = \"test\"\n }\n image = \"docker.registry/filebeat:7.10.1\"\n force_pull = true\n volumes = [\n \"/var/lib/docker/containers:/var/lib/docker/containers:ro\",\n \"/var/run/docker.sock:/var/run/docker.sock:ro\",\n \"/filebeat/registry:/filebeat/registry:rw\"\n ]\n }\n service {\n name = \"filebeat\"\n tags = [\"filebeat\", \"logging\", \"hostname=${attr.unique.hostname}\"]\n check_restart {\n grace = \"1m\"\n ignore_warnings = false\n }\n }\n logs {\n max_files = 3\n max_file_size = 10\n }\n resources {\n memory = 100\n memory_max = 150\n }\n }\n }\n}\n```\n\nТеперь перейдем к конфигурации traefik, здесь лишь фрагмент конфига traefik.yml, мы добавляем еще один entrypoint для graylog.\n\n```\n…………\nentryPoints:\n web:\n address: \":80\"\n forwardedHeaders:\n insecure: true\n\n websecure:\n address: \":443\"\n http:\n middlewares:\n - sslheader@file\n - trimwww@file\n\n graylog:\n address: \":11111\"\n…………\n```\n\nТакже не забываем сконфигурировать порт для работы с graylog в конфиге job nomad для traefik, ниже указан фрагмент traefik job.\n\n```\njob \"traefik\" {\n datacenters = [\"a\", \"b\", \"c\"]\n\n…………………\n\n type = \"system\"\n \n group \"traefik\" {\n network {\n mode = \"host\"\n\n port \"http\" {\n static = 80\n to = 80\n host_network = \"private\"\n }\n \n port \"public\" {\n static = 443\n to = 443\n host_network = \"public\"\n }\n \n port \"ui\" {\n to = 8080\n host_network = \"private\"\n }\n\n port \"graylog\" {\n static = 11111\n to = \"11111\"\n host_network = \"private\"\n }\n\n }\n\n task \"traefik\" {\n driver = \"docker\"\n kill_timeout = \"30s\"\n leader = true\n\n………………\n\n config {\n image = \"traefik:v2.8.7\"\n force_pull = true\n ports = [\"http\", \"public\", \"ui\",\n \"graylog\"\n ]\n…………………\n \n}\n\n```\n\nТак выглядят entrypoints в веб-интерфейсе traefik.\n\n![](https://lh5.googleusercontent.com/mz-VGdWXCb8e5kk2aw3Qu8fcp3HrmCBv7BH5c2zlDJ2Q-B-xZfRd26WpeoDzCoQ-Kn9ldo7fMgoK6DUGWAvJjOLNhXmFQB5-MV_MMr9W2XtgGYeEclMuMV6i57VLj7wHUM-AjcoFKy0uvpg-zjRhauC1xRanuFZGUa62nnjt8mh0e9Lv_M7FiI_CSQ)\n\nКонфигурация роутера для graylog осуществляется в tcp router, вот как это выглядит в веб-интерфейсе traefik:\n\n![](https://lh4.googleusercontent.com/Ysxr2MAe0rRmJ0oB8A13O0N92gb8XX2kVsDvvRpEXv2PXxo6pWrK3o0TZthTuj9oXI61ZL7cULvAW4jpEGBaykShHkKOWJiO-lcL3D8xJOUEH7BTjLKtebrbUlw5ay41UNAXMVu9MiWNnYEdgM5ALW3yDkcMmPw0WpyXvcoIH51mfHj1ZywVkyPvgA)\n\nТакже нужно сконфигурировать сервис (в терминологии traefik), куда мы будем отправлять логи в graylog, именно в конфигурации сервиса в traefik указывается сам ip и порт graylog сервера.\n\n![](https://lh6.googleusercontent.com/OCl-uznt6dQa2ele5-nFX-ONbgIOzshTwTHIGLklhWQRFmJJ6am12UCE2lvcwZ8VSWYkVWq5DsabEMRFFPAwhByOU8GzDvvCapa_8HiQ4k7CIDczWzHwugvFNcbq-A8nskxOIS_bTmBhAtWvVij2GXIK__IOunMQWz7jONWL-mGkrdHvPvbz1i38sg)\n\nВ веб-интерфейсе graylog вот так конфигурируется input (в терминологии graylog), т.е. входная точка, которая будет принимать логи на стороне graylog. Тут видно, что тип input у нас Beats.\n\n![](https://lh5.googleusercontent.com/Re1exwPm1JsctCoXBboA5KERvxyg-4Z6t1KISMzWUIMIASXtHdF42_Y1sWljwjcRY4gUNaJMPVVQ4NNjNxjEYBoaqz1T4G-0KESu9WK7pErdLXupdtG4bIUX1yotqUAx2nwUEGc5QjAAa2XvN_erECMhvUl0ryFhEymRM-OFJJzba6ikGgPIKCvV3w)\n\nВот так выглядит сам веб-интерфейс graylog, где мы уже ищем конкретные логи:\n\n![](https://lh3.googleusercontent.com/MvroZBekyjT2buttPXlO_nNx39TWqTxUdPz1hJr6CMhgAowPtyEoRo9TnD2eih88Z3ZwU0Ew8dD1KSXOuM2sIGzrX9QwD6Vw42f3G1Dg3NaY6otdBxQg4MF2gP461Il8dNu9PiNixWtIyB-IX7phrq-c8DOSF7dZ5MCJv2DuCCPSZEBC4R2fI100Ag)\n\nВот так достаточно просто (конечно после нескольких десятков часов исследований) настраивается централизованный сбор логов. Мы пойдем посмотрим, что у нас там в логах, а вы пока stay tuned.",
167
+ "positive_reactions_count": 0,
168
+ "cover_image": null,
169
+ "tag_list": [
170
+ "graylog",
171
+ "infra",
172
+ "nomad",
173
+ "traefik"
174
+ ],
175
+ "canonical_url": "https://blog.rnds.pro/033-centralized-logging?utm_source=devto&utm_medium=post&utm_campaign=rnds",
176
+ "reading_time_minutes": 3,
177
+ "user": {
178
+ "name": "Samoilenko Yuri",
179
+ "username": "kinnalru",
180
+ "twitter_username": null,
181
+ "github_username": "kinnalru",
182
+ "user_id": 559891,
183
+ "website_url": "https://teletype.in/@jerry_ru",
184
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
185
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
186
+ },
187
+ "organization": {
188
+ "name": "RNDSOFT",
189
+ "username": "rnds",
190
+ "slug": "rnds",
191
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
192
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
193
+ }
194
+ },
195
+ {
196
+ "type_of": "article",
197
+ "id": 1213700,
198
+ "title": "Использование Scientist для рефакторинга критических участков Ruby on Rails приложения",
199
+ "description": "Перевод статьи “Using Scientist to Refactor Critical Ruby on Rails Code” Darren Broemmer от 18 мая...",
200
+ "published": true,
201
+ "published_at": "2022-09-30T12:19:06.000Z",
202
+ "slug": "ispolzovaniie-scientist-dlia-riefaktoringha-kritichieskikh-uchastkov-ruby-on-rails-prilozhieniia-3ckl",
203
+ "path": "/rnds/ispolzovaniie-scientist-dlia-riefaktoringha-kritichieskikh-uchastkov-ruby-on-rails-prilozhieniia-3ckl",
204
+ "url": "https://dev.to/rnds/ispolzovaniie-scientist-dlia-riefaktoringha-kritichieskikh-uchastkov-ruby-on-rails-prilozhieniia-3ckl",
205
+ "comments_count": 0,
206
+ "public_reactions_count": 0,
207
+ "page_views_count": 100,
208
+ "published_timestamp": "2022-10-07T16:05:14Z",
209
+ "body_markdown": "---\ntitle: Использование Scientist для рефакторинга критических участков Ruby on Rails приложения\npublished: true\ndate: 2022-09-30 12:19:06 UTC\ntags: rails, ruby\ncanonical_url: https://blog.rnds.pro/032-scientist?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n[Перевод статьи _“Using Scientist to Refactor Critical Ruby on Rails Code”_](https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html) \n[Darren Broemmer](https://blog.appsignal.com/authors/darren-broemmer) от 18 мая 2022 года.\n\n![](https://img4.teletype.in/files/fe/ce/fece8ad1-1d4a-45ab-924c-b3d16342aa98.jpeg)\n\nПопросите любого программиста просмотреть ключевые участки продуктового кода, и он обязательно укажет на несколько моментов, требующих рефакторинга. Почему же так много плохого, ненадежного или непонятного кода продолжает работать в продуктовом окружении?\n\nОтвет прост: инженеры боятся его трогать. Задачи для рефакторинга обнаруживаются и добавляются в бэклог, но редко попадают в текущий спринт.\n\nДля этого есть множество причин. Код мог быть написан программистом, давно покинувшим команду и теперь в нём никто не разбирается. Бывает что этот код имеет решающее значение для бизнеса и из-за этого никто не хочет нести ответственность за потенциальный сбой или потерю дохода.\n\nВ данном посте мы рассмотрим, как можно использовать гем [Scientist](https://github.com/github/scientist) для уверенной миграции, рефакторинга и изменения критического продуктового Ruby-кода.\n\nНо сначала вы спросите — а нельзя ли использовать тесты для поиска ошибок?\n\n## Это ведь то самое для чего нужны тесты в Rails, верно?\n\nИ да и нет. Часто бывает трудно получить полную уверенность в новых изменениях до развертывания. Допустим модульные (Unit) и системные тесты проходят. Этого достаточно?\n\nРеальность такова, что ничто не может заменить реальный мир, то есть продуктовую эксплуатацию. А если качество тестовых данных плохое или тесты отсутствуют? А будет ли новое ПО работать достаточно хорошо, чтобы справляться с нагрузкой?\n\nКоманды с публичными сервисами иногда обнаруживают, что им нужно решать проблемы «совместимости с ошибками» (bugwards compatibility). Когда ошибка существует в рабочей среде некоторое время, клиенты могут обходить её таким способом, который зависит от привычного неправильного поведения. Клиенты часто используют ваш софт неожиданным образом.\n\n## с Scientist можно следить за правками в в Ruby и Rails сразу в боевом окружении\n\nЕсли продуктовое окружение это лучшее место, где можно обрести уверенность в своих правках, можно подумать о том, чтобы понаблюдать за поведением кода именно там. Поначалу это может показаться пугающим, ведь идея «тестирования в бою» противоречит классическим методам разработки программного обеспечения.\n\nОднако хорошая новость заключается в том, что это можно легко и безопасно сделать в Ruby и Rails с помощью гема [Scientist](https://github.com/github/scientist). Название утилиты основано на научном методе проведения экспериментов для проверки гипотез. В данном случае наша гипотеза состоит в том, что новый код работает, а его использование это “эксперимент”.\n\nПричина, по которой мы можем безопасно использовать этот подход, заключается в том, что в экспериментах по-прежнему используется результат существующего кода. Новый код оценивается только в целях наблюдения и сравнения, как на точность, так и на производительность. Мы уменьшаем проблемы с тестовым покрытием, которые обсуждались ранее, оценивая производительность с использованием реальных данных и параметров в боевом окружении. Эксперименты обычно проводят с определённой частотой, чтобы свести к минимуму влияние на производительность. Однако при желании можно оценить и каждый вызов.\n\nДавайте теперь кратко рассмотрим, как Scientist работает с техникой “[Branch by Abstraction](https://martinfowler.com/bliki/BranchByAbstraction.html)”.\n\n## Паттерн “Branch by Abstraction” в геме Scientist\n\nРабота Scientist начинается с паттерна [Branch by Abstraction](https://martinfowler.com/bliki/BranchByAbstraction.html), описанного Мартином Фаулером как «постепенное крупномасштабное изменение программной системы».\n\nМы вводим уровень абстракции, чтобы изолировать обновляемый код. Этот уровень решает, какую реализацию использовать, чтобы эксперимент был прозрачен для остальной части системы. Данный метод связан с использованием флага (feature flag), который определяет какая из ветвей кода будет исполнена.\n\n[Гем Scientist, созданный в Github](https://github.blog/2016-02-03-scientist/), реализует этот паттерн с помощью “эксперимента”. Существующий код называется “контрольным”, а новая реализация — “кандидатом”. Обе части кода выполняются в случайном порядке, но клиенту возвращается только результат ”контрольной” части.\n\n## Использование Scientist для рефакторинга сервиса в Ruby\n\nРассмотрим [Ruby-сервис](https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L20), который возвращает наибольший простой множитель для заданного числа. Предположим, мы определили способы оптимизации для сокращения необходимого набора кандидатов, что ускорит работу сервиса.\n\nОднако владельцы сервиса хотят быть уверены, что при оптимизации не вкралась ошибка. Они также хотят увидеть все улучшения производительности. Давайте напишем [следующий код](https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L3), для вызова этого метода:\n\n```\nrequire 'scientist'\n \ndef largest_prime_factor(number)\n science \"prime-factors\" do |experiment|\n experiment.use { find_largest_prime_factor(number) } # old way\n experiment.try { improved_largest_prime_factor(number) } # new way\n end # returns the control value\nend\n```\n\nВ этот момент вызывается только выражение `use` (контрольное). Но чтобы сделать эксперимент полезным, надо определить пользовательский класс `Experiment`, чтобы включить его и опубликовать результаты (в данном случае просто логирование). Scientist генерирует очень полезные данные, но по умолчанию ничего с ними не делает. Эта часть остается на ваше усмотрение.\n\n```\nrequire 'scientist/experiment'\nrequire 'pp'\n \nclass MyExperiment\n include Scientist::Experiment\n \n attr_accessor :name\n \n def initialize(name)\n @name = name\n end\n \n def enabled?\n true\n end\n \n def raised(operation, error)\n p \"Operation '#{operation}' failed with error '#{error.inspect}'\"\n super # will re-raise\n end\n \n def publish(result)\n pp result\n end\nend\n```\n\nРезультаты эксперимента будут зарегистрированы, и мы сможем со временем вносить улучшения на основе анализа. Как только новый код будет соответствовать требованиям и доверие к нему станет высоким, осуществим переход на новую реализацию, просто заменив код делегированием в новую реализацию.\n\n## LabTech для упрощения использования Scientist в Ruby on Rails\n\nЕсть [гем LabTech](https://github.com/RealGeeks/lab_tech), который может помочь настроить Scientist в Rails приложении и удобнее обрабатывать результаты.\n\nПриложения, использующие `AppSignal`, могут использовать [вспомогательный инструментарий](https://docs.appsignal.com/ruby/instrumentation/instrumentation.html) `Appsignal.instrument`, чтобы отслеживать, сколько времени требуется для выполнения событий Scientist. Обернув в него код эксперимента, можно увидеть, как события появляются в `AppSIgnal`.\n\nТеперь вернемся к LabTech — пример ниже просто принимает число для разложения.\n\n![](https://img2.teletype.in/files/9d/db/9ddbfb11-6a14-40c1-b318-5ddda1392c70.png)\n\nПриступить к работе легко, если у вас есть доступ к консоли. Сначала надо добавить гем LabTech в Gemfile и запустить `bundle install`.\n\n```\ngem 'lab_tech'\n```\n\nРезультаты и конфигурация эксперимента хранятся в БД, поэтому требуется миграция.\n\n```\nrails lab_tech:install:migrations db:migrate\n```\n\nУровень абстракции тот же, за исключением того, что используется модуль LabTech. Полный код доступен на GitHub.\n\n```\ndef largest_prime_factor(number)\n LabTech.science \"prime-factors\" do |experiment|\n ...\n end\nend\n```\n\nНа данный момент эксперимент отключен, поэтому воспользуемся консолью, чтобы включить его во всех случаях или настроить процент исполнения.\n\n```\nbin/rails console\nLabTech.enable \"prime-factors\"\nLabTech.enable \"prime-factors\", percent: 5\n```\n\nТеперь можно запустить тесты, и эксперимент будет выполнен. Для текстового представления результатов есть следующие команды в консоли Rails.\n\n```\nLabTech.summarize_results \"prime-factors\"\nLabTech.summarize_errors \"prime-factors\"\n```\n\nПосле нескольких успешных прогонов и одной синтетической ошибки получим пример того, как выглядит сводка результатов. Существует обзор успехов и ошибок, а также диаграмма ASCII, показывающая различия в производительности.\n\n```\n----------------------------------------------------------------------------\nExperiment: prime-factors\n----------------------------------------------------------------------------\nEarliest results: 2022-04-27T02:42:45Z\nLatest result: 2022-05-01T17:27:39Z (5 days)\n \n3 of 4 (75.00%) correct\n1 of 4 (25.00%) mismatched\n \nMedian time delta: +0.000s (90% of observations between +0.000s and +0.000s)\n \nSpeedups (by percentiles):\n 0% [· █] +2.4x faster\n 5% [· █] +2.4x faster\n 10% [· █] +2.4x faster\n 15% [· █] +2.4x faster\n 20% [· █] +2.4x faster\n 25% [· █] +2.4x faster\n 30% [· █] +2.4x faster\n 35% [· █] +2.4x faster\n 40% [· █] +2.4x faster\n 45% [· █] +2.4x faster\n 50% [· · · · · · ·· · · · · · · · · █ ·· · · · · ·] +2.4x faster\n 55% [· █] +2.4x faster\n 60% [· █] +2.4x faster\n 65% [· █] +2.4x faster\n 70% [· █] +6.9x faster\n 75% [· █] +6.9x faster\n 80% [· █] +6.9x faster\n 85% [· █] +6.9x faster\n 90% [· █] +6.9x faster\n 95% [· █] +6.9x faster\n 100% [· █] +6.9x faster\n----------------------------------------------------------------------------\n```\n\nДля лёгкого и удобного анализа результатов есть гем [Blazer](https://github.com/ankane/blazer). Он прост в установке и позволяет выполнять SQL-запросы. Запрос здесь показывает, что реализация-кандидат значительно быстрее оригинала.\n\n![](https://img3.teletype.in/files/a1/92/a192082e-2e7f-4b39-a978-d4f9857ed56a.png)\n\nВ примере нашего сервиса, возвращающего наибольший простой делитель, ускорение в улучшенной реализации происходит за счет эвристики, исключающей некоторые возможные факторы, которые необходимо учитывать. Поскольку мы рассматриваем большие числа и находим простой множитель, мы можем прекратить поиск после того, как доберемся до целевого числа, делённого на этот множитель. Новый код добавляет только [одну инструкцию](https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L38) для достижения этой цели.\n\nМы также можем увидеть сокращение времени выполнения используя Blazer.\n\n![](https://img3.teletype.in/files/6b/06/6b06ffce-40d8-42ed-b1e0-7a9a4190a876.png)\n\n## Варианты использования и ограничения Scientist\n\nОптимальное использование Scientist включает задачи поиска, вычисления и код, который не имеет побочных эффектов. Код, включающий транзакционные обновления или внешние интеграции, такие как электронная почта, не совсем вписывается в модель, поскольку изменяемое действие выполняется дважды (как в старой, так и в новой реализации).\n\nЭто не тривиальное ограничение. Оно блокирует несколько вариантов использования. Однако есть обходные пути, если эксперимент очень важен. Стоит подумать имеют ли значение побочные эффекты или дублирование или они не являются проблемой. Например, в некоторых случаях может не иметь значения, будут ли отправлены два электронных письма во время оценки. Другой вариант — сделать так, чтобы новый код определял результат, но не сохранял его. Это не позволит сравнить производительность, но позволит проверить точность.\n\nДругие ограничения проистекают из того, что Scientist фокусируется на возвращаемых значениях. В некоторых случаях действительные результаты могут отличаться с течением времени, независимо от того, включают ли они просто временные метки в ответ или изменяются сторонние факторы. Во многих случаях мы можем написать пользовательскую логику сравнения в эксперименте, чтобы проверить точность, выходящую за рамки обычного сравнения строк.\n\nНаконец, ограничение LabTech заключается в том, что он не был портирован на Rails 7 на момент написания.\n\n## Лучшие практики для эффективных Scientist-экспериментов в Rails\n\nПри проведении экспериментов стоит учитывать следующие моменты:\n\n- В Rails-проектах Scientist может быть настроен либо в инициализаторе, либо в оболочке, такой как гем Rails LabTech. Большинство приложений Rails уже имеют базу данных, поэтому LabTech использует ActiveRecord для хранения результатов.\n- Чтобы не замедлять разработку и тестирование, включайте эксперимент только в промежуточной (staging) и продуктовой средах.\n- Чтобы свести к минимуму любое потенциальное влияние на продуктовую среду, запускайте эксперимент только для некоторого процента запросов. LabTech поддерживает это из коробки как необязательный параметр при включении эксперимента (изначально он отключен по умолчанию). Используя чистый Scientist, эту логику легко реализовать через `enabled?` метод.\n- Некоторая логика требует больших ресурсов, поэтому хорошей отправной точкой может быть низкая частота дискретизации. По мере того, как вы обретете уверенность в результатах, увеличивайте процент оцениваемых запросов.\n- Вы можете добавить атрибуты контекста, чтобы получить максимальную отдачу от результатов. В качестве контекста эксперимента может быть задан хэш с параметрами, который затем становится доступным в опубликованных результатах, например:\n\n```\nexperiment.context :user => user\n```\n\n## Подводя итоги: наблюдайте и контролируйте свое приложение Ruby с помощью Scientist\n\nВ этом посте мы рассмотрели, как использовать гем Scientist для изменения, переноса и рефакторинга кода Ruby в продуктовой среде.\n\nМы рассмотрели место Scientist в паттерне Branch by Abstraction, и погрузились в рефакторинг. Затем увидели, как LabTech помогает со сбором результатов и конфигурацией Scientist.\n\nМы коснулись некоторых ограничений Scientist, прежде чем, наконец, изложить несколько лучших практик.\n\nВам необходимо наблюдать и контролировать то, что происходит в системе. Интегрируйте Scientist в процесс разработки, чтобы с большей уверенностью вносить критические изменения в код Ruby.\n\nHappy coding!\n\n[Перевод статьи _“Using Scientist to Refactor Critical Ruby on Rails Code”_](https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html) \n[Darren Broemmer](https://blog.appsignal.com/authors/darren-broemmer) от 18 мая 2022 года.\n",
210
+ "positive_reactions_count": 0,
211
+ "cover_image": null,
212
+ "tag_list": [
213
+ "rails",
214
+ "ruby"
215
+ ],
216
+ "canonical_url": "https://blog.rnds.pro/032-scientist?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
217
+ "reading_time_minutes": 3,
218
+ "user": {
219
+ "name": "Samoilenko Yuri",
220
+ "username": "kinnalru",
221
+ "twitter_username": null,
222
+ "github_username": "kinnalru",
223
+ "user_id": 559891,
224
+ "website_url": "https://teletype.in/@jerry_ru",
225
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
226
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
227
+ },
228
+ "organization": {
229
+ "name": "RNDSOFT",
230
+ "username": "rnds",
231
+ "slug": "rnds",
232
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
233
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
234
+ }
235
+ },
236
+ {
237
+ "type_of": "article",
238
+ "id": 1197896,
239
+ "title": "FTP? Нет, не слышал",
240
+ "description": "Сейчас будет немного боли и радости от победы над FTP. Мы много и упорно работаем со СМЭВ3. Никакой...",
241
+ "published": true,
242
+ "published_at": "2022-09-20T06:28:10.740Z",
243
+ "slug": "ftp-niet-nie-slyshal-503m",
244
+ "path": "/rnds/ftp-niet-nie-slyshal-503m",
245
+ "url": "https://dev.to/rnds/ftp-niet-nie-slyshal-503m",
246
+ "comments_count": 0,
247
+ "public_reactions_count": 0,
248
+ "page_views_count": 41,
249
+ "published_timestamp": "2022-09-20T06:28:10Z",
250
+ "body_markdown": "Сейчас будет немного боли и радости от победы над [FTP](https://ru.wikipedia.org/wiki/FTP). Мы много и упорно работаем со [СМЭВ3](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D0%BC%D0%B5%D0%B6%D0%B2%D0%B5%D0%B4%D0%BE%D0%BC%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE_%D1%8D%D0%BB%D0%B5%D0%BA%D1%82%D1%80%D0%BE%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE_%D0%B2%D0%B7%D0%B0%D0%B8%D0%BC%D0%BE%D0%B4%D0%B5%D0%B9%D1%81%D1%82%D0%B2%D0%B8%D1%8F). Никакой магии: обычный SOAP поверх HTTP, быстро, надёжно - все банки и гос. учреждения знают, как это делается. И вдруг (никогда такого не было, и вот опять) оказалось, что нам надо забирать большие файлы с FTP, предоставляемого СМЭВ.\n\n> Дисклеймер: постарайтесь воздерживаться от использования FTP - это технология немного устарела и не отвечает современным требованиям удобства и безопасности.\n\nТем не менее, надо - значит надо. СМЭВ располагается в защищенной инфраструктуре электронного правительства, куда мы ходим через шлюзы, расположенные в определённой инфраструктуре. В этих шлюзах нет ничего необычного - HTTP, балансировка, отказоустойчивость через избыточность. \n\n## В чём проблема?\nШлюзы есть, прокси есть, с чего бы нам бояться [FTP](https://ru.wikipedia.org/wiki/FTP)? Оказывается, бояться есть с чего - FTP использует не один порт, а множество динамически открываемых, причём есть особенный режим (active), когда сервер инициирует соединение к клиенту - это еще со стародавних времён, когда никакого NAT не было. \n\nВ результате мы не можем работать с этим FTP \"по простому\" - нам требуется как-то обеспечить сетевую связность и наши любимые и привычные инструменты нам в этом помочь не могут 😠 . Надо думать.\n\n> FTP (англ. File Transfer Protocol) — протокол передачи файлов по сети, появившийся в 1971 году задолго до HTTP и даже до TCP/IP, благодаря чему является одним из старейших прикладных протоколов. Изначально FTP работал поверх протокола NCP, на сегодняшний день широко используется для распространения ПО и доступа к удалённым хостам. \n\nДополнительной особенностью в нашем случае является не просто наличие шлюза, через который необходимо обращаться в СМЭВ, а целых 2:\n\n- Gateway 1 - внутри облака\n- Gateway 2 - в специальной инфраструктуре с сетевой связностью со СМЭВ \n\nвот так выглядит наш каскад проксей:\n![proxy cascade](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dfy0d3uo0ucyhrns2w7k.png)\n\n## Решение\nЕсли есть проблема, то должно быть и решение. Так и оказалось - есть несколько (наверное даже много) программных продуктов, связанных с проксированием FTP:\n\n- [3proxy](https://3proxy.ru/) - целый набор проксей. Прочитали доку, запустили - не завелось.\n- [Squid](https://en.wikipedia.org/wiki/Squid_(software)) - большой, надёжный, кеширующий прокси, на все случаи жизни. Для нас оказался слишком избыточен в настройке. И в контексте FTP практически нет документации.\n- [ftp-proxy](http://tdkare.ru/sysadmin/index.php/Ftp-proxy) (ftpproxy) - стандартный пакет в дистрибутиве Debian/Ubuntu, и он взлетел со свитом.\n\nПоскольку всё наше облако оркестрируется с помощью [Nomad](https://www.nomadproject.io/), то и этот компонент мы завернули docker.\n\nDockerfile\n```dockerfile\nFROM debian:bullseye-slim\n\nRUN apt-get update \nRUN apt-get install -y \\\n ftp-proxy gettext\n\nRUN mkdir -p /tmp/logs/\nADD ./ftp-proxy.conf.in /tmp/ftp-proxy.conf.in\n\nENV DESTINATION_ADDRESS=127.0.0.1\nENV DESTINATION_PORT=21\nENV PROXY_PORT=21\n\nENV PROXY_LOG_LEVEL=DBG\n\nEXPOSE 21\nCMD envsubst < /tmp/ftp-proxy.conf.in > /etc/proxy-suite/ftp-proxy.conf && exec /usr/sbin/ftp-proxy -n -d \nftp-proxy.conf\n```\n\nВ конфиге можно задать миллион других настроек, но в нашем случае достаточно только этого:\n```\n[-Global-]\n\nDestinationAddress ${DESTINATION_ADDRESS}\nDestinationPort ${DESTINATION_PORT}\n\nPort ${PROXY_PORT}\n\nLogDestination | cat\nLogLevel ${PROXY_LOG_LEVEL}\n```\n\n## nomad\nМы катаем джобы с помощью ансибла и используем его шаблонизацию, так что тут я приведу уже финальный результат препроцессинга:\n\n```hcl\nvariable \"instance\" {\n type = string\n default = \"prod\"\n}\n\njob \"ftp-proxy-prod\" {\n datacenters = [\"zone-a\", \"zone-b\", \"zone-c\"]\n namespace = \"default\"\n\n constraint {\n attribute = \"${meta.gate}\"\n value = \"true\"\n }\n\n update {\n max_parallel = 1\n health_check = \"checks\"\n min_healthy_time = \"10s\"\n healthy_deadline = \"3m\"\n progress_deadline = \"6m\"\n auto_revert = true\n auto_promote = false\n canary = 0\n }\n\n type = \"system\"\n\n group \"ftp-proxy\" {\n\n network {\n port \"ftp\" {\n static = 21\n host_network = \"private\"\n } \n }\n\n task \"ftp-proxy-prod\" {\n driver = \"docker\"\n kill_timeout = \"30s\"\n leader = true\n\n env {\n DESTINATION_ADDRESS = \"10.0.0.34\"\n DESTINATION_PORT = \"8878\"\n PROXY_PORT = \"21\"\n PROXY_LOG_LEVEL = \"DBG\"\n }\n\n restart {\n attempts = 3\n interval = \"5m\"\n delay = \"15s\"\n mode = \"fail\"\n }\n\n config {\n image = \"YOURIMAGE:latest\"\n force_pull = true\n network_mode = \"host\"\n\n auth {\n username = \"USER\"\n password = \"PASS\"\n }\n }\n\n service {\n name = \"prod-ftp\"\n tags = [ \"ftp-proxy\", \"prod\" ]\n \n port = \"ftp\"\n \n check {\n type = \"tcp\"\n port = \"ftp\"\n interval = \"2s\"\n timeout = \"5s\"\n } \n \n check_restart {\n limit = 3\n grace = \"10s\"\n ignore_warnings = false\n } \n }\n\n logs {\n max_files = 5\n max_file_size = 50\n }\n\n resources {\n memory = 100\n memory_max = 200\n cpu = 200\n }\n }\n }\n\n\n}\n\n```\n\n```bash\nnomad job run -detach ftp-proxy.hcl\n```\n\n## Конец\nВсем спасибо - всё работает, но если у кого-то есть замечания, связанные с данным решением - мы с огромным интересом их рассмотрим 🤞.\n",
251
+ "positive_reactions_count": 0,
252
+ "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4xvgxity0fxelhw4pp5.jpeg",
253
+ "tag_list": [
254
+ "nomad",
255
+ "ftp",
256
+ "docker",
257
+ "webdev"
258
+ ],
259
+ "canonical_url": "https://blog.rnds.pro/031-ftp-proxy",
260
+ "reading_time_minutes": 2,
261
+ "user": {
262
+ "name": "Samoilenko Yuri",
263
+ "username": "kinnalru",
264
+ "twitter_username": null,
265
+ "github_username": "kinnalru",
266
+ "user_id": 559891,
267
+ "website_url": "https://teletype.in/@jerry_ru",
268
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
269
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
270
+ },
271
+ "organization": {
272
+ "name": "RNDSOFT",
273
+ "username": "rnds",
274
+ "slug": "rnds",
275
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
276
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
277
+ }
278
+ },
279
+ {
280
+ "type_of": "article",
281
+ "id": 1169269,
282
+ "title": "Rapidity: распределённый rate limiting",
283
+ "description": "Когда ваш продукт начинает активно использоваться, то перед вами обязательно встаёт вопрос...",
284
+ "published": true,
285
+ "published_at": "2022-08-17T05:14:09.300Z",
286
+ "slug": "rapidity-raspriedielionnyi-rate-limiting-48g4",
287
+ "path": "/rnds/rapidity-raspriedielionnyi-rate-limiting-48g4",
288
+ "url": "https://dev.to/rnds/rapidity-raspriedielionnyi-rate-limiting-48g4",
289
+ "comments_count": 0,
290
+ "public_reactions_count": 0,
291
+ "page_views_count": 21,
292
+ "published_timestamp": "2022-08-17T05:14:09Z",
293
+ "body_markdown": "Когда ваш продукт начинает активно использоваться, то перед вами обязательно встаёт вопрос масштабирования, а вслед за ним и проблема ограничения доступа к чему-нибудь: [Rate Limiting](https://en.wikipedia.org/wiki/Rate_limiting).\n\nRNDSOFT не исключение, поэтому в этой статье мы расскажем небольшую историю и поделимся своим инструментом: [https://blog.rnds.pro/030-rapidity](https://blog.rnds.pro/030-rapidity?utm_source=devto&utm_medium=post&utm_campaign=rnds)\n\n\n![rapidity](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uratxhkufh5ygl3n72pj.jpeg)",
294
+ "positive_reactions_count": 0,
295
+ "cover_image": null,
296
+ "tag_list": [
297
+ "ruby",
298
+ "rails",
299
+ "distributedsystems",
300
+ "microservices"
301
+ ],
302
+ "canonical_url": "https://blog.rnds.pro/030-rapidity",
303
+ "reading_time_minutes": 1,
304
+ "user": {
305
+ "name": "Samoilenko Yuri",
306
+ "username": "kinnalru",
307
+ "twitter_username": null,
308
+ "github_username": "kinnalru",
309
+ "user_id": 559891,
310
+ "website_url": "https://teletype.in/@jerry_ru",
311
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
312
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
313
+ },
314
+ "organization": {
315
+ "name": "RNDSOFT",
316
+ "username": "rnds",
317
+ "slug": "rnds",
318
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
319
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
320
+ }
321
+ },
322
+ {
323
+ "type_of": "article",
324
+ "id": 1169264,
325
+ "title": "Rails 7: три мощных ответа JavaScript’у в 2021+",
326
+ "description": "https://blog.rnds.pro/026-three-great-answers Перевод статьи \"Rails 7 will have three great answers...",
327
+ "published": true,
328
+ "published_at": "2022-08-17T05:01:24.305Z",
329
+ "slug": "rails-7-tri-moshchnykh-otvieta-javascriptu-v-2021-50oi",
330
+ "path": "/rnds/rails-7-tri-moshchnykh-otvieta-javascriptu-v-2021-50oi",
331
+ "url": "https://dev.to/rnds/rails-7-tri-moshchnykh-otvieta-javascriptu-v-2021-50oi",
332
+ "comments_count": 3,
333
+ "public_reactions_count": 1,
334
+ "page_views_count": 117,
335
+ "published_timestamp": "2022-08-17T05:01:24Z",
336
+ "body_markdown": "[https://blog.rnds.pro/026-three-great-answers](https://blog.rnds.pro/026-three-great-answers?utm_source=devto&utm_medium=post)\n\n_Перевод статьи \"Rails 7 will have three great answers to JavaScript in 2021+\"_\n_David Heinemeier Hansson (DHH) от 6 сентября 2021 года._\n\n![rails 7](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1fkftwj82fdawp5vouog.jpeg)",
337
+ "positive_reactions_count": 1,
338
+ "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4pd4klv1on1fmwi0kwz.jpeg",
339
+ "tag_list": [
340
+ "rails",
341
+ "ruby",
342
+ "webdev",
343
+ "r"
344
+ ],
345
+ "canonical_url": "https://blog.rnds.pro/026-three-great-answers",
346
+ "reading_time_minutes": 1,
347
+ "user": {
348
+ "name": "Samoilenko Yuri",
349
+ "username": "kinnalru",
350
+ "twitter_username": null,
351
+ "github_username": "kinnalru",
352
+ "user_id": 559891,
353
+ "website_url": "https://teletype.in/@jerry_ru",
354
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
355
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
356
+ },
357
+ "organization": {
358
+ "name": "RNDSOFT",
359
+ "username": "rnds",
360
+ "slug": "rnds",
361
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
362
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
363
+ }
364
+ },
365
+ {
366
+ "type_of": "article",
367
+ "id": 1169260,
368
+ "title": "Вы хочете песен? Их есть у меня! (Poison Message #2)",
369
+ "description": "Самое время рассмотреть “достаточно хороший” алгоритм для борьбы с Poison Message. Здесь будет уже...",
370
+ "published": true,
371
+ "published_at": "2022-08-17T04:54:00.000Z",
372
+ "slug": "vy-khochietie-piesien-ikh-iest-u-mienia-poison-message-2-d0m",
373
+ "path": "/rnds/vy-khochietie-piesien-ikh-iest-u-mienia-poison-message-2-d0m",
374
+ "url": "https://dev.to/rnds/vy-khochietie-piesien-ikh-iest-u-mienia-poison-message-2-d0m",
375
+ "comments_count": 0,
376
+ "public_reactions_count": 0,
377
+ "page_views_count": 54,
378
+ "published_timestamp": "2022-08-17T04:54:00Z",
379
+ "body_markdown": "Самое время рассмотреть “достаточно хороший” алгоритм для борьбы с Poison Message. Здесь будет уже специфика RabbitMQ и к Apache Kafka она не применима, точнее применима только частично - но это уже совсем другая история. \n\n> В [первой части мы разобрали несколько примеров](https://blog.rnds.pro/018-posion1?utm_source=devto&utm_medium=post) и сформулировали проблему Poison Message, здесь же рассмотрим сам алгоритм её решения.\n\n[https://blog.rnds.pro/019-poison2](https://blog.rnds.pro/019-poison2?utm_source=devto&utm_medium=post)\n\n\n![Poison Message](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tb1h6qlxw7s56y08os3y.jpeg)\n",
380
+ "positive_reactions_count": 0,
381
+ "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqmgms4wvdty7hqzbr254.jpeg",
382
+ "tag_list": [
383
+ "rabbitmq",
384
+ "microservices",
385
+ "eventdriven",
386
+ "ruby"
387
+ ],
388
+ "canonical_url": "https://blog.rnds.pro/019-poison2",
389
+ "reading_time_minutes": 1,
390
+ "user": {
391
+ "name": "Samoilenko Yuri",
392
+ "username": "kinnalru",
393
+ "twitter_username": null,
394
+ "github_username": "kinnalru",
395
+ "user_id": 559891,
396
+ "website_url": "https://teletype.in/@jerry_ru",
397
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
398
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
399
+ },
400
+ "organization": {
401
+ "name": "RNDSOFT",
402
+ "username": "rnds",
403
+ "slug": "rnds",
404
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
405
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
406
+ }
407
+ },
408
+ {
409
+ "type_of": "article",
410
+ "id": 1175972,
411
+ "title": "Чистка build-агентов Gitlab",
412
+ "description": "Оригинал: https://blog.rnds.pro/030-gitlab-janitor Спешим поделиться с вами нашим инструментом для...",
413
+ "published": true,
414
+ "published_at": "2022-08-11T09:52:28.000Z",
415
+ "slug": "chistka-build-aghientov-gitlab-2lil",
416
+ "path": "/rnds/chistka-build-aghientov-gitlab-2lil",
417
+ "url": "https://dev.to/rnds/chistka-build-aghientov-gitlab-2lil",
418
+ "comments_count": 0,
419
+ "public_reactions_count": 0,
420
+ "page_views_count": 328,
421
+ "published_timestamp": "2022-08-26T08:48:27Z",
422
+ "body_markdown": "---\ntitle: Чистка build-агентов Gitlab\npublished: true\ndate: 2022-08-11 09:52:28 UTC\ntags: infra, gitlab, cleaner, devops\ncanonical_url: https://blog.rnds.pro/030-gitlab-janitor?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n![](https://img4.teletype.in/files/37/38/37383f40-cc8b-4523-b616-a1705311242e.jpeg)\n\n_Оригинал: [https://blog.rnds.pro/030-gitlab-janitor](https://blog.rnds.pro/030-gitlab-janitor?utm_source=devto&utm_medium=feed_rss&utm_campaign=rnds)_\n\nСпешим поделиться с вами нашим инструментом для поддержания чистоты и порядка на наших (RNDSOFT) сборочных серверах [gitlab-janitor](https://github.com/RND-SOFT/gitlab-janitor). О том, как мы к нему пришли, и каков первый опыт - далее по тексту.\n\nС каждым годом инфраструктура RNDSOFT, обеспечивающая процессы CI/CD, растёт. Появляются новые проекты, усложняются процессы (pipelines) сборки и тестирования, растет количество сборщиков (build agents), и всё это приносит дополнительные накладные расходы. Сегодня мы рассмотрим конкретную проблему - чистку/освобождение ресурсов на самих сборочных серверах. Мы используем Gitlab и несколько [Gitlab runners](https://docs.gitlab.com/runner/) с докером ([docker executor](https://docs.gitlab.com/runner/executors/docker.html)) под капотом. Вот с проблем и начнём.\n\n## Проблемы\n\nВся терминология будет опираться на [Gitlab](https://docs.gitlab.com/), но всё это применимо и к другим решениям, опирающимся на docker.\n\nТиповой процесс сборки и доставки состоит из 4х больших этапов (stages), по несколько задач (jobs) в каждом:\n\n- сборка docker-образа (обычно одного на проект, но бывает и больше).\n- тестирование. Тут мы используем подход, когда тестируем не сам код, а весь контейнер. Конечно, есть юнит-тесты, интеграционные, графические и прочие, но запускаем мы их непосредственно внутри боевого контейнера. А что? У нас ruby - можем себе позволить 💎 :)\n- тэгирование (image promoting). Этап в зависимости от результатов тестов и QA перетегируется во что-то вроде `stable` или `release`, или как-то еще, в зависимости от конкретного проекта, команды и принятых процессов.\n- доставка (deploy). Тут всё как обычно - дев/тест стенды, QA-стенды, динамические стенды для [MR](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) и всякое разное вроде документации.\n<section style=\"background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);\">\n <p id=\"kswC\">Давно хочу написать отдельно про процесс image promotiong, про то, как мы таскаем образы между стадиями, но руки не доходят. Если кого сильно интересует - пишите в комментариях, и мне придётся найти в себе силы :)</p>\n </section>\n\nВ результате описанных выше процессов (особенно тестирования) на сборщиках остаётся много мусора, именно это является проблемой. Далее по порядку.\n\n### Подвисшие контейнеры (dangling containers)\n\nТут речь, конечно, идёт не о повисании ПО внутри контейнера, а о самих \"ненужных\" контейнерах. Например, для графических тестов поднимается целый стек: Postgres, Chrome, Selenium, само приложение, контейнер, из которого запускаются тесты, Redis и бог еще знает что. Если весь процесс (pipeline) или непосредственно задача (job) будут прерваны, то никто не погасит за нас эти контейнеры, и они продолжат висеть и потреблять ресурсы. Проблема!\n\n### Ненужные образы\n\nВ процессе сборки имя образа претерпевает примерно следующие изменения: image:$SHA -> image:stable -> image:release. Конечно, это примерно, шагов может больше или именования другие, но сути это не меняет. При последовательных комитах ~~сратые~~ старые образы остаются лежать мёртвым грузом.\n\n<section style=\"background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);\">\n <p id=\"LCgm\">В день мы генерим примерно 75GB образов. ~25GB на каждом из трех сборщиков. И это летом, когда многие в отпуске :)</p>\n </section>\n\nСитуация усугубляется тем, что один и тот же образ находится (с максимальной вероятностью) сразу на всех сборщиках, поскольку разные типы тестов запускаются параллельно и попадают на все сборщики и занимают x3 места. Проблема! Дважды проблема, поскольку с образами всё не так просто и очень интересно - об этом ниже.\n\n### Безымянные и кеширующие тома (unnamed and cache volumes)\n\nМногие контейнеры при создании аллоцируют в докере временные анонимные тома/диски (мы же всегда про докер говорим, верно?) и оставляют их после себя. Также сам Gitlab, если вы не используете распределённые кеш (shared cache) [на базе S3](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscaches3-section), всё равно создаёт кеш-тома, [если ему явно не запретить это делать](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdocker-section) через `disable_cache`. Проблема!\n\n* * *\n\nКонечно, эти проблемы не простой \"мусор\" - старые образы могут ускорить последующую сборку, кеш-тома также теоретически могут ускорять сборку, так что они не совсем бесполезны. Однако, проблемы ускорения и кеширования мы решаем немного иначе - с помощью определённых схем именования образов, используя buildkit, который умеет подтягивать кешированные слои прямо из реестра образов (в нашем случае harbor) и пр.\n\n## Что же делать?\n\nПроблемы появляются не сразу. Когда-то у нас был один сборщик, там вообще не было проблем с перетягиванием образов между этапами, а образы чистились через `docker rmi` на последнем шаге процесса. Когда сборщиков стало больше, и на них докинули ресурсы, пришлось немного усложнить схему - сначала были bash-скрипты, которые по расписанию удаляли образы по шаблону имени. Затем периодически стали использовать `docker system prune`. Но эти решения очень негибкие, плохо поддерживаются и масштабируются и обладают фундаментальной проблемой - частыми кеш-промахами (cache miss), что периодически сильно тормозило процессы (pipelines). И вот однажды терпеть это стало невозможно 😜 :)))\n\nНам нужно хорошее решение, желательно с баристой и массажисткой! Встречаем - [gitlab-janitor](https://github.com/RND-SOFT/gitlab-janitor)!\n\n## Gitlab-janitor\n\nОсновными задачами были:\n\n- консолидация всех чисток сборщика в одном месте;\n- уменьшение кеш-промахов, насколько это возможно;\n- простота работы.\n\nДля чистки места на сборщиках [уже есть проект gitlab-runner-docker-cleanup](https://gitlab.com/gitlab-org/gitlab-runner-docker-cleanup), он и явился идейным вдохновителем нашей утилиты, но, к сожалению, не выполнял всех необходимых нам функций.\n\nНаписав своё решение, мы решили опубликовать его в открытом виде - и [зеркало на github](https://github.com/RND-SOFT/gitlab-janitor), и [образы на docker-hub](https://hub.docker.com/r/rnds/gitlab-janitor), и даже документация с примерами. Ну и не мог я удержаться от разнообразных бейджиков :)\n\n![](https://img3.teletype.in/files/62/47/6247a285-8416-4c43-be16-c2317598a279.png)\n\nПроще всего запускать его в докере на каждом сборщике (для этого мы используем [nomad](https://www.nomadproject.io/) 🥂 ):\n\n```\ndocker run --rm \\\n -v /var/run/docker.sock:/var/run/docker.sock \\\n -v /persistent/janitor:/store \\\n -e REMOVE=true \\\n -e INCLUDE=\"*integr*, *units*\" \\\n -e EXCLUDE=\"*gitlab*\" \\\n -e CONTAINER_DEADLINE=\"1h10m\" \\\n -e VOLUME_DEADLINE=\"4d\" \\\n -e IMAGE_DEADLINE=\"4d\" \\\n -e CACHE_SIZE=\"10G\" \\\n -e IMAGE_STORE=\"/store/images.txt\" \\\n rnds/gitlab-janitor:latest\n```\n\nЗаметили `IMAGE_STORE`? С остальными параметрами всё просто - шаблоны для имён образов, томов, сроки хранения, а вот для образов всё гораздо интереснее!\n\nДокер не сохраняет в своём локальном хранилище никакой информации о времени загрузки образа, а только обычную дату создания образа. В результате нет возможности определить, какой образ очень нужен и часто используется (хоть и создан давно), а какой свеженький, но больше никогда и никому не будет нужен.\n\nДля решения этой проблемы в gitlab-janitor сделан простенький механизм - когда образ встречается первый раз, он сохраняется в файл с временной меткой - это считается датой появления. Когда подходит срок - образ удаляется. Однако, если gitlab-janitor увидит созданный контейнер из этого образа - то временная метка в этом файле сбрасывается. Именно для этого и используется параметр `IMAGE_STORE`. Данный файл можно монтировать с хост-машины, а можно и нет - в этом случае история образов будет теряться при пересоздании/обновлении контейнера-чистильщика:\n\n```\nselenium/standalone-chrome:latest sha256:c01aea5eb0bf279df5f745e3c277e30d7d9c81f15b9d1d4e829f1075c31ed5b1 1660205645\npostgres:10-alpine sha256:d7023df56cb7cbe961f0c402888f7170397c98c099fb76fbe16b5442a236ad51 1660205040\nredis:latest sha256:3edbb69f9a493835e66a0f0138bed01075d8f4c2697baedd29111d667e1992b4 1660205645\n```\n\nС остальными параметрами, документацией и примерами можно ознакомиться на гитхабе.\n\n## Результаты\n\nВот и подъехали результаты объективного контроля за работой gitlab-janitor на трех наших сборщиках:\n\n![](https://img2.teletype.in/files/55/ae/55aecb6b-d59b-4bdd-b6e1-b079fcfb4805.png)\n\nВсе чистки с помощью `cron+bash`, `docker prune` и пр. выключены. Параметры, оптимально подходящие под наши процессы, мы еще подбираем (и будем подбирать), но уже видно, что процесс вошел в стабильную фазу, и наш чистильщик выполняет свою работу!",
423
+ "positive_reactions_count": 0,
424
+ "cover_image": null,
425
+ "tag_list": [
426
+ "infra",
427
+ "gitlab",
428
+ "cleaner",
429
+ "devops"
430
+ ],
431
+ "canonical_url": "https://blog.rnds.pro/030-gitlab-janitor?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
432
+ "reading_time_minutes": 2,
433
+ "user": {
434
+ "name": "Samoilenko Yuri",
435
+ "username": "kinnalru",
436
+ "twitter_username": null,
437
+ "github_username": "kinnalru",
438
+ "user_id": 559891,
439
+ "website_url": "https://teletype.in/@jerry_ru",
440
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
441
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
442
+ },
443
+ "organization": {
444
+ "name": "RNDSOFT",
445
+ "username": "rnds",
446
+ "slug": "rnds",
447
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
448
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
449
+ }
450
+ },
451
+ {
452
+ "type_of": "article",
453
+ "id": 1172539,
454
+ "title": "Dev containers: вскрытие",
455
+ "description": "Оригинал: https://blog.rnds.pro/029-dev-containers-uncovering В этой статье хотелось бы рассказать...",
456
+ "published": true,
457
+ "published_at": "2022-07-04T08:31:03.000Z",
458
+ "slug": "dev-containers-vskrytiie-20o3",
459
+ "path": "/rnds/dev-containers-vskrytiie-20o3",
460
+ "url": "https://dev.to/rnds/dev-containers-vskrytiie-20o3",
461
+ "comments_count": 0,
462
+ "public_reactions_count": 2,
463
+ "page_views_count": 314,
464
+ "published_timestamp": "2022-08-21T08:25:27Z",
465
+ "body_markdown": "---\ntitle: Dev containers: вскрытие\npublished: true\ndate: 2022-07-04 08:31:03 UTC\ntags: development, containers, docker, vscode\ncanonical_url: https://blog.rnds.pro/029-dev-containers-uncovering?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n![](https://img2.teletype.in/files/1f/20/1f20612a-6d37-4536-b0d2-cda4b477a1c7.jpeg)\n\n_Оригинал: [https://blog.rnds.pro/029-dev-containers-uncovering](https://blog.rnds.pro/029-dev-containers-uncovering?utm_source=devto&utm_medium=feed_rss&utm_campaign=rnds)_\n\nВ этой статье хотелось бы рассказать про разработку внутри docker-контейнера: зачем это нужно, что предоставляет для этого замечательная IDE VSCode, и как это работает.\n\nИтак, docker - это не только отличная технология для контейнеризации и деплоя приложений, но и возможность развернуть специфическую среду разработки в одно касание. \n\nДля тех, кто ещё (внезапно!) не знаком с Docker, очень рекомендую сразу пойти и ознакомиться с этой технологией ([https://en.wikipedia.org/wiki/Docker\\_(software)](https://en.wikipedia.org/wiki/Docker_(software))).\n\n## Для чего же это нужно?\n\nДавайте рассмотрим существующие проблемы, которые, как правило, возникают при работе над проектом.\n\n### Проблема 1\n\nВы - **новый разработчик в компании** , Вам дали свежеустановленный ПК, дали доступ в репозиторий проекта, с которым придется иметь дело. Предположим, что этот проект на Rails. Для запуска проекта необходимо:\n\n- установить системные dev пакеты\n- установить rvm\n- установить правильную версию ruby, bundle\n- установить nvm\n- установить node\n- установить и настроить БД, например, Postgres\n- возможно, что-то ещё, что требует проект (Redis, Consul и т.д.)\n\nВ процессе установки могут возникнуть следующие проблемы:\n\n- у вас Windows: это сразу +100 к сложности и боли\n- у вас слишком новая версия операционки, а в проекте используются устаревшие библиотеки, и при установке ruby пакетов, либо scss (когда же его наконец перепишут на JS) могут возникнуть ошибки компиляции\n- возможно, необходимы какие-то ещё дополнительные шаги, которые забыли описать в README, например, это может быть установка корневых сертификатов, прописывание каких-то переменных среды\n\nВ целом видно, что процесс первого запуска проекта непростой. Скорее всего, его придется повторить, когда захочется поработать с домашнего компьютера, а там как раз больше вероятность встретить Windows =)\n\n### Проблема 2\n\nВы - опытный разработчик и уже давно работаете в компании, **у вас много проектов** , на разных языках, фреймворках и приходится часто переключаться. Согласитесь, не очень хочется проводить существенное время на развертывание окружения, вспоминая, как это делать.\n\n### Проблема 3\n\nБывает так, что **проект уже очень старый** , порос мхом и обновлять его нерентабельно, но поддерживать надо. Там всё очень старое, и на своей современной операционке это просто не запустить.\n\n### Проблема 4\n\nЕщё одна проблема - это **замусоренность хостовой системы** , когда мы долго уже работаем, много всего разного поставили себе в систему, у нас появляются странные глюки, а остальные разработчики говорят нам, что [УМВР](https://wikireality.ru/wiki/%D0%A3%D0%9C%D0%92%D0%A0).\n\n### Решение\n\nИтак, эти все проблемы решает **контейнеризированное** окружение разработки. Мы получаем:\n\n- повторяемость среды разработки\n- быстрое развертывание\n- независимость от хостовой системы, её типа и версии\n- возможность полностью очистить свою систему от ненужных рудиментов старых проектов\n\nКонечно же есть и недостатки:\n\n- **Повышается сложность понимания что происходит**. Надо не забывать, что мы находимся внутри контейнера, и некоторые внешние ресурсы могут быть недоступны, такие как файлы, либо сторонние сервисы, запущенные на localhost.\n- **Требуется больше системных ресурсов**. В основном, это дополнительное пространство на диске под контейнеры и образы docker, а в случае с MacOS ещё и дополнительный расход оперативной памяти под виртуальную машину с докером.\n- Необходимо учитывать, что **ID пользователя внутри контейнера может быть другим** , и может возникнуть проблема с доступом к файлам проекта. Забегая вперед, скажу, что эта проблема вполне решаемая.\n\nСуществует множество вариантов реализовать такое контейнеризированное окружение:\n\n- написать Dockerfile с нужной версией ОС, установкой всего необходимого, зайти в контейнер и разрабатывать в VIM.\n- можно смонтировать исходники в запущенный контейнер и разрабатывать в своей IDE, а запускать в контейнере. Правда, у IDE скорее всего будут проблемы с дополнением кода.\n- можно установить IDE внутрь контейнера и пробросить X-ы с хостовой машины (работает только с Linux).\n- можно поставить sshd в контейнер и заходить внутрь по ssh из VSCode, IDEA или любой другой IDE, которая поддерживает удалённую разработку.\n- либо воспользоваться специализированным решением, которое нам предлагает VSCode. Предлагаю в этой статье подробно рассмотреть это решение как самое, на мой взгляд, продвинутое из того, что я видел.\n\n## VS Code Dev container\n\nПолная документация находится здесь: [https://code.visualstudio.com/docs/remote/containers](https://code.visualstudio.com/docs/remote/containers)\n\nВкратце, это работает следующим образом:\n\n- в проект добавляется конфигурация в папке .devcontainer\n- при открытии такого проекта VS Code предлагает переоткрыть его внутри контейнера\n- если мы соглашаемся, то после непродолжительных магических действий со стороны VS Code проект открывается\n- мы оказываемся внутри контейнера и получаем ровно то окружение, которое нам необходимо для разработки\n\nВот шаги, которые нужно выполнить для добавления к себе в проект конфигурации:\n\n- Docker уже должен быть установлен\n- устанавливаем плагин в VS Code [https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)\n- добавляем поддержку dev container в наш проект\n\nДля этого открываем палитру команд в VS Code, выбираем пункт “Remote-Containers: Add Development Configuration Files”.\n\nНам предоставляется выбор:\n\n1. воспользоваться готовыми конфигурациями (Node.js, Ruby on Rails, C++, Go, PHP и ещё много других)\n2. добавить существующий в проекте Dockerfile или docker-compose.yml\n\nЕсли сильно спешим, то можно воспользоваться готовыми конфигурациями. Но, на мой взгляд, лучше написать свою конфигурацию с нуля, так мы получаем больше контроля над происходящим, и это же так интересно!\n\n**Разбираемся в магии**\n\nОтдельно хотелось бы рассмотреть, что же делает VS Code с нашим контейнером, прежде чем его открыть.\n\nСуществует ряд нюансов, на которые следует обратить внимание:\n\n- при монтировании исходников в контейнер ID пользователя и ID группы внутри и снаружи должны совпадать, иначе мы получим в своих исходниках новые файлы, созданные от имени другого пользователя (возможно от рута)\n- у Git есть глобальные конфигурационные файлы, в которых задается ряд параметров, таких как имя пользователя, email, тип переноса строк и т.д., желательно, чтобы git внутри контейнера использовал такие же настройки\n- также необходимо скопировать некоторые пользовательские настройки для того, чтобы вы чувствовали себя как дома (.ssh/authorized\\_hosts, gpg конфигурацию)\n\nИменно это делает VS Code при открытии devcontainer:\n\n- он изучает версию операционной системы, которая используется в Dockerfile\n- оборачивает ваш Dockerfile своим с дополнительными командами по пробросу UID и GID внутрь контейнера\n- патчит /etc/passwd, синхронизируя UID и GID внутреннего пользователя с хостовым пользователем\n- копирует в него все необходимые конфигурационные файлы из папки пользователя (git, ssh, gpg)\n- устанавливает свой бэкенд на ноде (это порядка 450Мб) внутрь контейнера, который будет связываться с хостовым VSCode по рандомному TCP порту\n- устанавливает внутрь контейнера все необходимые плагины для VS Code, прописанные в .devcontainer/devcontainer.json#extensions\n- монтирует исходники проекта в папку /workspaces\n\nВуаля! Проект открыт, и можно кодить как обычно.\n\nВ итоге, чтобы открыть любой проект с поддержкой devcontainer и начать быстро кодить, Вам нужна лишь машина на Linux, Windows, MacOS с установленным Docker и VS Code.\n\n## Уже не терпится? Попробуем в деле!\n\nЯ заготовил пример проекта, который можно открыть в VSCode и посмотреть, как это работает.\n\nДля этого я форкнул первый найденный проект “Блог” на Rails ([https://github.com/navrocky/rails-blog-sample-devcontainers](https://github.com/navrocky/rails-blog-sample-devcontainers) ).\n\nКак нельзя кстати оказалось, что он уже немного “подзасох”, последний коммит 5 лет назад. Соответственно, в те годы люди сидели на Ruby 2.3, Debian 8 (Jessie). Вот и попробуем всё это завести в нашем 2К22.\n\nDev container поддерживает два варианта окружения:\n\n- один **Dockerfile**. Этот вариант проще и подходит, когда нам не нужны дополнительные сервисы для работы, такие как БД.\n- **docker-compose.yml**. Этот вариант позволяет нам запустить как само окружение, так и все необходимые дополнительные сервисы. В примере я использовал именно этот вариант, так как нам нужна ещё сконфигурированная база данных.\n\nТак выглядит типичная конфигурация в проекте:\n\n![](https://img2.teletype.in/files/97/65/97653f9b-a2dc-4b57-8243-76b0e479d6b2.png)\n\nВсё находится внутри папки **.devcontainer**. Файл **devcontainer.json** является основным и описывает конфигурацию для плагина VSCode.\n\n**devcontainer.json**\n\n```\n{\n\t// Имя конфигурации\n\t\"name\": \"Ruby\",\n\n\t// указание файла docker-compose.yml и сервиса в нем, \n\t// который будет использоваться для разработки\n\t\"dockerComposeFile\": \"./docker-compose.yml\",\n\t\"service\": \"dev\",\n\n\t// папка внутри контейнера, в которую монтируются исходники проекта\n\t\"workspaceFolder\": \"/workspace\",\n\n\t// действие при завершении работы с проектом\n\t\"shutdownAction\": \"stopContainer\",\n\n\t// дополнительные расширения VSCode, которые необходимо установить \n\t// внутрь контейнера перед началом работы\n\t\"extensions\": [\n\t\t\"castwide.solargraph\", \"eamodio.gitlens\"\n\t],\n\t\n\t// пользователь внутри контейнера, под которым будет происходить вход \n\t// в контейнер\n\t\"remoteUser\": \"user\"\n}\n```\n\n**Dockerfile**\n\n```\n# берем нужную версию базового дистрибутива. Debian 8 из 2015 года, нам подходит.\nFROM debian:8\n\n# устанавливаем все необходимые утилиты\nRUN apt-get update && apt-get install -y gnupg2 ca-certificates curl procps sudo git mc libpq-dev\n\n# добавляем пользователя, под которым будем работать внутри контейнера, заодно даем ему возможность делать sudo\nRUN useradd -m -d /home/user -s /bin/bash user && adduser user sudo && \\\n echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers\n\n# дальше все действия производим уже от пользователя\nUSER user \n\n# устанавливаем rvm\nRUN gpg2 --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB && \\\n curl -ksSL https://get.rvm.io | bash -s stable\n\n# устанавливаем nvm\nRUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash\n\n# подгружаем все инициализационные скрипты и переменные среды в bash, чтобы заработал установленный rvm\nSHELL [\"/bin/bash\", \"-c\", \"-l\"]\n\n# устанавливаем нужную версию Ruby и Bundler\nRUN rvm install ruby-2.4 && gem install bundler:1.17.3\n\n# создаем пользовательский каталог .bundle, чтобы были правильные права на эту папку при монтировании volume\nRUN mkdir -p /home/user/.bundle\n\n# устанавливаем node v5.3.0 и делаем её по умолчанию\nRUN source $HOME/.nvm/nvm.sh && nvm install v5.3.0 && nvm alias default v5.3.0\n\n# просто висим в бесконечном ожидании из /dev/null, это нужно чтобы контейнер не закрылся после запуска\nENTRYPOINT [\"tail\", \"-f\", \"/dev/null\"]\n```\n\nОткрываем проект, VSCode предлагает нам открыть его внутри контейнера - соглашаемся:\n\n![](https://img4.teletype.in/files/78/7f/787f534a-12bb-4b22-aaad-acb6bc445088.png)\n\nЖдем некоторое время, пока все образы будут загружены, и будет произведена сборка контейнера:\n\n![](https://img2.teletype.in/files/1e/e7/1ee7dbe3-6331-42f3-bb97-1b6ecc86b1c9.png)\n\nПока ждем, можно нажать show log и посмотреть, что там происходит.\n\nВсё - проект открыт, и можно приступать к работе:\n\n![](https://img1.teletype.in/files/8f/14/8f14721f-03e9-4b11-aedd-9ebfbf1dbee0.png)\n\nВыполняем в терминале VSCode следующие команды:\n\n```\nrake db:setup\nrails s -b 0.0.0.0\n```\n\nОткрываем в браузере наш блог ([http://localhost:3000/blog](http://localhost:3000/blog)):\n\n![](https://img3.teletype.in/files/6f/4d/6f4d9b67-e7c9-48c5-9ab9-b303f5061497.png)\n\nМожно зайти в административную панель ([http://localhost:3000/admin](http://localhost:3000/admin)) и добавить контент. Пользователь и пароль для входа: **admin@example.com** / **123456**\n\n**Вот так просто и быстро можно открыть незнакомый проект и приступить к работе!**\n\n## А что же IDEA и её производные, спросите вы?\n\nТам не всё так радужно, но есть свет в конце туннеля.\n\nНе так давно JetBrains добавила поддержку remote development в свои платные IDE. Community варианты её, к сожалению, не получили. И называется этот продукт JetBrains Gateway ([https://www.jetbrains.com/remote-development/gateway/](https://www.jetbrains.com/remote-development/gateway/)). Данная штука позволяет зайти на удалённую машину, в нашем случае в контейнер с поднятым ssh демоном, и загрузить туда выбранную вами IDE и запустить её в режиме сервера, а на вашем хосте будет запущен IDE Client, который будет по ssh туннелю общаться с серверной IDE. В целом всё выглядит примерно так же, как и в VS Code, но менее автоматизированно. При открытии проекта необходимо руками поднимать контейнер или docker-compose конфигурацию, настраивать соединение внутри контейнера. Серверная IDE, которая грузится в контейнер, ничем не отличается от десктопной, и в размерах тоже (это примерно 1,5 ГБ), что весьма печально. Интерфейс IDE клиента ощущается тормознее по сравнению с десктопной IDE и местами подглючивает. Но в целом, всё довольно работоспособно.\n\nСтоит заметить, что эта система пока находится в бете, поэтому держим кулачки и внимательно наблюдаем за дальнейшим развитием событий.",
466
+ "positive_reactions_count": 2,
467
+ "cover_image": null,
468
+ "tag_list": [
469
+ "development",
470
+ "containers",
471
+ "docker",
472
+ "vscode"
473
+ ],
474
+ "canonical_url": "https://blog.rnds.pro/029-dev-containers-uncovering?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
475
+ "reading_time_minutes": 3,
476
+ "user": {
477
+ "name": "Samoilenko Yuri",
478
+ "username": "kinnalru",
479
+ "twitter_username": null,
480
+ "github_username": "kinnalru",
481
+ "user_id": 559891,
482
+ "website_url": "https://teletype.in/@jerry_ru",
483
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
484
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
485
+ },
486
+ "organization": {
487
+ "name": "RNDSOFT",
488
+ "username": "rnds",
489
+ "slug": "rnds",
490
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
491
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
492
+ }
493
+ },
494
+ {
495
+ "type_of": "article",
496
+ "id": 1172538,
497
+ "title": "Хозяюшке на заметку: тэгируем логи и ошибки",
498
+ "description": "Оригинал: https://blog.rnds.pro/028-logs-and-errors-tagging В этой статье будет рассказано, что,...",
499
+ "published": true,
500
+ "published_at": "2022-06-06T11:40:27.000Z",
501
+ "slug": "khoziaiushkie-na-zamietku-teghiruiem-loghi-i-oshibki-4hc8",
502
+ "path": "/rnds/khoziaiushkie-na-zamietku-teghiruiem-loghi-i-oshibki-4hc8",
503
+ "url": "https://dev.to/rnds/khoziaiushkie-na-zamietku-teghiruiem-loghi-i-oshibki-4hc8",
504
+ "comments_count": 0,
505
+ "public_reactions_count": 0,
506
+ "page_views_count": 59,
507
+ "published_timestamp": "2022-08-21T08:23:52Z",
508
+ "body_markdown": "---\ntitle: Хозяюшке на заметку: тэгируем логи и ошибки\npublished: true\ndate: 2022-06-06 11:40:27 UTC\ntags: development, logs\ncanonical_url: https://blog.rnds.pro/028-logs-and-errors-tagging?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n![](https://img4.teletype.in/files/75/be/75be3761-fec6-4c54-bbee-d0471cac4cbc.jpeg)\n\n_Оригинал: [https://blog.rnds.pro/028-logs-and-errors-tagging](https://blog.rnds.pro/028-logs-and-errors-tagging?utm_source=devto&utm_medium=feed_rss&utm_campaign=rnds)_\n\n> В этой статье будет рассказано, что, возможно, не так с вашими диагностическими сообщениями, как их можно тэгировать, а главное - зачем это делать.\n\nДиагностические сообщения - это текстовые сообщения об ошибках и логи вашей программы. Так что же с ними не так и как это можно улучшить? Но если ты нетерпелив, то можешь поверить на слово, что проблемы есть, пропустить всю воду и перейти сразу к [сути](#kqAX) статьи и выводам. \n\n## **Что не так?**\n\nСперва опишу некоторую проблематику, которую я начал осознавать со временем и с опытом.\n\n### **Проблема №1**\n\nРассмотрим обработку ошибок. Существует два вида обработки ошибок: \n\n- на исключениях\n- с кодами возврата\n\nВ некотором роде идеологически эти два варианта одинаковы, при возникновении ошибки мы имеем код ошибки (класс исключения) и текстовое сообщение, описывающее ошибку.\n\n> В случае с исключениями, мы ещё имеем стек возникновения исключения, цепочку зависимых исключений, какие-то дополнительные значения, прикрепленные к исключению, но это не имеет отношения к дальнейшему повествованию. \n\nПрактически во всех языках в стандартной библиотеке уже имеется ряд предопределённых кодов ошибок (классов исключений), такие как ArgumentError, SecurityError, IOError, RuntimeError и т.д. И предполагается, что разработчик для всех без исключения новых ошибок в своей системе будет объявлять новые классы исключений и коды ошибок. Но, как правило, писать столько кода очень затратно по времени и все используют узкий набор стандартных исключений или кодов + небольшое количество собственных . А что же произошло конкретно, описывается в сообщении об ошибке. \n\nЧто же здесь плохого? Плохо то, что с точки зрения кода мы можем анализировать только код ошибки, но не человекочитаемое сообщение об ошибке. Поэтому в данном случае остается только расширять список кодов ошибок (исключений) и это правильно. Но в любом случае в коде большого приложения остается очень много редко используемых веток исполнения, на которые не может быть внятной реакции извне. Пользователь тоже с этим ничего поделать не может, и в таких случаях обычно кидается RuntimeError и пользователю отображается “Что-то пошло не так”. Либо это бывает ошибка ArgumentError, которая может возникнуть практически в любой функции при валидации параметров.\n\n### **Проблема №2**\n\nНенадолго переключимся на логирование, здесь тоже есть проблема. Обычно в нагруженной системе мы получаем тонны логов, и иногда их как-то нужно анализировать, даже при помощи различных фильтров и инструментов. К примеру, нужно поискать некое сообщение в логах, как часто оно возникает. Но сообщения в логах обычно это человекочитаемые строки, которые часто подвергаются форматированию и локализации. И искать в логах точное соответствие строке порой бывает очень сложно, не спасают даже регулярки. Например: “Client #{client.id} not found in #{list}”. Если мы будем искать это сообщение по частям, то мы найдем множество записей с “Client” или “not found” и дальше уже глазками грепать пересечение. Также это отсутствие формализации сильно усложняет последующие автоматизированные анализы логов на наличие определенных ошибок, вывод статистики.\n\n### **Проблема №3**\n\nМы увидели какую-то ошибку или сообщение в логах, но как теперь найти то место в коде, которое её/его вызывает? Хорошо, если у исключения залогировался стектрейс со строками кода… Иначе мы беремся за инструмент полнотекстового поиска в нашей любимой IDE и начинаем искать сперва по нескольким словам, потом регуляркой, если сообщение было отформатировано. Через некоторое время конечно же находим нужное нам место. Но это было не очень просто, не так ли? А если строка была локализована… Ну вы поняли.\n\n### **Проблема №4**\n\nЭто поддержка. Пользователи начинают нам звонить и пытаться на ломаном языке сообщить, что же им выскочило на экране, либо делают скрины с выхлопом ошибки, а там что-то типа “Передан неправильный параметр”. Какой? Где? Стектрейса нет. Тут начинаются гадания на кофейной гуще и дальнейшие переписки, созвоны.\n\n## **Что делать? Тэгировать!**\n\nКак-то очень давно я обратил внимание на синие экраны смерти Windows, также при работе с WinAPI я наткнулся на каталог кодов ошибок в Windows. Ребята явно заморочились на этот счет, сейчас он переваливает за 12000 записей. Там можно найти отдельный код на любой чих.\n\n![](https://img3.teletype.in/files/a3/6f/a36faf98-18cd-4e1b-8374-14b221401806.png)\n\nНо проблема в том, что нужно прямо брать и вести такой реестр порядковых кодов для своего проекта, чтобы случайно не пересечься с каким-то другим кодом. А если у тебя микросервисы, то желательно, чтобы между ними тоже не было пересечений. Поэтому я для себя быстро накатал незамысловатый скрипт на sh:\n\ngenuid.sh\n\n```\n#!/bin/sh\nSHA256=$(uuidgen | sha256sum); echo -n \"<${SHA256:0:8}>\"\n```\n\nРасшифровываю: берем обычный **GUID** , считаем от него **SHA256** , берем первые 8 символов, обрамляем в угловые скобки “<”, “>” для пущей красоты.\n\nПотом повесил вызов этого скрипта с эмуляцией вбития его с клавиатуры на хоткей, спасибо божественному KDE, что там всё для этого есть штатно (ну практически). И начал быстро растыкивать такие коды в тексты сообщений об ошибках и в логи.\n\nВот так примерно выглядят сообщения об ошибках:\n\n```\nraise SecurityError.new \"<c920fbaf> Hello #{username}, \" +\n \"you’re not supposed to be here\"\n```\n\nлибо:\n\n```\nlogger.warn \"<dc8a357a> #{username} entered restricted area\"\n```\n\n## Выводы\n\nЧто же я получил с таким тэгированием:\n\n- описанный метод универсален, и выгоду можно получить на любой платформе, будь то бэк, либо фронт, написанные на чём угодно\n- мне не надо вести реестр кодов, я практически уверен, что очередной код будет уникален, и он не такой громоздкий как **GUID**\n- видя код ошибки в выхлопе на экране, я могу быстро найти её в исходниках простейшим грепом без тайных знаний regexp\n- благодаря строгому формату тэга, я могу незамысловатым скриптом собрать все ошибки в проекте с описанием в текстовый файл (он же реестр кодов ошибок) и отдать его, например, фронтендеру\n- пользователи мне слали коды ошибок, и могли это делать хоть через SMS\n- стало возможным вычленить из текста тэг и обработать программно специфичную ошибку особым образом\n- в системе сбора и анализа логов я настраивал фильтры на особые ошибки, по которым необходимо было экстренно реагировать, получал сообщения в телегу\n- в отличие от стектрейса с файлом и номером строки **UID** не изменится. Это означает, что даже если вы сделаете грандиозный рефакторинг, вы всегда найдете ошибку или сообщение по коду, сколько бы лет не прошло, и если она там ещё осталась.\n- на сервере при формировании response я выделял **UID** ошибки в отдельное поле, чтобы клиенту было удобнее. Получалось примерно так:\n\n```\n{ \n \"result\": \"ACCESS_DENIED\", \n \"uid\": \"c920fbaf\", \n \"message\": \"Hello John, you’re not supposed to be here\"\n}\n```\n\nКонечно же, есть и некоторые неудобства, недостатки данного тэгирования:\n\n- когда занимаешь копипастой, то частенько коды начинают задваиваться, тут надо быть внимательным и генерить для только что вставленного кода новые тэги\n- коды могут просочиться в итоге на экран пользователю в сообщении об ошибке и на фронте надо озаботиться отделением текста от тэга, а полный текст ошибки убрать в секцию “Дополнительно”\n\nСпасибо за внимание, надеюсь, что данный несложный приём ещё кому-нибудь хорошо зайдет. \n\n## В качестве дополнения\n\nПод Linux я использую **xvkbd** для эмуляции ввода с клавиатуры.\n\n```\n#!/bin/sh\nGUID=`genuid.py`\nxvkbd -text $GUID\n```\n\nЧто существует для эмуляции ввода с клавиатуры и автоматизации под Windows и Mac - мне неведомо, хотелось бы получить отклик в комментариях, и я дополню статью =)",
509
+ "positive_reactions_count": 0,
510
+ "cover_image": null,
511
+ "tag_list": [
512
+ "development",
513
+ "logs"
514
+ ],
515
+ "canonical_url": "https://blog.rnds.pro/028-logs-and-errors-tagging?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
516
+ "reading_time_minutes": 1,
517
+ "user": {
518
+ "name": "Samoilenko Yuri",
519
+ "username": "kinnalru",
520
+ "twitter_username": null,
521
+ "github_username": "kinnalru",
522
+ "user_id": 559891,
523
+ "website_url": "https://teletype.in/@jerry_ru",
524
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
525
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
526
+ },
527
+ "organization": {
528
+ "name": "RNDSOFT",
529
+ "username": "rnds",
530
+ "slug": "rnds",
531
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
532
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
533
+ }
534
+ },
535
+ {
536
+ "type_of": "article",
537
+ "id": 1172537,
538
+ "title": "Структура тестов Go, для RSpec-нутых",
539
+ "description": "Оригинал: https://blog.rnds.pro/027-go-structure-of-tests/ Для кого статья? Для...",
540
+ "published": true,
541
+ "published_at": "2022-05-03T14:43:48.000Z",
542
+ "slug": "struktura-tiestov-go-dlia-rspec-nutykh-34l3",
543
+ "path": "/rnds/struktura-tiestov-go-dlia-rspec-nutykh-34l3",
544
+ "url": "https://dev.to/rnds/struktura-tiestov-go-dlia-rspec-nutykh-34l3",
545
+ "comments_count": 0,
546
+ "public_reactions_count": 0,
547
+ "page_views_count": 47,
548
+ "published_timestamp": "2022-08-21T08:30:41Z",
549
+ "body_markdown": "---\ntitle: Структура тестов Go, для RSpec-нутых\npublished: true\ndate: 2022-05-03 14:43:48 UTC\ntags: golang, rspec, ruby, go\ncanonical_url: https://blog.rnds.pro/027-go-structure-of-tests?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n![](https://img1.teletype.in/files/cd/56/cd56ce9a-c471-423f-9ae0-d8474fa27c9a.jpeg)\n\n_Оригинал: [https://blog.rnds.pro/027-go-structure-of-tests/](https://blog.rnds.pro/027-go-structure-of-tests/?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds)_\n\n## Для кого статья?\n\nДля Ruby-разработчиков, которые знают что такое [хорошие тесты на RSpec](https://blog.rnds.pro/023-rspec-tests-structure) (остальных в Go ничего не смутит) и хотят писать тесты на Go соответствующим образом.\n\nДля Go-разработчиков. На Go можно написать хорошие тесты, но почему-то я таких пока не видел 😇. Почему-то даже в крупных и популярных пакетах используют не лучшие подходы.\n\nДокументация по тестированию на Go не очень богата и оставляет много вопросов (пока не получил опыта работы с RSpec, они у меня не возникали), ну что же, попытаемся на них ответить.\n\n## Немного предыстории\n\n[RNDSOFT](https://rnds.pro/) как-то организовывала [Ruby meetup](https://www.youtube.com/watch?v=nDFjZ4vFr88), где я выступал с докладом **\"Тесты Go глазами Ruby-разработчика\"** и проводил параллели между тестами в Go и RSpec, рассказывая о трудностях, с которыми сталкиваешься, начиная писать тесты в Go, и как преодолеть их с наименьшей болью.\n\nИ вот сейчас у меня дошли руки и я хочу изложить мои мысли текстом, сконцентрировав внимание именно на структурировании тестов.\n\n## Что будет?\n\n1. Посмотрим, что говорит нам [документация](https://pkg.go.dev/testing) по поводу того, как нужно писать тесты.\n2. Обратим внимание на то, что документация не даёт нам всех ответов по структурированию тестов.\n3. Поймём, что для структурирования тестов в реальном проекте нам понадобится некое соглашение.\n4. Чтобы не изобретать велосипед, определим, какое соглашение можно взять за эталон и работать с ним.\n5. Разберём функционал, предоставляемый Go, с точки зрения RSpec (только стандартный пакет тестирования. Почему так, поговорим [ниже](#zVhA)).\n6. Поймём, как в Go решить те же задачи, которые позволяет решить RSpec, но не выходя за парадигму Go.\n\n## Чего не будет?\n\nКак можно скорее хочется спасти мебель от возгорания по вине тех, кто уже начал кричать что-то в духе:\n\n- \"Как вообще можно сравнивать RSpec и Go\".\n- \"Привыкли писать на своём RSpec, так теперь везде вам подавай всё как там? Учитесь писать в парадигме Go\".\n- \"Go строго типизированный, это вам не Ruby, в нём всё по-другому\".\n- \"Go строго типизированный, в нём не нужны такие тесты, как на RSpec\".\n\nВыдохните, сосчитайте до десяти, заварите себе горячий напиток 😤☕️🙂.\n\nТут не будет:\n\n- попыток писать на Go, как на RSpec,\n- сравнения специфический возможностей,\n- сравнения особенностей написания тестов.\n\nЯ прекрасно понимаю, что языки разные и у них разные подходы. Но когда разработчик пишет, он должен (но не всегда это понимает) решать ряд стандартных задач. И вот именно о том, как взяв в руки Go, обеспечить тот необходимый минимум, с реализацией которого мы (Ruby-разработчики) справляемся, когда используем RSpec.\n\n## Почему не рассматриваем BDD фреймворки языка Go?\n\nRSpec - это BDD фреймворк и было бы логично сравнивать его с [BDD](https://ru.wikipedia.org/wiki/BDD_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)) фреймворком в Go, например с пакетами [Ginkgo](https://github.com/onsi/ginkgo) или [GoConvey](https://github.com/smartystreets/goconvey), но делать мы этого не будем.\n\nА дело собственно в том, что согласно опросу в телеграм канале [Go-go!](https://t.me/gogolang), который я проводил при подготовке к докладу, можно сказать, что описанные выше пакеты использует примерно 10%-15% разработчиков. Не говоря уже о том, что достичь тех же результатов можно и при помощи стандартного пакета, начиная с Go 1.7 уж точно.\n\n## **К чему мы привыкли в Rspec?**\n\n### Тестирование методов\n\nВ первую очередь стоит сказать, что это [BDD](https://ru.wikipedia.org/wiki/BDD_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)) фреймворк и поэтому имеет специфичную для этой парадигмы структуру написания тестов.\n\nОсновными элементами теста на Rspec являются блоки:\n\n- describe - описывает тестируемый класс или метод,\n- context - для описания состояний, окружения, контекста (когда пользователь авторизован, когда пользователь не авторизован),\n- it - тело теста.\n\n```\ndescribe Store do\n describe '.sum' do\n context 'when summing positive numbers' do\n it { expect(described_class.sum(2, 2)).to eq 4 }\n end\n\n context 'when summing negative numbers' do\n it { expect(described_class.sum(-2, -2)).to eq -4 }\n end\n end\nend\n```\n\nКогда мы описываем метод как **.sum** или **::sum** , это означает, что мы тестируем метод класса (статический метод). Если мы описываем метод как **#sum** , значит мы тестируем метод инстанса (метод, вызываемый на объекте).\n\n### Тестирование функции\n\nА вот тут ясности поменьше.\n\nПо сути можно сказать, что RSpec - это фреймворк для тестирования ООП приложений. И в рамках тестирования такого приложения, данный вопрос у вас не возникнет. Так например, шанс, что в Rails приложении вам понадобится сделать нечто подобное, стремится у нулю.\n\nНо проблема лишь в том, что не ясно, как наиболее правильно описать такой тест. \nRSpec по сути говорит нам, как наилучшим образом тестировать классы, но не запрещает нам использовать его для тестирования отдельных функций.\n\nВ таком случае, если мы тестируем некий скрипт, в котором есть отдельно стоящая функция **sum** , то мы можем написать тест как-то так и не переживать по этому поводу:\n\n```\ndescribe 'Test some script' do\n describe '#sum' do\n it { expect(sum(2, 2)).to eq 4 }\n end\nend\n```\n\nТест не потерял в читаемости. Написать некий текст вместо имени тестируемого класса, вполне допустимо. В полноценном приложении (а не в скрипте), подобная ситуации - это редкость и исключительный случай. Так что сделаем маленькое исключение для него и не будем об этом жалеть.\n\n### Тестирование приватного метода\n\nДа, подобный трюк лучше не исполнять. Но рассмотрим саму возможность.\n\nОписание теста для приватного метода ничем не будет отличаться от публичного. Но тестировать придётся иначе, ведь Ruby не позволит просто так вызвать приватный метод. Но тут нам поможет метод **send**.\n\n```\ndescribe Store do\n let(:store) { described_class.new }\n\n describe '#sum' do\n it { expect(store.send(:sum, 2, 2)).to eq 4 }\n end\nend\n```\n\n## А как это в Go?\n\nПрежде чем мы углубимся, я хочу сделать акцент на одном принципиальном для меня отличии RSpec и тестов в Go.\n\n**RSpec** даёт каркас для написания тестов и набор рекомендаций, как всем этим пользоваться. Уже после сюда добавляются соглашения [Better Specs,](https://www.betterspecs.org/) делающие тесты ещё прекраснее.\n\n**Go** старается быть простым и однозначным (тот же вопрос с фигурными скобками решён весьма однозначно 🙂). Язык даёт нам много хорошего инструментария и простые, но строгие руководства, как с ним работать. И эти руководства не просто строгие, они не гибкие (в рамках официальной документации).\n\nСобственно в этом и кроется **ключевая проблема**. RSpec с виду не так прост, но практически не оставляет вопросов. Есть описание в документации, есть соглашения, но помимо этого есть гибкость, позволяющая покрывать специфичные случаи. А Go однозначен и строг. И разработчик, оказавшись в ситуации строгости, но недосказанности, не может понять, как ему быть в тех или иных случаях, которые не покрывает документация. Само собой, сложности добавляет отсутствие достаточного опыта в языке и контакта с другими разработчиками Go, чтобы интуитивно понимать допустимые и недопустимые решения.\n\n### Тестирование функции\n\nТут всё на высоте. Всё просто и понятно. Идём читать документацию и\n\nвидим там следующий шаблон для написания тестов:\n\n```\nfunc TestXxx(*testing.T)\n```\n\nПо сути, нам показали, как протестировать публичную (начинается с заглавной буквы) функцию.\n\nСледовательно, если в нашем пакете есть публичная функция Sum, то тест для неё будет выглядеть вот так:\n\n```\nfunc TestSum(t *testing.T) {\n\t...\n}\n```\n\nСобственно, это всё, что нам рассказывает документация касательно именования тестов.\n\nЕсли в вашем приложении есть только функции, у вас нет структур, а если и есть, то они не содержат поведения, то описанного в документации вам достаточно, можете закрывать статью.\n\n### Тестирование приватной функции\n\n??????????????????????????????????\n\nЗнаю-знаю, что тестировать приватные функции это не правильно, но в реальности случаются, пускай и редко, ситуации, где лучше сделать это, чем не сделать.\n\n_Но думаю, что Вы так не делаете. Вы же пишите идеальный код в идеальном приложении, спроектированном по всем канонам, а все разработчики, работавшие над ним, личной вскормлены дядюшкой Бобом._\n\n> В зыке Ruby нет приватных функций, есть только приватные методы, поэтому что делать с такой ситуацией, Ruby-разработчикам не очень понятно.\n\n### Тестирование метода\n\n??????????????????????????????????\n\nТоже не понять но как это делать. Нужно разбираться.\n\n> Всё тестирование в Ruby по сути заточено на тестирование методов, в то время как тестирование в Go, как следует из документации, больше ориентировано на тестирование функций (методы тестировать само собой тоже можно, но документация делает акцент на функциях). Это ещё одна особенность, которая усложняет адаптацию Ruby-разработчика к написанию тестов на Go.\n\n### Тестирование в разных обстоятельствах (в разном окружении)\n\n??????????????????????????????????\n\nТут ответов тоже нет. Нам говорят о технике табличного тестирования, которая хороша и закрывает часть проблем, но всё же не применима для задачи разделения контекста.\n\n> Как мы помним, в RSpec эту задачу решает блок context. В Go это тоже можно сделать, просто ответ лежит не на поверхности.\n\n## Тестирование приватной функции\n\nЕсли написанное выше, касательно тестирования всего приватного, тебя не успокоило и душа всё ещё горит, можешь написать в комментах \"Не тестируйте приватные функции\".\n\nПроблема тут в том, что шаблон именования теста, подразумевает, что название функции будет написано с большой буквы, то есть это будет публичная функция.\n\nПоэтому, все варианты, которыми мы можем описать тест для приватной функции sum, буду нарушать соглашение из официальной документации.\n\n```\nfunc Testsum(t *testing.T) {\n\t...\n}\n\n// или\n\nfunc Test_sum(t *testing.T) {\n\t...\n}\n```\n\nТак написать конечно можно, но какой вариант выбрать и почему? Как объяснять это другим разработчикам. Ответы будут [ниже](#2BwB).\n\n## Тестирование метода\n\nДавайте вспомним нашу функцию **Sum** и то, как мы писали тест для неё:\n\n```\nfunc Sum(x, y int) int {\n\treturn x + y\n}\n```\n\nНо что если **Sum** - это метод структуры? Можно предположить, что именование теста менять не стоит, и писать надо так же, как сказано в документации применительно к функциям. Это сработает, но глядя на именование теста, Вы не будете понимать, что тестируете. Подобная неоднозначность конечно не приятна, но с ней всё ещё можно мириться.\n\nТрудности начинаются, когда в пакете присутствуют [метод](https://github.com/spf13/viper/blob/master/viper.go#L1492) и [функция](https://github.com/spf13/viper/blob/master/viper.go#L1490) с одинаковым именованием, как например в пакете [viper](https://github.com/spf13/viper).\n\n![](https://img2.teletype.in/files/9c/f0/9cf049fd-a81b-46c6-8a58-8251ae1cd4d1.png)\n\nК сожалению, в самом пакете viper не удалось найти тест, который бы показал, как решать эту проблему. Придётся выкручиваться самим.\n\n## Соглашение по именованию тестов из gotests - решение проблем\n\nОтветы на поставленные выше вопросы и не только на них даёт пакет [gotests](http://github.com/cweill/gotests). Этот пакет используется для автоматической генерации заготовок тестов, для абсолютно всего, что находится в вашем пакете.\n\nВзглянув на то, как он работает, мы можем определить используемые им правила именования и позаимствовать их.\n\nПравила выглядят следующим образом:\n\n**TestPubFunc(t \\*testing.T) \nTest\\_privateFunct(t \\*testing.T) \nTestPubStruct\\_PubMethod(t \\*testing.T) \nTestPubStruct\\_privateMethod(t \\*testing.T) \nTest\\_privateStruct\\_privateMethod(t \\*testing.T)**\n\nВ чём плюсы:\n\n1. Подобное именование решает все заявленные проблемы.\n2. Из названия теста понятно, что тестируется.\n3. Соглашение отражено в паке, у которого почти 4 тыс. звёзд на GitHub.\n\nКак мне кажется, этих причин достаточно, чтобы внедрить эту практику именования в свою команду. Она будет понятна всем, а кому-то уже знакомой, и не нужно изобретать \"велосипед\".\n\n## Тестирование в разном контексте\n\nРассматривая, как подобное реализовано в RSpec, мы приводили вот такой пример:\n\n```\ndescribe Store do\n describe '.sum' do\n context 'when summing positive numbers' do\n it { expect(described_class.sum(2, 2)).to eq 4 }\n end\n\n context 'when summing negative numbers' do\n it { expect(described_class.sum(-2, -2)).to eq -4 }\n end\n end\nend\n```\n\nЗнатоки, вероятно, обратили внимание, что тех же результатов в Go можно добиться через табличное тестирование.\n\nТо есть вот так:\n\n```\nfunc TestSum(t *testing.T) {\n\ttCases := []struct{\n\t\tx int\n\t\ty int\n\t\tres int\n\t}{\n\t {2, 2, 4},\n \t{3, 2, 5},\n\t}\n\tfor _, tCase := range tCases {\n\t\tres := Sum(tCase.x, tCase.y)\n\t\t...\n\t}\n}\n```\n\nЕдинственное, чего тут не хватает, это ясности относительно того, а какой же случай мы тестируем в текущий момент.ю и какой тестовый набор провалился.\n\nДокументация даёт нам решение и предлагает использовать [Subtests](https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks) (подтесты).\n\n```\nfunc TestSum(t *testing.T) {\n\ttCases := []struct{...}{...}\n\tfor _, tCase := range tCases {\n\t\tt.Run(\n\t\t\tfmt.Sprintf(“x=%d y=%d”, tCase.x, tCase.y),\n \t\t\tfunc(t *testing.T) {\n\t\t\t\t// test body\n\t\t\t},\n\t\t)\n\t}\n}\n```\n\nВот теперь мы будем понимать, какой тестовый набор провалился.\n\nВы скажете, что данная ситуация хорошо ложится на концепцию табличного тестирования, но есть и другие. И будете абсолютно правы.\n\nНаиболее удобный и понятный пример, на мой взгляд, это ситуация с авторизованным и не авторизованным пользователем. Так же можно рассмотреть ситуации с разным состоянием конфигурации приложения, но давайте возьмём пример с пользователем.\n\nЧто делать, есть нам нужно проверить работу экшена контроллера для авторизованного и не авторизованного пользователя?\n\nНапрямую документация не даёт ответа на этот вопрос, но почему бы нам не воспользоваться функциональностью Subtests? Не вижу причин отказывать себе в этом. Единственное, чего не хватает, это некоего соглашения о том, как описывать в подтестах различные ситуации. Нам не известно такое соглашение в Go, не беда, возьмём соглашение из [Better Specs](https://www.betterspecs.org/).\n\n```\nfunc TestDeleteDatabase(t *testing.T) {\n t.Run(\"When the user is logged in\", func(t *testing.T) {\n\t // test body\n\t})\n\t\n t.Run(\"When the user is not logged in\", func(t *testing.T) {\n\t // test body\n\t})\n}\n```\n\nМне кажется, что выглядит хорошо, не так ли?\n\n## Итог\n\nВ Go нет общепринятого стандарта именования тестов, так как официальная документация описывает лишь тестирование публичных функций, не уделяя внимание остальному, но это соглашение есть в пакете [gotests](https://github.com/cweill/gotests). Изучите его и используйте, если не сам пакет, то хотя бы его подход.\n\nПроблему тестирования в разном контексте прекрасно можно решить при помощи [Subtests](https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks), нужно лишь добавить к ним хорошее соглашение по описанию тестируемых случаев, как это сделано в [Better Specs](https://www.betterspecs.org/).\n\n> Огромное количество времени разработчик тратит не на решение задачи, а на выбор из нескольких вариантов решения. Как назвать переменную, метод или функцию, куда положить тот или иной файл, как описывать тесты. \n> Все эти вопросы решаются соглашениями.\n\nК сожалению, в Go соглашения - это слабая сторона, во всяком случае в сравнении с экосистемой Ruby. Я полагаю, что локально команды решают этот вопрос, но не хватает именно общепринятых соглашений.\n\nОтмечу, что разработать соглашение - это лишь малая часть работы, самое главное - это соглашение распространить в сообществе разработчиков.\n\nПо моему мнению, сообщество Go нуждается в большем внедрении стандартов и соглашений, поэтому призываю вас принимать активное участие в решении этой проблемы.\n\nВсем сил, терпения и чистого кода 💪😎",
550
+ "positive_reactions_count": 0,
551
+ "cover_image": null,
552
+ "tag_list": [
553
+ "go",
554
+ "rspec",
555
+ "ruby"
556
+ ],
557
+ "canonical_url": "https://blog.rnds.pro/027-go-structure-of-tests?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
558
+ "reading_time_minutes": 3,
559
+ "user": {
560
+ "name": "Samoilenko Yuri",
561
+ "username": "kinnalru",
562
+ "twitter_username": null,
563
+ "github_username": "kinnalru",
564
+ "user_id": 559891,
565
+ "website_url": "https://teletype.in/@jerry_ru",
566
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
567
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
568
+ },
569
+ "organization": {
570
+ "name": "RNDSOFT",
571
+ "username": "rnds",
572
+ "slug": "rnds",
573
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
574
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
575
+ }
576
+ },
577
+ {
578
+ "type_of": "article",
579
+ "id": 1172536,
580
+ "title": "Что делать, когда кругом враги?",
581
+ "description": "Оригинал: https://blog.rnds.pro/025-matching Когда вы интегрируете между собой две системы, одной...",
582
+ "published": true,
583
+ "published_at": "2022-03-18T13:05:00.000Z",
584
+ "slug": "chto-dielat-koghda-krughom-vraghi-172e",
585
+ "path": "/rnds/chto-dielat-koghda-krughom-vraghi-172e",
586
+ "url": "https://dev.to/rnds/chto-dielat-koghda-krughom-vraghi-172e",
587
+ "comments_count": 0,
588
+ "public_reactions_count": 0,
589
+ "page_views_count": 36,
590
+ "published_timestamp": "2022-08-21T08:22:35Z",
591
+ "body_markdown": "---\ntitle: Что делать, когда кругом враги?\npublished: true\ndate: 2022-03-18 13:05:00 UTC\ntags: ruby, security, integration\ncanonical_url: https://blog.rnds.pro/025-matching?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n![logo](https://img4.teletype.in/files/f2/e7/f2e74556-36c9-41d1-97e5-89216fa6ec9c.jpeg)\n\n_Оригинал: [https://blog.rnds.pro/025-matching](?utm_source=devto&utm_medium=feed_rss&utm_campaign=rnds)_\n\nКогда вы интегрируете между собой две системы, одной из первоочередных задач для вас станет сопоставление существующих данных. Самый простой пример - это пользователи. В ИС1 много лет работали люди, и в ИС2 тоже, причем, пока не было задачи интеграции, никто не озаботился тем, чтобы вести единую идентификацию.\n\nВ довесок она может быть (и скорее всего будет) построена на кардинально разных принципах! Для ИС1 программист завел в базе `bigint primary-key` с автоинкрементом и автоматически получал новый номер для каждого пользователя. При разработке ИС2 психическое здоровье разработчика или аналитика (а возможно и обоих) могло быть не столь крепким, и было принято решение использовать в качестве однозначного идентификатора `ФИО` + дату рождения.\n\nИ когда нужно передать, скажем, заявку из ИС1 в ИС2, то хотелось бы не передавать внутри неё все, что мы знаем о пользователе, который её оставил. Ну и ещё больше не хочется потом с использованием всего этого вороха данных искать соответствующую запись у себя, встраивая огромную логику сопоставления в процесс обработки заявки. Хочется получать некий заранее заданный идентификатор, по которому можно легко и быстро достать корректного пользователя из своей базы.\n\nДля этого будет хорошо предварительно сопоставить базы двух ИС, найти совпадения, и либо ИС2 согласится сохранять в пару к своему идентификатору идентификатор из ИС1, либо победит зло и bigint дополнится “ФИО + дата рождения” в базе ИС1.\n\nМы не будем говорить о том, как правильно сопоставлять людей - по `ИНН`, `СНИЛС`, паспорту или всему сразу. Мы рассмотрим довольно неожиданную проблему - а что если ИС1 и ИС2 не доверяют друг другу и не готовы показать свой список пользователей другой системе?!\n\n![hmmm](https://img1.teletype.in/files/41/ab/41ab2a26-585f-4005-b312-b8c8eb5538a1.jpeg)\n\nПри независимом партнерстве ситуация в целом не редкая. Есть например некая [платформа](https://winvestor.ru), агрегирующая в себе разные данные людей из различных источников, и есть компания, которая может предоставить эти данные. Но компания не хочет раскрывать платформе полный список своих клиентов (предоставляя таким образом халявные лиды), а готова отдавать данные только по тем людям, которые уже работают с платформой. Платформа в свою очередь также не желает \"сливать\" свою базу по сходным причинам. Но работать надо!\n\nВ этом случае на помощь могут прийти алгоритмы хэширования ([хэш-функции](https://ru.wikipedia.org/wiki/%D0%A5%D0%B5%D1%88-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F)). Кроме того, что результат хэш-функции напрямую зависит от входных данных (то есть два одинаковых набора данных всегда дадут один и тот же результат), по этому результату невозможно восстановить исходные данные. Фарш невозможно провернуть назад… Ладно, вру, возможно с помощью радужных таблиц, по сути перебором, но об этом позже. Есть ещё коллизии, но там тервер говорит \"да ладно, не кипишуй…\".\n\nИтак, к сути - легким движением руки `ФИО` из базы ИС1 превращаются в веселую мешанину символов, передаются в ИС2, где сравниваются с подобной мешаниной, сформированной для каждой записи в базе ИС2. И вуаля! ИС2 может записать к себе в базу для Иванова идентификатор этого Иванова из ИС1 - 45. И во всех дальнейших взаимодействиях, где будет иметься в виду Иванов, обе системы спокойно могут использовать 45.\n\n![pic](https://img4.teletype.in/files/37/79/377959be-20fc-4784-ba7b-55ddd9322baf.png)\n\nЕстественно, чтобы не очень сильно насиловать `CPU`, обе ИС должны провести процедуру вычисления хэшей заранее и хранить результаты в базе, чтобы по мере необходимости в синхронизации идентификаторов они были всегда под рукой. Кстати, при желании такой результат и как общий идентификатор можно использовать, если это укладывается в ваше представление о том, как должен выглядеть primary-key записи в базе.\n\nВыбор полей для сопоставления остается на совести разработчиков. `ФИО`, `ИНН`, `СНИЛС`, номер паспорта, дата рождения, девичья фамилия матери, название куска камня, что был в зените в момент рождения. Можно использовать несколько разных наборов, если есть разные кейсы заполненности профилей. В общем все что угодно при одном очень важном НО - то, что вы будете отдавать хэш-функции на вход должно быть хоть сколько-нибудь длинным! \n\n![pic3](https://img2.teletype.in/files/5e/93/5e935e2d-25c4-44e0-9b8a-9b6a10c353d9.jpeg)\n\nПример: если строкой для хэширования будет серия и номер паспорта, то это суммарно 10 символов(экзотику в расчет не берем), то есть всего лишь 10 миллиардов вариантов различных значений. Далеко не самая современная видеокарта `ATI Radeon HD 4850 X2` позволяет генерировать до 2,2 миллиардов хешей по алгоритму `MD5` в секунду. То есть меньше, чем через 5 секунд все возможные значения для всех возможных паспортов будут готовы, останется только сравнить и получить на выходе исходный номер. Радужные таблицы VS безопасность - 1:0.\n\nНо уже добавив к серии и номеру паспорта телефон в международном формате мы получаем примерно 15 тысяч лет, что делает нас достаточно защищенными от школьника за старым папкиным компом. Победа!\n\nДальше можно накрутить несколько последовательных применений хэш-функций, соль, перец, оливочку… И в целом добиться приемлемой защиты от эникейщиков компании, с которой вы проводите подобную интеграцию. Особенно, если ИТ для них не профильная сфера.\n\nДа, внедряя, например, \"перчение\" вы можете потерять в оптимальности из-за невозможности нормально предпросчитывать и хранить в базе хэши. Поэтому, как и в любом вопросе, связанном с безопасностью, надо вовремя остановиться, пока безопасность не перешла в паранойю. В конце концов, скорее всего, в вашем API найдется десяток других более простых способов спереть весь набор данных, кроме многоуровневого хэширования и вопрос серьезного промышленного шпионажа вы будете закрывать другими методами, оставив хэширование как \"защиту от дурака\".\n\n![pic4](https://img4.teletype.in/files/31/00/3100b698-b510-4f90-a0e1-97f7a58d38db.jpeg)\n\nПодводя итог:\n\n1. Хэшируйте как можно более длинные объемы данных;\n2. Используйте более менее современные алгоритмы, с минимальной вероятностью коллизии и медленным хэшированием. `OWASP` рекомендует `Argon2` или `bcrypt` с предварительным хэшированием `SHA`. Но в вашем случае может быть достаточно пары `SHA512` каскадом;\n3. Следует из предыдущего пункта - подбирайте решение под задачу, чтобы overengineering не стал бесполезным over-геморроингом.",
592
+ "positive_reactions_count": 0,
593
+ "cover_image": null,
594
+ "tag_list": [
595
+ "ruby",
596
+ "security",
597
+ "integration"
598
+ ],
599
+ "canonical_url": "https://blog.rnds.pro/025-matching?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
600
+ "reading_time_minutes": 1,
601
+ "user": {
602
+ "name": "Samoilenko Yuri",
603
+ "username": "kinnalru",
604
+ "twitter_username": null,
605
+ "github_username": "kinnalru",
606
+ "user_id": 559891,
607
+ "website_url": "https://teletype.in/@jerry_ru",
608
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
609
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
610
+ },
611
+ "organization": {
612
+ "name": "RNDSOFT",
613
+ "username": "rnds",
614
+ "slug": "rnds",
615
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
616
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
617
+ }
618
+ },
619
+ {
620
+ "type_of": "article",
621
+ "id": 1018265,
622
+ "title": "Синхронизация файлов без регистрации и СМС - lsyncd",
623
+ "description": "В одном из проектов RNDSOFT появилась необходимость обеспечить высокую надежность продукта при работе...",
624
+ "published": true,
625
+ "published_at": "2022-03-10T10:52:33.118Z",
626
+ "slug": "sinkhronizatsiia-failov-biez-rieghistratsii-i-sms-lsyncd-4cko",
627
+ "path": "/rnds/sinkhronizatsiia-failov-biez-rieghistratsii-i-sms-lsyncd-4cko",
628
+ "url": "https://dev.to/rnds/sinkhronizatsiia-failov-biez-rieghistratsii-i-sms-lsyncd-4cko",
629
+ "comments_count": 0,
630
+ "public_reactions_count": 2,
631
+ "page_views_count": 60,
632
+ "published_timestamp": "2022-03-10T10:52:33Z",
633
+ "body_markdown": "В одном из проектов RNDSOFT появилась необходимость обеспечить высокую надежность продукта при работе со сторонним софтом. И не просто с софтом, а таким, который работает с файлами - в самом что ни на есть [\"файловом\"](https://www.quobyte.com/storage-explained/posix-filesystem) виде. Далее я рассмотрю решение и подводные камни, о которые можно распороть пятку…\n\nЧитать тут: [https://blog.rnds.pro/024-lsyncd/](https://blog.rnds.pro/024-lsyncd/?utm_source=devto&utm_medium=post)",
634
+ "positive_reactions_count": 2,
635
+ "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4eyhybk7gl702k2mtdf2.jpeg",
636
+ "tag_list": [
637
+ "lsyncd",
638
+ "rsync",
639
+ "inotify"
640
+ ],
641
+ "canonical_url": "https://blog.rnds.pro/024-lsyncd/",
642
+ "reading_time_minutes": 1,
643
+ "user": {
644
+ "name": "Samoilenko Yuri",
645
+ "username": "kinnalru",
646
+ "twitter_username": null,
647
+ "github_username": "kinnalru",
648
+ "user_id": 559891,
649
+ "website_url": "https://teletype.in/@jerry_ru",
650
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
651
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
652
+ },
653
+ "organization": {
654
+ "name": "RNDSOFT",
655
+ "username": "rnds",
656
+ "slug": "rnds",
657
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
658
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
659
+ }
660
+ },
661
+ {
662
+ "type_of": "article",
663
+ "id": 1172535,
664
+ "title": "RSpec. Структура тестов. Как и зачем?",
665
+ "description": "Оригинал: https://blog.rnds.pro/023-rspec-tests-structure Мы в RNDSOFT очень любим писать тесты, а...",
666
+ "published": true,
667
+ "published_at": "2022-01-16T19:45:03.000Z",
668
+ "slug": "rspec-struktura-tiestov-kak-i-zachiem-4o1k",
669
+ "path": "/rnds/rspec-struktura-tiestov-kak-i-zachiem-4o1k",
670
+ "url": "https://dev.to/rnds/rspec-struktura-tiestov-kak-i-zachiem-4o1k",
671
+ "comments_count": 0,
672
+ "public_reactions_count": 0,
673
+ "page_views_count": 135,
674
+ "published_timestamp": "2022-08-21T08:29:32Z",
675
+ "body_markdown": "---\ntitle: RSpec. Структура тестов. Как и зачем?\npublished: true\ndate: 2022-01-16 19:45:03 UTC\ntags: rails\ncanonical_url: https://blog.rnds.pro/023-rspec-tests-structure?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds\n---\n\n![](https://img3.teletype.in/files/e5/a7/e5a78956-a8c7-4067-8857-42bc2aebf020.jpeg)\n\n_Оригинал: [https://blog.rnds.pro/023-rspec-tests-structure](https://blog.rnds.pro/023-rspec-tests-structure/?utm_source=devto&utm_medium=feed_rss&utm_campaign=rnds)_\n\nМы в **RNDSOFT** очень любим писать тесты, а самое главное, любим красивые тесты и тщательно следим за этим на code review. От любви к тестам и появилась эта статья.\n\n**RSpec** - это самый популярный фреймворк для тестирования в мире Ruby и, казалось бы, что о нём можно сказать? Имеются и статьи, и документация, недостатка в информации нет. Но плохо написанных тестов поразительно много. Почему так происходит? Придётся разбираться.\n\nВ рамках этой статьи я полагаю, что вы уже знакомы с RSpec, и вам доводилось писать на нём тесты. Мы не будем детально разбирать синтаксис RSpec, наша задача разобраться, как мыслить и какой инструментарий использовать, чтобы сделать тесты лучше.\n\n## О чём поговорим?\n\nRSpec - очень мощный инструмент, который позволяет не только эффективно тестировать код, но и документировать его.\n\nИметь в своём проекте хорошо структурированные тесты, которые ещё и документируют его, хотят наверное все, но не все умеют этого добиваться.\n\nА причина кроется чаще всего в незнании или осознанном не соблюдении принципов форматирования тестов, а чаще и в том, и в другом, да еще и при нехватке времени и постоянных выкатках новых фич. Поэтому я попытаюсь не только рассказать о существующих **стандартах** (обзорно), но и хочу донести их **смысл** , чтобы соблюдение этих правил было более осознанным. Для этого мы рассмотрим следующие вопросы:\n\n- Что такое хорошая структура тестов?\n- Почему мы хотим структурировать тесты?\n- Как документация RSpec учит плохому?\n- Чему учит Better Specs?\n- Почему rubocop-rspec - не злой контролёр, а мудрый учитель?\n\n## Что такое хорошая структура тестов?\n\nПрежде чем переходить к обсуждению правил написания хорошо структурированных тестов, нужно понять, как отличить хорошую структуру от плохой.\n\nРассуждать об этом вопросе можно очень долго, но я бы сократил этот разговор до нескольких правил.\n\nТест плохо структурирован, если:\n\n- тест нарушает правила форматирования, принятые в команде или в сообществе;\n- тест недостаточно документирует тестируемую логику и код;\n- у теста плохая архитектура (код громоздкий, запутанный, но его можно улучшить);\n- у теста есть технические проблемы, вызванные ошибками структурирования (работает медленно, не оптимально использует память и т.д.).\n\nКажется этих критериев будет достаточно. В остальных случаях тест можно считать хорошо структурированным.\n\n_Тут бы привести несколько примеров хороших и плохих тестов, но написанное далее сделает всё понятным и так._\n\n## Почему мы хотим структурировать тесты?\n\n### Тестируем простые функции\n\nДавайте напишем две функции s_um_ и _multiply_ в файле _math.rb_\n\n```\n# math.rb\ndef sum(x, y)\n x + y\nend\n\ndef multiply(x, y)\n x * y\nend\n```\n\nИ напишем для них тест в **максимально простой форме**\n\n```\n# math_spec.rb\nrequire './math'\n\nRSpec.describe do\n it { expect(sum(2, 2)).to eq 4 }\n\n it { expect(multiply(2, 2)).to eq 4 }\nend\n```\n\nКак видим, в данном случае у нас **нет** острой необходимости структурировать тест каким-то особым образом (практической пользы мы не получим), и будь на то возможность, мы бы отбросили и _describe_ (но так нельзя делать в рамках используемого фреймворка тестирования).\n\n### Тестируем простые методы\n\nЕсли представить что обе описанные выше функции стали методами класса _MyMath_, ситуация принципиально не поменяется и особая структура теста по-прежнему **не нужна** , а любовь к форматированию тестов это не достаточный повод.\n\n```\n# my_math_spec.rb\nrequire './my_math'\n\nRSpec.describe MyMath do\n it { expect(described_class.sum(2, 2)).to eq 4 }\n\n it { expect(described_class.multiply(2, 2)).to eq 4 }\nend\n```\n\nА теперь представим, что один метод - это метод класса, а второй - метод инстанса.\n\n```\n# my_math_spec.rb\nrequire './my_math'\n\nRSpec.describe MyMath do\n describe '.sum' do\n it { expect(described_class.sum(2, 2)).to eq 4 }\n end\n\n describe '#multiply' do\n subject(:math) { described_class.new }\n\n it { expect(math.multiply(2, 2)).to eq 4 }\n end\nend\n```\n\nТут мы начинаем обходиться с методами по-разному, теперь они принадлежат к двум различным группам методов и уже хочется сразу видеть эти различия, а не вчитываться в тело теста. Структура в тестах становится **нужной**.\n\n_Так же можно добавить, что в случае наличия нескольких методов инстанса, их можно выделить в отдельный context, где описать общее для всех методов создание инстанса класса. Но давайте придём к необходимости контекстов иным путём._\n\n### Тестируем методы с бизнес логикой\n\nОдно дело, когда метод выполняет простую работу, складывает числа например, невзирая на то, при каких обстоятельствах он это делает. И совсем другое, если результат этого метода будет зависеть от роли пользователя.\n\nПредставим, что только для роли _admin_ 2 + 2 = 4, а обычные пользователи живут в мире где 2 + 2 = 5. С умножением ситуация аналогичная.\n\n```\n# my_math_spec.rb\nrequire './my_math'\n\nRSpec.describe MyMath do\n context 'when user is admin' do\n describe '.sum' do\n it { expect(described_class.sum(2, 2)).to eq 4 }\n end\n\n describe '#multiply' do\n subject(:math) { described_class.new }\n\n it { expect(math.multiply(2, 2)).to eq 4 }\n end\n end\n\n context 'when user is client' do\n describe '.sum' do\n it { expect(described_class.sum(2, 2)).to eq 5 }\n end\n\n describe '#multiply' do\n subject(:math) { described_class.new }\n\n it { expect(math.multiply(2, 2)).to eq 5 }\n end\n end\nend\n```\n\nТут мы уже отчётливо видим **необходимость** контекстов. Контексты помогут отделить одну логику от другой, инициализировать переменные и данные в БД (если нужно), а так же позволят нашим тестам стать отличной документацией для других разработчиков.\n\n### Мы структурируем тесты, потому что...\n\nСтруктурирование тестов - это попытка обуздать **сложность** и внести **ясность**. Мы пытаемся максимально явно дать понять, что мы тестируем и при каких обстоятельствах. Понятность и лаконичность - вот к чему мы стремимся (не забывайте об этом при написании тестов). Если коротко, то мы хотим иметь адекватную [когнитивную нагрузку](https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D0%B8%D1%8F_%D0%BA%D0%BE%D0%B3%D0%BD%D0%B8%D1%82%D0%B8%D0%B2%D0%BD%D0%BE%D0%B9_%D0%BD%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B8) при дальнейшей работе с кодом.\n\n> Тесты - не только проверка работы кода, но и документация к нему. \n> И чем сложнее бизнес-логика проекта, тем ценнее способность тестов её документировать.\n\n## Как документация RSpec учит плохому?\n\nЗаголовок звучит странно, не так ли? Но так оно и есть.\n\nПрочитав документацию поверхностно и не изучив дополнительные материалы, можно легко выработать вредные привычки, избавиться от которых будет очень трудно.\n\n### Примеры из документации\n\nДословно приведём код из документации [rspec-core](https://rspec.info/documentation/3.10/rspec-core/):\n\n```\nRSpec.describe Order do\n it \"sums the prices of its line items\" do\n order = Order.new\n order.add_entry(LineItem.new(:item => Item.new(\n :price => Money.new(1.11, :USD)\n )))\n order.add_entry(LineItem.new(:item => Item.new(\n :price => Money.new(2.22, :USD),\n :quantity => 2\n )))\n expect(order.total).to eq(Money.new(5.55, :USD))\n end\nend\n```\n\nСразу оговорюсь, что я понимаю, что примеры в документации это максимально простая демонстрация возможностей гема, без лишних подробностей. Но увиденное тут становится для многих нормой, без попытки адаптировать пример к боевым условиям.\n\nПриведём ещё код из документации:\n\n```\nRSpec.describe \"Using an array as a stack\" do\n def build_stack\n []\n end\n before(:example) do\n @stack = build_stack\n end\n it 'is initially empty' do\n expect(@stack).to be_empty\n end\n context \"after an item has been pushed\" do\n before(:example) do\n @stack.push :item\n end\n it 'allows the pushed item to be popped' do\n expect(@stack.pop).to eq(:item)\n end\n end\nend\n```\n\n### Что же не так?\n\n**1. Имя класса Order используется в тесте напрямую, вместо использования _described\\_class_**\n\nКогда в _describe_ верхнего уровня описывается тестируемый класс (что является хорошей практикой), то разумно использовать внутри теста хелпер _described\\_class_. Это позволит сократить код в тех случаях, когда имя класса длинное, а также избавит от необходимости переписывать код теста в случае, если имя класса изменится.\n\n**2. Для описания тестируемого метода не используется _describe_**\n\nЧитая тест, мы хотим сразу понимать, какой метод тестируется. Поэтому хорошей практикой считается описание тестируемого метода при помощи блока _describe_.\n\n**3. Не лучшее использование _context_**\n\nНесмотря на то, что _context_ используется по назначению, а именно для описания того, в какой ситуации и при каких условиях выполняется конкретный тест, его использование должно было быть шире, а описание более строгим (далее поясню).\n\n**4. Использование переменных инстанса вместо _let_ и let!**\n\n_let_ и _let!_ хороши тем, что имеют ограниченную область видимости, в отличии от переменных инстанса. \nПеременная из _let_ будет инициализирована, только если она действительно понадобилась.\n\n**5. Тестируемый код, не очень хорошо подходит для объяснения основных концепций и принципов тестирования на RSpec**\n\nБыло бы лучше, если бы самые первые примеры в документации описывали тесты для самых простых случаев (как например метод для складывания чисел). Приведённые в документации примеры заставляют думать о большем, чем просто структура тестов (а ведь это может быть ваше первое знакомство с фреймворком, и вы вообще не понимаете, что происходит).\n\n> Документация - это источник знаний, имеющий высочайшую степень доверия к нему, и задача авторов документации - доносить свои идеи в кристально чистом виде и писать образцово-показательный код в примерах, ведь эти примеры станут для многих эталоном.\n\nК сожалению, подобная ситуация не редкость, поэтому нужно иметь ввиду, что не всегда документация к тому или иному инструменту является эталонным примером его использования в **реальных проектах** , а лишь описанием его возможностей.\n\n## Чему учит Better Specs?\n\n[Better Specs](https://www.betterspecs.org/) - набор правил, призванный сделать ваши тесты не только читаемыми, но и эффективными.\n\n> Из правил всегда есть исключения, но лишь настоящие эксперты с уверенностью могут определить исключительную ситуацию и осознанно пренебречь правилом. Остальные же рискуют попасть в ловушку своего незнания.\n\nМы рассмотрим лишь часть правил, а остальные оставим на самостоятельное изучение.\n\n### Вводное слово\n\nСтруктура тестов на RSpec в основном обеспечивается блоками:\n\n- describe;\n- context;\n- it.\n\nОстальные синтаксические конструкции уже дополняют структуру, выстроенную на их основе. Обсуждение синтаксиса RSpec заслуживает отдельного разговора, поэтому остановимся пока на этих трёх.\n\nДавайте посмотрим, как эти блоки применяются в документации [rspec-core](https://rspec.info/documentation/3.10/rspec-core/):\n\n```\nRSpec.describe Order\nRSpec.describe Array\nRSpec.describe Widget\nRSpec.describe \"Using an array as a stack\"\n\ndescribe '#add'\n\ncontext \"after an item has been pushed\"\ncontext \"with no items\"\ncontext \"with one item\"\n\nit \"sums the prices of its line items\"\nit 'is initially empty'\nit 'allows the pushed item to be popped'\n```\n\nЕсли вы опытный пользователь RSpec, то вы с лёгкостью определите назначения этих блоков, а также сможете отбросить мусорные примеры, они тут есть, но в целом не так всё и плохо. В реальных проектах встречаются тесты, написанные на порядок хуже, и описанные выше блоки в них используются как попало, разница между ними практически не различима.\n\n### Перейдём к правилам\n\nМы рассмотрим лишь часть правил, но следование им станет хорошим началом и ощутимо улучшит ваши тесты.\n\n### Describe Your Methods (описывайте ваши методы)\n\n```\n# ПЛОХО\ndescribe 'the authenticate method for User' do\ndescribe 'if the user is an admin' do\n\n# ХОРОШО\ndescribe '.authenticate' do\ndescribe '#admin?' do\n```\n\n**discribe** стоит использовать для описания тестируемого метода. Если это метод инстанса, нужно начитать именование с **#** , если метод класса то с **.** (или **::** ).\n\nОсобых комментариев, я думаю, тут не нужно. Это просто очень удобно. Обозначил метод, который тестируешь, а далее уже внутри блока описываешь все возможные контексты проверок и сами проверки.\n\nСделав как велит правило, вы превратите тест для конкретного метода в отдельную абстракцию, со своей логикой и областью видимости, которая не будет конфликтовать с остальными.\n\n### Use contexts (используйте контексты)\n\n```\n# ПЛОХО\nit 'has 200 status code if logged in' do\n expect(response).to respond_with 200\nend\n\nit 'has 401 status code if not logged in' do\n expect(response).to respond_with 401\nend\n\n# ХОРОШО\ncontext 'when logged in' do\n it { is_expected.to respond_with 200 }\nend\n\ncontext 'when logged out' do\n it { is_expected.to respond_with 401 }\nend\n```\n\n**Важно!** Описание контекста может начинаться с одного из трёх слов:\n\n- when;\n- with;\n- without.\n\nЗдесь мы получаем больше чем читаемость (но и её тоже)! Мы получили изолированную область видимости и можем делать в ней, что захотим. Можем описывать блоки **before** и **after** , можем описывать любые блоки **let** и **let!,** не боясь сломать другие тесты, в то время как блок **it** не может дать нам этого.\n\nБлок **context** умеет то же, что и **it** , но даёт большую гибкость.\n\nА что касательно правил именования (when, with, without), то это элементарное снижение когнитивной нагрузки. Если соблюдать это правило, блоки всегда будут выглядеть привычным и понятным образом и будут легко считываться всеми участниками команды.\n\n### Use let and let! (используйте let и let!)\n\n```\n# ПЛОХО\ndescribe '#type_id' do\n before { @resource = FactoryBot.create :device }\n before { @type = Type.find @resource.type_id }\n\n it 'sets the type_id field' do\n expect(@resource.type_id).to eq(@type.id)\n end\nend\n\n# ХОРОШО\ndescribe '#type_id' do\n let(:resource) { FactoryBot.create :device }\n let(:type) { Type.find resource.type_id }\n\n it 'sets the type_id field' do\n expect(resource.type_id).to eq(type.id)\n end\nend\n```\n\nBetter Specs ссылается на вот [такой ответ](https://stackoverflow.com/questions/5359558/when-to-use-rspec-let/5359979#5359979) на вопрос о целесообразности этого правила.\n\nЯ бы выделил тот факт, что переменная из _let_ будет инициализирована только в случае необходимости, что позволит вам повысить производительность. В случае когда вы грамотно используете **describe** и **context** , риск задать не используемую переменную снижается, но всё ещё остаётся, так что почему бы его не исключить полностью?\n\nЛюбые утверждения по поводу чистоты кода субъективны, но лично я согласен, что с использованием **let** код выглядит чище.\n\n## Почему rubocop-rspec - не злой контролёр, а мудрый учитель?\n\nПервым делом, вот вам ссылочка на [rubocop-rspec](https://github.com/rubocop/rubocop-rspec) (пока я буду тут рассуждать, вы как раз успеете затянуть его во все ваши проекты, если его там ещё нет).\n\nrubocop-rspec заслуживает отдельного разговора, но я не мог пройти мимо него, когда речь идёт о построении эффективных и читаемых тестов.\n\nЕсть те, кто не любят [Rubocop](https://github.com/rubocop/rubocop) как явление и считают, что он больше мешает, всё время ругая за что-то. Я же придерживаюсь противоположной точки зрения.\n\nНе будем распыляться и сконцентрируемся на rubocop-rspec. Если вы ознакомились с правилами, описанными в Better Spec, то rubocop-rspec это тот инструмент, который поможет вам их соблюдать.\n\nНемного из того, за что вас может поругать rubocop-rspec:\n\n- неверное именование контекстов;\n- использование переменных инстанса;\n- слишком большая вложенность блоков (многие страдают от огромной вложенности контекстов. А вот хороший способ её ограничить);\n- слишком много использований _let_ (иногда их действительно нужно много, а иногда это разработчик разошёлся и решил вынести в _let_ что-то, что нужно лишь для инициализации другого _let_, а в тесте не используется);\n- слишком много _expect_ в одном тесте;\n- слишком длинный тест;\n- и т.д.\n\nКак по мне, такой помощник просто незаменим.\n\n> Если rubocop-rspec вас ругает, то примерно в 99,9% случаев это вы решили сделать что-то недопустимое, и лишь 0,1% случаев это исключительная ситуация, где вы осознанно пренебрегаете правилом и отключаете его лишь там, где оно не актуально.\n\n> Чтобы уверенно отличать исключительные ситуации, где отключение правила Rubocop действительно оправдано, требуется определённый опыт, но и он не панацея, так что прежде чем отключить правило - подумайте дважды.\n\n## Итоги\n\nТесты - это не только проверка кода, но и его документация.\n\nДокументация должна быть читаемой, а значит вам нужны читаемые тесты.\n\nЧитаемые тесты достигаются через структурирование.\n\nХорошее структурирование будет затруднено без соблюдения правил форматирования (желательно общепринятых, вы же хотите, чтобы вас понимали другие).\n\n[Better Specs](https://www.betterspecs.org/) - это правила, которые вам стоит соблюдать.\n\n[rubocop-rspec](https://github.com/rubocop/rubocop-rspec) поможет вам соблюдать правила из Better Specs и не только.",
676
+ "positive_reactions_count": 0,
677
+ "cover_image": null,
678
+ "tag_list": [
679
+ "rails"
680
+ ],
681
+ "canonical_url": "https://blog.rnds.pro/023-rspec-tests-structure?utm_source=teletype&utm_medium=feed_rss&utm_campaign=rnds",
682
+ "reading_time_minutes": 3,
683
+ "user": {
684
+ "name": "Samoilenko Yuri",
685
+ "username": "kinnalru",
686
+ "twitter_username": null,
687
+ "github_username": "kinnalru",
688
+ "user_id": 559891,
689
+ "website_url": "https://teletype.in/@jerry_ru",
690
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
691
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
692
+ },
693
+ "organization": {
694
+ "name": "RNDSOFT",
695
+ "username": "rnds",
696
+ "slug": "rnds",
697
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
698
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
699
+ }
700
+ },
701
+ {
702
+ "type_of": "article",
703
+ "id": 924999,
704
+ "title": "Infra3: Мониторинг, Event Logging и сбор логов",
705
+ "description": "Выпустили очередную статью из цикла infra по нашей инфраструктуре. О то как мы в RNDSOFT строим...",
706
+ "published": true,
707
+ "published_at": "2021-12-13T12:40:57.922Z",
708
+ "slug": "infra3-monitoringh-event-logging-i-sbor-loghov-34j3",
709
+ "path": "/rnds/infra3-monitoringh-event-logging-i-sbor-loghov-34j3",
710
+ "url": "https://dev.to/rnds/infra3-monitoringh-event-logging-i-sbor-loghov-34j3",
711
+ "comments_count": 0,
712
+ "public_reactions_count": 2,
713
+ "page_views_count": 61,
714
+ "published_timestamp": "2021-12-13T12:40:57Z",
715
+ "body_markdown": "Выпустили очередную статью из [цикла infra](https://blog.rnds.pro/021-infra3?utm_source=devto&utm_medium=post) по нашей инфраструктуре. О то как мы в RNDSOFT строим процесс мониторинга, логирования и прочего observability. Подробности тут: [https://blog.rnds.pro/021-infra3](https://blog.rnds.pro/021-infra3?utm_source=devto&utm_medium=post)\n\n\n![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/khmsn8mu24nemn2q1sep.png)\n",
716
+ "positive_reactions_count": 2,
717
+ "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F51ff6upe9vstwpdu3udb.jpeg",
718
+ "tag_list": [
719
+ "consul",
720
+ "monitoring",
721
+ "prometheus",
722
+ "docker"
723
+ ],
724
+ "canonical_url": "https://blog.rnds.pro/021-infra3",
725
+ "reading_time_minutes": 1,
726
+ "user": {
727
+ "name": "Samoilenko Yuri",
728
+ "username": "kinnalru",
729
+ "twitter_username": null,
730
+ "github_username": "kinnalru",
731
+ "user_id": 559891,
732
+ "website_url": "https://teletype.in/@jerry_ru",
733
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
734
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
735
+ },
736
+ "organization": {
737
+ "name": "RNDSOFT",
738
+ "username": "rnds",
739
+ "slug": "rnds",
740
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
741
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
742
+ }
743
+ },
744
+ {
745
+ "type_of": "article",
746
+ "id": 891748,
747
+ "title": "Poison Message",
748
+ "description": "У нас в RNDSOFT есть один проект, в котором очень интенсивно используется брокер сообщений RabbitMQ....",
749
+ "published": true,
750
+ "published_at": "2021-11-08T10:02:53.570Z",
751
+ "slug": "poison-message-ohp",
752
+ "path": "/rnds/poison-message-ohp",
753
+ "url": "https://dev.to/rnds/poison-message-ohp",
754
+ "comments_count": 0,
755
+ "public_reactions_count": 4,
756
+ "page_views_count": 26,
757
+ "published_timestamp": "2021-11-08T10:02:53Z",
758
+ "body_markdown": "У нас в RNDSOFT есть один проект, в котором очень интенсивно используется брокер сообщений `RabbitMQ`. Под \"очень интенсивно\" я подразумеваю, что это единственный канал взаимодействия десятков сервисов - никаких вам `HTTP` и `REST`. И в этой статье мы рассмотрим понятие \"Poison Message\" и как с ним можно жить.\n\nЧитать тут: [https://blog.rnds.pro/018-posion1](https://blog.rnds.pro/018-posion1?utm_source=devto&utm_medium=post)\n\n\n![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bltlrfjmfk96ron0uro0.png)",
759
+ "positive_reactions_count": 4,
760
+ "cover_image": null,
761
+ "tag_list": [
762
+ "eventdriven",
763
+ "microservices",
764
+ "ruby"
765
+ ],
766
+ "canonical_url": "https://blog.rnds.pro/018-posion1",
767
+ "reading_time_minutes": 1,
768
+ "user": {
769
+ "name": "Samoilenko Yuri",
770
+ "username": "kinnalru",
771
+ "twitter_username": null,
772
+ "github_username": "kinnalru",
773
+ "user_id": 559891,
774
+ "website_url": "https://teletype.in/@jerry_ru",
775
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
776
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
777
+ },
778
+ "organization": {
779
+ "name": "RNDSOFT",
780
+ "username": "rnds",
781
+ "slug": "rnds",
782
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
783
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
784
+ }
785
+ },
786
+ {
787
+ "type_of": "article",
788
+ "id": 678710,
789
+ "title": "WorkerKiller",
790
+ "description": "Another gem for handling memory fragmentation and leaks. Working with Phusion Passenger and Delayed...",
791
+ "published": true,
792
+ "published_at": "2021-04-26T07:08:00.000Z",
793
+ "slug": "workerkiller-40ji",
794
+ "path": "/rnds/workerkiller-40ji",
795
+ "url": "https://dev.to/rnds/workerkiller-40ji",
796
+ "comments_count": 0,
797
+ "public_reactions_count": 1,
798
+ "page_views_count": 56,
799
+ "published_timestamp": "2021-04-26T07:08:00Z",
800
+ "body_markdown": "Another [gem for handling memory](https://github.com/RND-SOFT/worker_killer) fragmentation and leaks. Working with Phusion Passenger and Delayed Job by Memory Limiter and Request Limiter.\n\n[Expanded article(Russian)](https://blog.rnds.pro/015-workerkiller?utm_source=devto&utm_medium=post) about gem usage. \n",
801
+ "positive_reactions_count": 1,
802
+ "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8ux199hdxi7sl3zpwk2.jpeg",
803
+ "tag_list": [
804
+ "ruby",
805
+ "rails",
806
+ "passenger",
807
+ "delayedjob"
808
+ ],
809
+ "canonical_url": "https://blog.rnds.pro/015-workerkiller?utm_source=devto&utm_medium=post",
810
+ "reading_time_minutes": 1,
811
+ "user": {
812
+ "name": "Samoilenko Yuri",
813
+ "username": "kinnalru",
814
+ "twitter_username": null,
815
+ "github_username": "kinnalru",
816
+ "user_id": 559891,
817
+ "website_url": "https://teletype.in/@jerry_ru",
818
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
819
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
820
+ },
821
+ "organization": {
822
+ "name": "RNDSOFT",
823
+ "username": "rnds",
824
+ "slug": "rnds",
825
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
826
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
827
+ }
828
+ },
829
+ {
830
+ "type_of": "article",
831
+ "id": 673446,
832
+ "title": "polite timeout",
833
+ "description": "Simple ruby gem for gracefully timeout handling: Timeouter! An article(russian) about: polite timeout",
834
+ "published": true,
835
+ "published_at": "2021-04-21T05:32:00.000Z",
836
+ "slug": "polite-timeout-507c",
837
+ "path": "/rnds/polite-timeout-507c",
838
+ "url": "https://dev.to/rnds/polite-timeout-507c",
839
+ "comments_count": 0,
840
+ "public_reactions_count": 1,
841
+ "page_views_count": 73,
842
+ "published_timestamp": "2021-04-21T05:32:00Z",
843
+ "body_markdown": "Simple ruby gem for gracefully timeout handling: [Timeouter](https://github.com/RND-SOFT/timeouter)!\n\nAn article(russian) about: [polite timeout](https://blog.rnds.pro/014-timeouter?utm_source=devto&utm_medium=post)",
844
+ "positive_reactions_count": 1,
845
+ "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ve4owpkjllgr48y2qoe.png",
846
+ "tag_list": [
847
+ "ruby",
848
+ "gem",
849
+ "timeout",
850
+ "time"
851
+ ],
852
+ "canonical_url": "https://dev.to/rnds/polite-timeout-507c",
853
+ "reading_time_minutes": 1,
854
+ "user": {
855
+ "name": "Samoilenko Yuri",
856
+ "username": "kinnalru",
857
+ "twitter_username": null,
858
+ "github_username": "kinnalru",
859
+ "user_id": 559891,
860
+ "website_url": "https://teletype.in/@jerry_ru",
861
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
862
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
863
+ },
864
+ "organization": {
865
+ "name": "RNDSOFT",
866
+ "username": "rnds",
867
+ "slug": "rnds",
868
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
869
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
870
+ }
871
+ },
872
+ {
873
+ "type_of": "article",
874
+ "id": 649701,
875
+ "title": "Hyperstack vs Hotwire",
876
+ "description": "Пора читать новый перевод: Hyperstack vs Hotwire",
877
+ "published": true,
878
+ "published_at": "2021-03-30T06:34:12.806Z",
879
+ "slug": "hyperstack-vs-hotwire-54ap",
880
+ "path": "/rnds/hyperstack-vs-hotwire-54ap",
881
+ "url": "https://dev.to/rnds/hyperstack-vs-hotwire-54ap",
882
+ "comments_count": 0,
883
+ "public_reactions_count": 2,
884
+ "page_views_count": 246,
885
+ "published_timestamp": "2021-03-30T06:34:12Z",
886
+ "body_markdown": "Пора читать новый перевод: [Hyperstack vs Hotwire](https://blog.rnds.pro/012-hotwire?utm_source=devto&utm_medium=post)\n\n![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8eh3spwuag3lwy9gt8tp.png)\n\n",
887
+ "positive_reactions_count": 2,
888
+ "cover_image": null,
889
+ "tag_list": [
890
+ "rails",
891
+ "ruby",
892
+ "turbolinks",
893
+ "hotwire"
894
+ ],
895
+ "canonical_url": "https://blog.rnds.pro/012-hotwire?utm_source=devto&utm_medium=post",
896
+ "reading_time_minutes": 1,
897
+ "user": {
898
+ "name": "Samoilenko Yuri",
899
+ "username": "kinnalru",
900
+ "twitter_username": null,
901
+ "github_username": "kinnalru",
902
+ "user_id": 559891,
903
+ "website_url": "https://teletype.in/@jerry_ru",
904
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
905
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
906
+ },
907
+ "organization": {
908
+ "name": "RNDSOFT",
909
+ "username": "rnds",
910
+ "slug": "rnds",
911
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
912
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
913
+ }
914
+ },
915
+ {
916
+ "type_of": "article",
917
+ "id": 624677,
918
+ "title": "Infra2: Consul Registrator",
919
+ "description": "Вторая статья из цикла про инфраструктуру RNDSOFT. Сегодня про Consul Registrator - замечательный инс...",
920
+ "published": true,
921
+ "published_at": "2021-03-04T07:01:27.961Z",
922
+ "slug": "infra2-consul-registrator-46eh",
923
+ "path": "/rnds/infra2-consul-registrator-46eh",
924
+ "url": "https://dev.to/rnds/infra2-consul-registrator-46eh",
925
+ "comments_count": 0,
926
+ "public_reactions_count": 0,
927
+ "page_views_count": 143,
928
+ "published_timestamp": "2021-03-04T07:01:27Z",
929
+ "body_markdown": "Вторая статья из цикла про инфраструктуру RNDSOFT. Сегодня про Consul Registrator - замечательный инструмент, который нас никогда не подводил: [https://blog.rnds.pro/009-infra2](https://blog.rnds.pro/009-infra2?utm_source=devto&utm_medium=post)\n\n\n![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/msl3caqegpp1ehp3f69r.png)",
930
+ "positive_reactions_count": 0,
931
+ "cover_image": null,
932
+ "tag_list": [
933
+ "consul",
934
+ "traefik",
935
+ "docker",
936
+ "cloudnative"
937
+ ],
938
+ "canonical_url": "https://dev.to/rnds/infra2-consul-registrator-46eh",
939
+ "reading_time_minutes": 1,
940
+ "user": {
941
+ "name": "Samoilenko Yuri",
942
+ "username": "kinnalru",
943
+ "twitter_username": null,
944
+ "github_username": "kinnalru",
945
+ "user_id": 559891,
946
+ "website_url": "https://teletype.in/@jerry_ru",
947
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
948
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
949
+ },
950
+ "organization": {
951
+ "name": "RNDSOFT",
952
+ "username": "rnds",
953
+ "slug": "rnds",
954
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
955
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
956
+ }
957
+ },
958
+ {
959
+ "type_of": "article",
960
+ "id": 623769,
961
+ "title": "Бизнес логика в Rails приложениях",
962
+ "description": "Для многих не секрет, что Ruby on Rails — это фрэймворк, основанный на паттерне Model-View-Controller...",
963
+ "published": true,
964
+ "published_at": "2021-03-03T11:21:58.298Z",
965
+ "slug": "rails-56ad",
966
+ "path": "/rnds/rails-56ad",
967
+ "url": "https://dev.to/rnds/rails-56ad",
968
+ "comments_count": 0,
969
+ "public_reactions_count": 2,
970
+ "page_views_count": 122,
971
+ "published_timestamp": "2021-03-03T11:21:58Z",
972
+ "body_markdown": "Для многих не секрет, что Ruby on Rails — это фрэймворк, основанный на паттерне Model-View-Controller.\n\nИ не смотря на то, чтоRails позволяет решать огромное количество задач, данный паттерн становится нежизнеспособным в больших проектах, особенно в проектах с быстро меняющейся бизнес логикой. Решением данной проблемы может стать внедрение еще одного слоя, который вынесет бизнес логику из моделей.\nО том как мы в решаем эту проблему можно прочитать тут: [https://blog.rnds.pro/010-interactor](https://blog.rnds.pro/010-interactor?utm_source=devto&utm_medium=post)\n\n\n![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kepp90vzabuqaxw6tvz.png)",
973
+ "positive_reactions_count": 2,
974
+ "cover_image": null,
975
+ "tag_list": [
976
+ "rails",
977
+ "ruby"
978
+ ],
979
+ "canonical_url": "https://dev.to/rnds/rails-56ad",
980
+ "reading_time_minutes": 1,
981
+ "user": {
982
+ "name": "Samoilenko Yuri",
983
+ "username": "kinnalru",
984
+ "twitter_username": null,
985
+ "github_username": "kinnalru",
986
+ "user_id": 559891,
987
+ "website_url": "https://teletype.in/@jerry_ru",
988
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
989
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
990
+ },
991
+ "organization": {
992
+ "name": "RNDSOFT",
993
+ "username": "rnds",
994
+ "slug": "rnds",
995
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
996
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
997
+ }
998
+ },
999
+ {
1000
+ "type_of": "article",
1001
+ "id": 618821,
1002
+ "title": "Кешированные HTTP-соединения",
1003
+ "description": "Как-то на кухне зашел разговор за потерю времени на установление HTTP-соединения. Вот небольшая замет...",
1004
+ "published": true,
1005
+ "published_at": "2021-02-26T07:28:58.988Z",
1006
+ "slug": "http-ce1",
1007
+ "path": "/rnds/http-ce1",
1008
+ "url": "https://dev.to/rnds/http-ce1",
1009
+ "comments_count": 0,
1010
+ "public_reactions_count": 1,
1011
+ "page_views_count": 42,
1012
+ "published_timestamp": "2021-02-26T07:28:58Z",
1013
+ "body_markdown": "Как-то на кухне зашел разговор за потерю времени на установление HTTP-соединения. Вот небольшая заметка: [https://blog.rnds.pro/007-http](https://blog.rnds.pro/007-http?utm_source=devto&utm_medium=post)\n\n![http](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u5l4dskjzicyh7dt65m8.png)",
1014
+ "positive_reactions_count": 1,
1015
+ "cover_image": null,
1016
+ "tag_list": [
1017
+ "ruby",
1018
+ "http",
1019
+ "docker"
1020
+ ],
1021
+ "canonical_url": "https://dev.to/rnds/http-ce1",
1022
+ "reading_time_minutes": 1,
1023
+ "user": {
1024
+ "name": "Samoilenko Yuri",
1025
+ "username": "kinnalru",
1026
+ "twitter_username": null,
1027
+ "github_username": "kinnalru",
1028
+ "user_id": 559891,
1029
+ "website_url": "https://teletype.in/@jerry_ru",
1030
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
1031
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
1032
+ },
1033
+ "organization": {
1034
+ "name": "RNDSOFT",
1035
+ "username": "rnds",
1036
+ "slug": "rnds",
1037
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
1038
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
1039
+ }
1040
+ },
1041
+ {
1042
+ "type_of": "article",
1043
+ "id": 617870,
1044
+ "title": "RSpec-flaky: gem to deal with flaky test",
1045
+ "description": "The most common reason for test flakiness is randomized factories which fill a database before test e...",
1046
+ "published": true,
1047
+ "published_at": "2021-02-25T09:50:32.832Z",
1048
+ "slug": "rspec-flaky-gem-to-deal-with-flaky-test-49o7",
1049
+ "path": "/rnds/rspec-flaky-gem-to-deal-with-flaky-test-49o7",
1050
+ "url": "https://dev.to/rnds/rspec-flaky-gem-to-deal-with-flaky-test-49o7",
1051
+ "comments_count": 0,
1052
+ "public_reactions_count": 1,
1053
+ "page_views_count": 642,
1054
+ "published_timestamp": "2021-02-25T09:50:32Z",
1055
+ "body_markdown": "The most common reason for test flakiness is randomized factories which fill a database before test execution. [This small gem](https://github.com/RND-SOFT/rspec-flaky) is designed to help you find out what exactly attribute values were assigned to an investigated model during a failed and passed execution. \n\nArticle about gem: [https://blog.rnds.pro/006-rspecflaky](https://blog.rnds.pro/006-rspecflaky?utm_source=devto&utm_medium=article) (in russian)\n\n![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lh1w477u447mahbc1oob.png)",
1056
+ "positive_reactions_count": 1,
1057
+ "cover_image": null,
1058
+ "tag_list": [
1059
+ "ruby",
1060
+ "rails",
1061
+ "rspec"
1062
+ ],
1063
+ "canonical_url": "https://dev.to/rnds/rspec-flaky-gem-to-deal-with-flaky-test-49o7",
1064
+ "reading_time_minutes": 1,
1065
+ "user": {
1066
+ "name": "Samoilenko Yuri",
1067
+ "username": "kinnalru",
1068
+ "twitter_username": null,
1069
+ "github_username": "kinnalru",
1070
+ "user_id": 559891,
1071
+ "website_url": "https://teletype.in/@jerry_ru",
1072
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
1073
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
1074
+ },
1075
+ "organization": {
1076
+ "name": "RNDSOFT",
1077
+ "username": "rnds",
1078
+ "slug": "rnds",
1079
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
1080
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
1081
+ }
1082
+ },
1083
+ {
1084
+ "type_of": "article",
1085
+ "id": 597639,
1086
+ "title": "Infra1: Service Discovery, Cloud Native Proxy и деплой",
1087
+ "description": "Мы в RNDSOFT решили гармонично совместить процесс написания документации с выпуском цикла статей по...",
1088
+ "published": true,
1089
+ "published_at": "2021-02-11T05:00:00.000Z",
1090
+ "slug": "infra1-service-discovery-cloud-native-proxy-2m35",
1091
+ "path": "/rnds/infra1-service-discovery-cloud-native-proxy-2m35",
1092
+ "url": "https://dev.to/rnds/infra1-service-discovery-cloud-native-proxy-2m35",
1093
+ "comments_count": 0,
1094
+ "public_reactions_count": 4,
1095
+ "page_views_count": 110,
1096
+ "published_timestamp": "2021-02-11T05:00:00Z",
1097
+ "body_markdown": "Мы в RNDSOFT решили гармонично совместить процесс написания документации с выпуском [цикла статей](https://blog.rnds.pro/+infra?utm_source=devto&utm_medium=social) по подходам, используемым на многих продуктах, инструментам и особенностям нашей инфраструктуры. Тут будет и про consul и про traefik и про многое другое. [Читать тут](https://blog.rnds.pro/005-infra1?utm_source=devto&utm_medium=social). \n![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2wfj27m8ttm65zbamvub.png)",
1098
+ "positive_reactions_count": 4,
1099
+ "cover_image": null,
1100
+ "tag_list": [
1101
+ "consul",
1102
+ "traefik",
1103
+ "docker",
1104
+ "ruby"
1105
+ ],
1106
+ "canonical_url": "https://dev.to/rnds/infra1-service-discovery-cloud-native-proxy-2m35",
1107
+ "reading_time_minutes": 1,
1108
+ "user": {
1109
+ "name": "Samoilenko Yuri",
1110
+ "username": "kinnalru",
1111
+ "twitter_username": null,
1112
+ "github_username": "kinnalru",
1113
+ "user_id": 559891,
1114
+ "website_url": "https://teletype.in/@jerry_ru",
1115
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
1116
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
1117
+ },
1118
+ "organization": {
1119
+ "name": "RNDSOFT",
1120
+ "username": "rnds",
1121
+ "slug": "rnds",
1122
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
1123
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
1124
+ }
1125
+ },
1126
+ {
1127
+ "type_of": "article",
1128
+ "id": 579639,
1129
+ "title": "Ractors (Ракторы)",
1130
+ "description": "Перевод статьи про ракторы, которая мне показалась очень интересной: https://blog.rnds.pro/003-ractor...",
1131
+ "published": true,
1132
+ "published_at": "2021-01-22T19:14:58.540Z",
1133
+ "slug": "ractors-47pd",
1134
+ "path": "/rnds/ractors-47pd",
1135
+ "url": "https://dev.to/rnds/ractors-47pd",
1136
+ "comments_count": 0,
1137
+ "public_reactions_count": 1,
1138
+ "page_views_count": 46,
1139
+ "published_timestamp": "2021-01-22T19:14:58Z",
1140
+ "body_markdown": "Перевод статьи про ракторы, которая мне показалась очень интересной: https://blog.rnds.pro/003-ractors\n",
1141
+ "positive_reactions_count": 1,
1142
+ "cover_image": null,
1143
+ "tag_list": [
1144
+ "ruby",
1145
+ "ractors",
1146
+ "concurrency"
1147
+ ],
1148
+ "canonical_url": "https://dev.to/rnds/ractors-47pd",
1149
+ "reading_time_minutes": 1,
1150
+ "user": {
1151
+ "name": "Samoilenko Yuri",
1152
+ "username": "kinnalru",
1153
+ "twitter_username": null,
1154
+ "github_username": "kinnalru",
1155
+ "user_id": 559891,
1156
+ "website_url": "https://teletype.in/@jerry_ru",
1157
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
1158
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
1159
+ },
1160
+ "organization": {
1161
+ "name": "RNDSOFT",
1162
+ "username": "rnds",
1163
+ "slug": "rnds",
1164
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
1165
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
1166
+ }
1167
+ },
1168
+ {
1169
+ "type_of": "article",
1170
+ "id": 571605,
1171
+ "title": "Lusnoc - gem realized Mutex interface over Consul",
1172
+ "description": "It allows you to interact with Consul to provide distributed locks to your application. Source and...",
1173
+ "published": true,
1174
+ "published_at": "2021-01-14T14:13:00.000Z",
1175
+ "slug": "lusnoc-gem-realized-mutex-interface-over-consul-5ed0",
1176
+ "path": "/rnds/lusnoc-gem-realized-mutex-interface-over-consul-5ed0",
1177
+ "url": "https://dev.to/rnds/lusnoc-gem-realized-mutex-interface-over-consul-5ed0",
1178
+ "comments_count": 0,
1179
+ "public_reactions_count": 1,
1180
+ "page_views_count": 53,
1181
+ "published_timestamp": "2021-01-14T14:13:00Z",
1182
+ "body_markdown": "It allows you to interact with Consul to provide distributed locks to your application.\n\nSource and description: https://github.com/RND-SOFT/lusnoc\nArticle (russian): https://blog.rnds.pro/lusnoc",
1183
+ "positive_reactions_count": 1,
1184
+ "cover_image": "https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fkktukwnwnprx8rtdxbc5.jpeg",
1185
+ "tag_list": [
1186
+ "ruby",
1187
+ "consul",
1188
+ "distributedsystems",
1189
+ "mutex"
1190
+ ],
1191
+ "canonical_url": "https://dev.to/rnds/lusnoc-gem-realized-mutex-interface-over-consul-5ed0",
1192
+ "reading_time_minutes": 1,
1193
+ "user": {
1194
+ "name": "Samoilenko Yuri",
1195
+ "username": "kinnalru",
1196
+ "twitter_username": null,
1197
+ "github_username": "kinnalru",
1198
+ "user_id": 559891,
1199
+ "website_url": "https://teletype.in/@jerry_ru",
1200
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png",
1201
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png"
1202
+ },
1203
+ "organization": {
1204
+ "name": "RNDSOFT",
1205
+ "username": "rnds",
1206
+ "slug": "rnds",
1207
+ "profile_image": "https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png",
1208
+ "profile_image_90": "https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3591%2Fdbaf4648-1791-4e01-aa3b-aa6059717d48.png"
1209
+ }
1210
+ }
1211
+ ]