<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samoilenko Yuri</title>
    <description>The latest articles on DEV Community by Samoilenko Yuri (@kinnalru).</description>
    <link>https://dev.to/kinnalru</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559891%2Fe6a6dcca-2504-4ab8-ab71-d4b4cb6f492c.png</url>
      <title>DEV Community: Samoilenko Yuri</title>
      <link>https://dev.to/kinnalru</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kinnalru"/>
    <language>en</language>
    <item>
      <title>Selenium. Как заставить браузер работать на вас</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 25 Dec 2024 05:52:13 +0000</pubDate>
      <link>https://dev.to/rnds/selenium-kak-zastavit-brauzier-rabotat-na-vas-1mim</link>
      <guid>https://dev.to/rnds/selenium-kak-zastavit-brauzier-rabotat-na-vas-1mim</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9azkyq7fh0qahmlt3s3y.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9azkyq7fh0qahmlt3s3y.jpeg" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Всем привет! Сегодня я расскажу, что такое Selenium, как его запустить, зачем он нужен, и какие у него есть плюсы и минусы.&lt;/p&gt;

&lt;h2&gt;
  
  
  Небольшая вводная
&lt;/h2&gt;

&lt;p&gt;Selenium — это инструмент для автоматизации веб-браузеров. Он позволяет разработчикам и тестировщикам писать скрипты, которые могут управлять браузером, имитируя действия пользователя, такие как клики, ввод текста и навигация по страницам.&lt;/p&gt;

&lt;p&gt;Selenium поддерживает множество языков программирования, включая Python, Java, Ruby и другие, и может работать с различными браузерам. Это делает его популярным выбором для автоматизации тестирования веб-приложений и выполнения рутинных задач в браузере.&lt;/p&gt;

&lt;h2&gt;
  
  
  Задачи, решаемые Selenium
&lt;/h2&gt;

&lt;p&gt;Можно разделить на 2 большие группы. Это тестирование и парсинг (скрейпинг). Парсинг - это процесс извлечения и обработки данных из целевых ресурсов.&lt;/p&gt;

&lt;p&gt;К тестированию можно отнести такие подгруппы как:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Тестирование пользовательских интерфейсов.&lt;/strong&gt; Позволяет проверять элементы интерфейса, такие как кнопки, поля ввода и ссылки, чтобы убедиться, что они работают, как задумано.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Кросс-браузерное тестирование.&lt;/strong&gt; Позволяет запускать тесты в различных браузерах, что помогает убедиться, что приложение работает корректно в разных средах.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Регрессионное тестирование.&lt;/strong&gt; Позволяет повторно запускать тесты после внесения изменений в код, чтобы убедиться, что новые изменения не нарушили существующий функционал.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В простых случаях можно обойтись и без Selenium. Например получать html страницы с помощью простых инструментов, таких как curl.&lt;/p&gt;

&lt;p&gt;Но бывает более сложные случаи, когда Selenium становится нашим лучшим другом:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Динамически загружаемые страницы.&lt;/strong&gt; SPA приложению требуется js.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Обход ограничений.&lt;/strong&gt; Современные сайты часто содержат механизмы защиты от парсинга. Например, проверка cookie, user-agent и, конечно же, captcha.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Для обхода капчи часто используется комбинация Selenium, который позволяет выполнять js на странице и прогрузить капчу, и специального механизма для решения капчи. К таким механизмам относятся:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;_ &lt;strong&gt;Сторонние платные сервисы.&lt;/strong&gt; Принимают изображение или аудиофайл через API и возвращают готовое решение._&lt;/li&gt;
&lt;li&gt;_ &lt;strong&gt;Обученные нейронные сети.&lt;/strong&gt; Запускаются локально и самостоятельно распознают капчи._&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;После получения ответа от выбранного механизма Selenium используется для ввода решения в поле или для выполнения требуемых действий, например, выбора объектов вроде "светофоров" или “мостов”.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Конфигурирование приложения для работы с Selenium
&lt;/h2&gt;

&lt;p&gt;Рассмотрим конфигурирование на примере Ruby on Rails приложения.&lt;/p&gt;

&lt;p&gt;В первую очередь нам необходимо поставить гемы. Для этого добавляем в Gemfile следующие строки:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem 'selenium-webdriver'
gem 'webdrivers'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;И устанавливаем гемы:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Работа с Selenium
&lt;/h2&gt;

&lt;p&gt;Пример парсинга данных:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'selenium-webdriver'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'webdrivers'&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Selenium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for&lt;/span&gt; &lt;span class="ss"&gt;:chrome&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="s1"&gt;'http://example.com/products'&lt;/span&gt;
&lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-item'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-name'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'product-price'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Название: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, Цена: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Функционально пример выше найдет названия и цену товаров на воображаемом сайте и напечатает их в логах. Но этот код можно улучшить.&lt;/p&gt;

&lt;p&gt;Во-первых, нужно учитывать то, что Selenium - это браузер, а значит нам стоит ждать загрузки страницы. Поэтому модифицируем код, и при попытке получить список товаров, ждем 5 секунд и только потом падаем с ошибкой.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products = Selenium::WebDriver::Wait.new(timeout: 5).until do
  driver.find_elements(class: 'product-item')
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Во-вторых, нужно хоть немного замаскироваться от систем сайта, которые ограничивают работу парсеров. Для этого добавим немного “человечности” нашему браузеру.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--disable-blink-features=AutomationControlled')
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')
options.add_argument('--headless=new')
options.add_argument('--disable-gpu')
options.add_argument('window-size=1200x800')

driver = Selenium::WebDriver.for(:chrome, options: options)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Небольшое пояснение к опциям:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;--disable-blink-features=AutomationControlled.&lt;/strong&gt; Отключает некоторые функции Blink, которые указывают на то, что браузер управляется автоматизированным инструментом. Может помочь избежать обнаружения автоматизации на некоторых сайтах.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36.&lt;/strong&gt; Устанавливает пользовательский агент (User-Agent) для браузера. Пользовательский агент сообщает веб-сайтам, какую версию браузера и операционной системы использует пользователь. Установка пользовательского агента может помочь в обходе блокировок или в получении контента, оптимизированного для определенного браузера.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;--headless=new.&lt;/strong&gt; Запускает браузер в "безголовом" режиме, что означает, что он будет работать без графического интерфейса. Нужно для автоматизации и тестирования.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;--disable-gpu.&lt;/strong&gt; Отключает использование графического процессора (GPU). Полезно в безголовом режиме, так как некоторые функции, зависящие от GPU, могут вызывать проблемы или не поддерживаться.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;По итогу получится такой код:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'selenium-webdriver'
require 'webdrivers'

def wait(time)
  Selenium::WebDriver::Wait.new(timeout: time)
end

def parse
  options = Selenium::WebDriver::Chrome::Options.new
  options.add_argument('--disable-blink-features=AutomationControlled')
  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')
  options.add_argument('--headless=new')
  options.add_argument('--disable-gpu')

  driver = Selenium::WebDriver.for(:chrome, options: options)

  driver.navigate.to 'https://example.com/products'
  products = wait(5).until { driver.find_elements(class: 'product-item') }
  products.each do |product|
    name = product.find_element(class: 'product-name').text
    price = product.find_element(class: 'product-price').text
    Rails.logger.info { "Название: #{name}, Цена: #{price}" }
  end
rescue Selenium::WebDriver::Error::TimeoutError
  'Элемент с классом product-name не найден'
ensure
  driver.quit
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;И еще пара советов:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;В браузер можно подгружать плагины:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;options.add_extension(Rails.root.join('plugin.crx'))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Обязательно надо закрывать за собой браузер, чтобы избегать утечек памяти:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;driver.quit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Иногда требуется работа с cookie. В этом помогут следующие команды:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;driver.manage.all_cookies # получить все куки
driver.manage.cookie_named # получить куку по названию
driver.manage.add_cookie # записать куку
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Альтернативы Selenium
&lt;/h2&gt;

&lt;p&gt;Самые популярные инструменты для работы с виртуальными браузерами это:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selenium.&lt;/strong&gt; Старейший представитель. Поддержка множества браузеров и библиотек на различных языках. Большое и активное сообщество. Более сложное api, чем у остальных.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puppeteer.&lt;/strong&gt; Работает только на Node.js. Поддержка Chrome и Firefox. Сообщество растет, но меньше по сравнению с Selenium. Простое и интуитивно понятное api.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright.&lt;/strong&gt; Самый молодой представитель в этом списке. Поддержка множества браузеров и есть библиотеки на различных языках. Удобное и современное api. Встроенная поддержка мобильных устройств. Сообщество активно развивается.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Вывод
&lt;/h2&gt;

&lt;p&gt;Selenium является одним из самых популярных инструментов для автоматизации браузеров и тестирования веб-приложений. Широкая поддержка различных браузеров и языков программирования упрощает процесс написания кода для реализации задач, будь то тестирование или парсинг.&lt;/p&gt;

&lt;p&gt;Долговечность и стабильность подтверждают его надежность и эффективность.&lt;/p&gt;

&lt;p&gt;С большой вероятностью проблема, которую вы пытаетесь решить, уже кем-то была решена, и в интернете можно найти гайд, который поможет.&lt;/p&gt;

&lt;p&gt;Таким образом, Selenium остается одним из лучших инструментов для автоматизации браузеров. Используем его у себя в работе и вам советуем 🙂&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Простейший AI ассистент или Tools or not tools</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Thu, 12 Dec 2024 15:43:22 +0000</pubDate>
      <link>https://dev.to/rnds/prostieishii-ai-assistient-ili-tools-or-not-tools-4387</link>
      <guid>https://dev.to/rnds/prostieishii-ai-assistient-ili-tools-or-not-tools-4387</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Нужно бежать со всех ног, чтобы только оставаться на месте, а чтобы куда-то попасть, надо бежать как минимум вдвое быстрее! &lt;em&gt;Льюис Кэролл Алиса в Cтране Чудес&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqef2k6fo2rty6q7za0w4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqef2k6fo2rty6q7za0w4.jpeg" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Вступление&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;В данной статье мы продемонстрируем, как можно построить простейшего AI ассистента. Давайте сперва определимся с терминами. Обычно под AI ассистентом подразумевают способности Больших языковых моделей (далее по тексту LLM) не просто выдавать готовый текстовый ответ, но и совершать какую-то автономную работу по вызову сторонних функций, отправку запросов в API и на основании полученной информации из сторонних сервисов (но иногда нужно получить именно точный ответ в заданном формате), промпта, а также запроса пользователя выдавать итоговый ответ. Так, с терминами определились. Теперь вкратце о чем будет статья: в статье мы покажем, как 2мя способами LLM заставить взаимодействовать с внешним миром и с информацией, полученной не от пользователя, а из внешнего мира. В данной статье мы будем обогащать вывод LLM информацией из поиска, т.к. основной проблемой LLM является то, что в них информация заморожена на определенный момент времени, и с течением времени она устаревает и требует переобучения модели. Переобучение модели является очень дорогостоящим мероприятием, т.о. чтобы актуализировать информацию можно делать запросы в интернет, чтобы получать свежую информацию, а LLM будет нам, используя информацию из своего пространства знаний, а также дополняя информацией из поиска, выдавать достаточно свежий результат. На основе скриптов из этой статьи можно будет уже делать первые попытки для построения собственных мини-ассистентов.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Инструменты&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;В данной статье будут использованы следующие технологии:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;В качестве поискового движка будем использовать Tavily, т.к. у него простое API, а также они заявляют, что оптимизируют свой поиск как раз для использования с LLM (подробней можно почитать в документации к &lt;a href="https://docs.tavily.com/docs/welcome#tavily-search-api" rel="noopener noreferrer"&gt;tavily&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;LLM YandexGPT 4 Pro 32k RC&lt;/li&gt;
&lt;li&gt;Python, Gradio&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Нужно получить &lt;a href="https://yandex.cloud/ru/docs/foundation-models/api-ref/authentication#service-account_1" rel="noopener noreferrer"&gt;API ключи&lt;/a&gt; для YandexGPT API и &lt;a href="https://docs.tavily.com/docs/python-sdk/tavily-search/getting-started" rel="noopener noreferrer"&gt;ключ&lt;/a&gt; для Tavily&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Промптинг&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;В первом способе будем использовать результаты вызова функции в промпте для получения окончательного ответа от LLM как самый примитивный способ.&lt;/p&gt;

&lt;p&gt;Ниже представлен код скрипта &lt;code&gt;yc-search-prompt.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/env python3
import httpx
import os
import gradio as gr
from tavily import TavilyClient

BASE_YC_GPT_URL = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"

def format_search_results(search_results):
  formatted_results = "\nRelevant search results:\n"
  for result in search_results['results']:
formatted_results += f"- {result['title']}: - URL: {result['url']}  \n {result['content'][:200]}...\n"

def create_prompt_with_search(user_message, search_results):
  search_context = format_search_results(search_results)
  prompt = f"""Here is some relevant context from a web search:
  {search_context}
  Using the above context, please answer the following question:
  {user_message}
  Please provide a comprehensive answer based on both the search results and your knowledge.
  And add at the end of final answer all titles and URL links at format Title - Url from above context."""
  return prompt

def make_search_request(text):
  tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"] )
  response = tavily_client.search(text, max_results=8)
  return response

def make_request_yc_gpt(text, history):
  with httpx.Client() as client:
  headers = {'Authorization': "Api-Key " + os.environ['YC_API_KEY'], 
  'content-type':'application/json'}
  r = client.post(BASE_YC_GPT_URL, timeout=None, 
  json={"modelUri": "gpt://"+os.environ['YC_FOLDER_ID'] + "/yandexgpt-32k/rc",
  "completionOptions": {
  "stream": False,
  "temperature": "0.3",
  "maxTokens": "2000"
  },"messages": [{"role": "user","text": text}]}, headers=headers)
  return r.json()["result"]["alternatives"][0]["message"]["text"]

def chatbot_function(message, chat_history, model_choice):
  try:
    if model_choice == "YandexGPT+Tavily":
      search_results = make_search_request(message)
      enhanced_prompt = create_prompt_with_search(message, search_results)
      print(enhanced_prompt)
      bot_message = f"You selected the {model_choice} model.\n" + make_request_yc_gpt(enhanced_prompt, chat_history)
      chat_history.append((message, bot_message))
    else:
      bot_message = f"You selected the {model_choice} model.\n" + make_request_yc_gpt(message, chat_history)
      chat_history.append((message, bot_message))
    return "", chat_history
  except Exception as e:
    error_message = f"An error occurred: {str(e)}"
    chat_history.append((message, error_message))
    return "", chat_history
with gr.Blocks() as demo:
  gr.Markdown("AI prompting with internet search")
  with gr.Row():
    with gr.Column(scale=4):
      chatbot = gr.Chatbot()
      msg = gr.Textbox(label="Сообщение")
      submit = gr.Button("Отправить")
      clear = gr.Button("Очистить")
    with gr.Column(scale=1):
      model = gr.Radio(
          ["YandexGPT+Tavily", "YandexGPT"],
          label="Выберите модель",
          value="YandexGPT+Tavily"
        )
  submit.click(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot])
  msg.submit(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot])
  clear.click(lambda: None, None, chatbot, queue=False)
demo.launch()

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Примечание:&lt;/em&gt; &lt;strong&gt;Помните: при запросах к YandexGPT и Tavily могут списываться денежные средства. Перед запуском скрипта читайте актуальные правила использования сервисов.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Запускать скрипт следующим образом&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YC_FOLDER_ID=folder_id YC_API_KEY=YANDEX_API_KEY TAVILY_API_KEY=TAVILY_KEY  python3 yc-search-prompt.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Принцип работы скрипта: В UI Gradio на вход скрипт принимает текст от пользователя, в зависимости от того выбран ли вариант использования вместе с Tavily (YandexGPT+Tavily), тогда отправляется запрос в поиск Tavily, потом результат поиска отдается YandexGPT с промптом и просьбой сформировать окончательный ответ из собственных знаний, а также результатов поиска, а также в ответ добавить ссылки на источники из поиска.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Function calling (Tools)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Второй способ также будет использовать промпт для получения окончательного ответа, но для получения результатов поиска мы будем использовать функционал function calling. Наверное стоит остановиться подробней, для чего это нужно, т.к. кода стало почти в 2 раза больше, а результат такой же. Function calling (Tools) - это способность LLM вызывать сторонние приложения, это могут быть скрипты, обращения к различным API. В большинстве случаев это необходимо, когда для LLM нужно получить конкретный ответ (конечно, пример с использованием поиска не очень подходящий, но хотелось сделать примеры максимально похожими, больше здесь подходит, например, вызов функции, которая использует калькулятор), что-то посчитать, а т.к. LLM не предназначены для конкретных вычислений, то для этого используется функционал function calling. При этом если Tools будет много, то LLM может и сама принимать решение, когда и какой Tool ей вызывать (у anthropic есть прямо &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use#controlling-claudes-output" rel="noopener noreferrer"&gt;определение поведения&lt;/a&gt; LLM для выбора Tools. Важный момент: нужно делать хорошее описание для tools. Вот примеры хороших и плохих описаний tools от одного из &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use#best-practices-for-tool-definitions" rel="noopener noreferrer"&gt;лидеров индустрии&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Примечания:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;в YandexGPT API на момент написания статьи функционал Tools находился в режиме бета-тестирования, может быть непредвиденное поведение.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;На момент написания статьи в скриптах использовалась версия релиз кандидат YandexGPT RC 32k, подробней про жизненный цикл моделей &lt;a href="https://yandex.cloud/ru/docs/foundation-models/concepts/yandexgpt/models#model-lifecycle" rel="noopener noreferrer"&gt;читайте в документации&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ниже представлен код скрипта &lt;code&gt;yc-search-tools.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/env python3

import httpx
import os
import json
from tavily import TavilyClient
import gradio as gr

BASE_YC_GPT_URL = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"
search_tool = {
  "function": {
    "name": "search_tavily",
    "description": "Search the web for current information",
      "parameters": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "The search query"
          }
        },
        "required": ["query"]
      }
  }
}

def format_search_results(search_results):
  formatted_results = "\nRelevant search results:\n"
  for result in search_results['results']:
    formatted_results += f"- {result['title']}: - URL: {result['url']} \n {result['content'][:200]}...\n"
  return formatted_results

def create_prompt_with_search(user_message, search_results):
  search_context = format_search_results(search_results)
  prompt = f"""Here is some relevant context from a web search:
{search_context}

Using the above context, please answer the following question:
{user_message}

Please provide a comprehensive answer based on both the search results and your knowledge.
And add at the end of final answer all titles and URL links at format Title - Url from above context."""
  return prompt

def make_request_yc_gpt(text, is_tool_call=True):
  with httpx.Client() as client:
    headers = {'Authorization': "Api-Key " + os.environ['YC_API_KEY'], 
    'content-type':'application/json'}
    payload = {
      "modelUri": f"gpt://{os.environ['YC_FOLDER_ID']}/yandexgpt-32k/rc",
      "completionOptions": {
        "stream": False,
        "temperature": 0.0,
        "maxTokens": 8000
      },
      "messages": text
    }

    if is_tool_call:
      payload["tools"] = [search_tool]

    r = client.post(
    BASE_YC_GPT_URL,
    timeout=None,
    json=payload,
    headers=headers
    )

    response = r.json()
  return response

def handle_tool_calls(toolCalls):
  results = []
  for tool_call in toolCalls:
    if toolCalls[0]["functionCall"]["name"] == "search_tavily":
      result = make_search_request(tool_call["functionCall"]["arguments"]["query"])
  return result
def make_search_request(text):
  tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"] )
  response = tavily_client.search(text, max_results=8)
  return response

def process_conversation(user_input, history):
  conversation = [
    {
      "role": "system",
      "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."
    },
    {
      "role": "user",
      "text": user_input
    }
  ]
  initial_response = make_request_yc_gpt(conversation)

  if "toolCalls" in initial_response['result']['alternatives'][0]['message']['toolCallList']:
    tool_results = handle_tool_calls(
      initial_response['result']['alternatives'][0]['message']['toolCallList']["toolCalls"]
    )

    enhanced_prompt = create_prompt_with_search(user_input, tool_results)

    final_conversation = [
      {
        "role": "user",
        "text": enhanced_prompt
      }
    ]

    final_response = make_request_yc_gpt(final_conversation, is_tool_call=False)
    return final_response['result']['alternatives'][0]['message']['text']
  else:
    return initial_response['result']['alternatives'][0]['message']['text']

def chatbot_function(message, chat_history, model_choice):
  try:
    response_list = process_conversation(message, chat_history)

    if isinstance(response_list, list):
      formatted_responses = [item['text'] for item in response_list if isinstance(item, dict) and 'text' in item] 
      bot_message = f"You selected the {model_choice} model.\n" + "\n".join(formatted_responses)
    else:
      bot_message = f"You selected the {model_choice} model.\n" + str(response_list)
    chat_history.append((message, bot_message))
    return "", chat_history 
  except Exception as e: 
    error_message = f"An error occurred: {str(e)}" 
    chat_history.append((message, error_message)) 
    return "", chat_history
with gr.Blocks() as demo:
  gr.Markdown("AI function calling tools internet search") 
    with gr.Row(): 
      with gr.Column(scale=4): 
        chatbot = gr.Chatbot() 
        msg = gr.Textbox(label="Сообщение") 
        submit = gr.Button("Отправить") 
        clear = gr.Button("Очистить") 
      with gr.Column(scale=1):
        model = gr.Radio( 
          ["YandexGPT+Tavily"], 
          label="Модель", 
          value="YandexGPT+Tavily" 
        ) 
  submit.click(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot]) 
  msg.submit(chatbot_function, inputs=[msg, chatbot, model], outputs=[msg, chatbot]) 
  clear.click(lambda: None, None, chatbot, queue=False) 
demo.launch()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Примечание:&lt;/em&gt; &lt;strong&gt;Помните, при запросах к YandexGPT и Tavily могут списываться денежные средства. Перед запуском скрипта читайте актуальные правила использования сервисов.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Запускать скрипт следующим образом&lt;/p&gt;

&lt;p&gt;&lt;code&gt;YC_FOLDER_ID=folder_id YC_API_KEY=YANDEX_API_KEY TAVILY_API_KEY=TAVILY_KEY  python3 yc-search-tools.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Вкратце принцип работы скрипта: В UI Gradio на вход скрипт принимает текст от пользователя, вызывает tool tavily_search, после этого результаты поиска отдается YandexGPt с промптом и просьбой сформировать окончательный ответ из собственных знаний, результатов поиска, а также в ответ добавить ссылки на источники из поиска.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Итоги и дополнительные материалы&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;В итоге мы получили простенький аналог perplexity.ai, сделанный своими руками. Несколько скринов как это выглядит.&lt;/p&gt;

&lt;p&gt;Ответ YandexGPT, дополненный информацией из Tavily&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcJbJAI8kAVt__Oh7QxUEfbEdLVUd1PM_8QhHozgTxkYysnJy-TwswWc5LHTALoKykLIxo5NcPFQysdTYPf2aTgNv8O2jRi-MoS5bcvVwiprp_u5U8KOrEeYgf_MWVeC58PM1Oo1w%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcJbJAI8kAVt__Oh7QxUEfbEdLVUd1PM_8QhHozgTxkYysnJy-TwswWc5LHTALoKykLIxo5NcPFQysdTYPf2aTgNv8O2jRi-MoS5bcvVwiprp_u5U8KOrEeYgf_MWVeC58PM1Oo1w%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT дополненный информацией из Tavily&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;И ниже ответ дополняется ссылками (блок Titles and URLs) в поиске&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeGwTUa0egcd0d3Ul4ZnRlsdnRm4Oth5r9L2S8LUl7A-L-u0xdQlxQsNi9glUEygtrIvZlg_4LclUISlB7bppvD3f2h7j8c5lr-hbymRtHia2JjMgUIyWytv9pECVQ3SwmK5n71DA%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeGwTUa0egcd0d3Ul4ZnRlsdnRm4Oth5r9L2S8LUl7A-L-u0xdQlxQsNi9glUEygtrIvZlg_4LclUISlB7bppvD3f2h7j8c5lr-hbymRtHia2JjMgUIyWytv9pECVQ3SwmK5n71DA%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="443"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT дополненный информацией из Tavily&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ответ YandexGPT без дополнения ответа результатами из поиска, как видно внизу без ссылок на результаты поиска&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXf1nEIXuj2_I2fENHgvryV7nbi4dMTKjjwdR7u3RnFDXz-NRaawv8-sSqoKSk0Y1SHgQnsjR5eRQn6fsXS5SUfbrF6pz0p5LqHUt5zR-SJzoQI9Zwq4pIrlq_l8bh-kypZOXUvl7Q%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXf1nEIXuj2_I2fENHgvryV7nbi4dMTKjjwdR7u3RnFDXz-NRaawv8-sSqoKSk0Y1SHgQnsjR5eRQn6fsXS5SUfbrF6pz0p5LqHUt5zR-SJzoQI9Zwq4pIrlq_l8bh-kypZOXUvl7Q%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="438"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT без дополненным ответом из поиска&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Приводим аналогичные скрины с использованием функционала Tools&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXdQxydh47aqy9Z0CUGYvsn57NT9l79qLo_Rdm2K94Xabl-AeI3JXCZSjsE_esTQ02RutkkCQU1dzAT9pdgGeG8haHNybUrLEylXEfHFf0I_HVthKogWFFTWl_d_H_FqYCHRkZwS%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXdQxydh47aqy9Z0CUGYvsn57NT9l79qLo_Rdm2K94Xabl-AeI3JXCZSjsE_esTQ02RutkkCQU1dzAT9pdgGeG8haHNybUrLEylXEfHFf0I_HVthKogWFFTWl_d_H_FqYCHRkZwS%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="435"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT + Tavily + Function calling (Tools)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXfS0QQxLYPTKE3p_QkRlrqBIsq2GHCmDhFnXQ8KwASXqkLOEJbVoiq4wztaGQ8CnfbcT0-Z2s8O1623EhOsa5VwSefNHimaJbikHIBJcFKIXplvQZcLf0nouWzggTRFklbyFmy3gA%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXfS0QQxLYPTKE3p_QkRlrqBIsq2GHCmDhFnXQ8KwASXqkLOEJbVoiq4wztaGQ8CnfbcT0-Z2s8O1623EhOsa5VwSefNHimaJbikHIBJcFKIXplvQZcLf0nouWzggTRFklbyFmy3gA%3Fkey%3DUhEGguG-LoZ_EPfcxuJK_gbd" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;YandexGPT + Tavily + Function calling (Tools)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Одним из самых частых препятствий в процессе промышленного внедрения LLM являются галлюцинации и проблемы получения конкретных ответов, и хочется сказать еще пару слов про библиотеки и фреймворки, которые могут быть полезны для решения этих проблем.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Не пренебрегайте промптами, если у вас нет других инструментов для контроля LLM.  Хороший материал на тему &lt;a href="https://www.promptingguide.ai/" rel="noopener noreferrer"&gt;prompt engineering&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Кроме промптинга есть фреймворки для работы с промптами, например, &lt;a href="https://dspy.ai/" rel="noopener noreferrer"&gt;dspy&lt;/a&gt;, к сожалению, из коробки поддержки YandexGPT там нет, но можно пробовать использовать &lt;a href="https://github.com/all-mute/openai2yandex_api_adapter" rel="noopener noreferrer"&gt;адаптер для совместимости с OpenAI API&lt;/a&gt; + dspy (сами, честно говоря, еще не пробовали) и &lt;a href="https://yandex.cloud/ru/features/4631" rel="noopener noreferrer"&gt;проголосовать за фичу&lt;/a&gt; в Yandex Cloud&lt;/li&gt;
&lt;li&gt;Помимо коммерческих реализаций есть также уже много open source моделей, в которых реализован функционал tools, например, у &lt;a href="https://ollama.com/search?c=tools" rel="noopener noreferrer"&gt;ollama&lt;/a&gt;. Кажется, скоро эта фича станет стандартной в LLM.&lt;/li&gt;
&lt;li&gt;Кроме промптинга есть еще возможность заставить LLM четко следовать формату ответа, это т.н. structured output, вот &lt;a href="https://simmering.dev/blog/structured_output/" rel="noopener noreferrer"&gt;хорошая статья&lt;/a&gt; с библиотеками, большинство библиотек в этом списке тоже не поддерживают YandexGPT (а также почти все нестабильных версий 0.x.x), но чтобы этот мир стал еще лучше, можете проголосовать &lt;a href="https://yandex.cloud/ru/features/4627" rel="noopener noreferrer"&gt;за эту фичу&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Фреймворки для построения агентов и мультиагентских систем&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://langchain-ai.github.io/langgraph/tutorials/introduction/" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; кандидат, чтобы стать стандартом в индустрии (там готовится целая экосистема библиотек для работы с LLM LangChain, LangSmith, LangGraph), самая большая поддержка различных LLM. Очень нестабильно. Но в документации к LangGraph можно почерпнуть много хороших идей для построения агентов &lt;a href="https://langchain-ai.github.io/langgraph/tutorials/" rel="noopener noreferrer"&gt;Tutorials&lt;/a&gt; и &lt;a href="https://langchain-ai.github.io/langgraph/how-tos/" rel="noopener noreferrer"&gt;How-To&lt;/a&gt;, это прямо must read! Для построения MVP и быстрого прототипирования подходит отлично.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.llamaindex.ai/en/stable/use_cases/agents/" rel="noopener noreferrer"&gt;Llamaindex&lt;/a&gt; Тоже довольно большая библиотека для работы с LLM. Подробней что-то  рассказать сложно, сами не пользовались.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://microsoft.github.io/autogen/0.2/" rel="noopener noreferrer"&gt;AutoGen&lt;/a&gt; библиотека для построения мультиагентских систем. Огромное кол-во примеров под разные use cases.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Совсем недавно в Yandex cloud появился новый функционал &lt;a href="https://yandex.cloud/ru/docs/foundation-models/concepts/assistant/" rel="noopener noreferrer"&gt;AI assistant&lt;/a&gt;, который с использованием их ML SDK тоже позволяет строить LLM приложения и прячет некоторые вещи “под капот”, которые обычно используются в LLM приложениях: RAG, сохранение контекста.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Также совсем недавно anthropic выпустил &lt;a href="https://modelcontextprotocol.io/introduction" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;, для более глубокой интеграции LLM, tools и источников данных. Посмотрим, сможет ли MCP стать стандартом в будущем.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Также аналогичный функционал, скорей всего, можно реализовать более простым способом, использовав лишь один инструмент &lt;a href="https://yandex.cloud/ru/docs/search-api/concepts/generative-response" rel="noopener noreferrer"&gt;Search API&lt;/a&gt; в Yandex Cloud (на момент написания статьи функционал был в Preview режиме)&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>other</category>
    </item>
    <item>
      <title>Централизованное логирование</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Thu, 03 Nov 2022 13:12:06 +0000</pubDate>
      <link>https://dev.to/rnds/tsientralizovannoie-loghirovaniie-naa</link>
      <guid>https://dev.to/rnds/tsientralizovannoie-loghirovaniie-naa</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fktmqer6g4p7xl2hbqeyy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fktmqer6g4p7xl2hbqeyy.png" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В данной статье мы рассмотрим вопрос централизованного логирования с использованием filebeat (гребаный спойлер) и graylog. В какой-то момент мы заметили, что машин в нашей инфраструктуре стало достаточно много, и чтобы посмотреть логи, приходилось иногда заходить на несколько машин и мучительно искать по множеству контейнеров, в этот момент мы поняли, что дальше жить так нельзя.&lt;/p&gt;

&lt;p&gt;Постановка задачи стояла следующим образом:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Чем собирать логи&lt;/li&gt;
&lt;li&gt;Где хранить&lt;/li&gt;
&lt;li&gt;Нужно не потерять логи при недоступности центрального хранилища логов&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ранее мы уже пробовали централизованно собирать логи vector (&lt;a href="https://vector.dev/" rel="noopener noreferrer"&gt;https://vector.dev/&lt;/a&gt;), но нам хотелось иметь удобный веб-интерфейс для просмотра. А vector веб-интерфейса не имеет. В качестве веб-интерфейса, а заодно и местом централизованного хранения, мы выбрали graylog, фактически сейчас единственное open source решение, которое имеет обширный функционал и удобство использования. На момент написания статьи vector “из коробки” не умеет отправлять в gelf формате в graylog, но в одном из issues на github один энтузиаст смог сконфигурировать vector нужным образом (&lt;a href="https://github.com/vectordotdev/vector/issues/4868#issuecomment-782740639" rel="noopener noreferrer"&gt;https://github.com/vectordotdev/vector/issues/4868#issuecomment-782740639&lt;/a&gt;). В общем vector в качестве решения для сбора логов отбросили.&lt;/p&gt;

&lt;p&gt;Прежде чем отвечать на вопрос “Чем собирать”, нужно немного сказать про инфраструктуру: в инфраструктуре у нас все приложения запускаются в docker контейнерах, машин на момент написания у нас уже 16, а самих контейнеров около 60, контейнеры в свою очередь находятся под управлением nomad. Также важным моментом является то, что несколько машин в нашей инфраструктуре являются прерываемыми, они один раз в сутки перезапускаются. С инфраструктурой разобрались, теперь нужно определяться, чем собирать. Сперва наш взгляд упал на fluentd, fluentbit, т.к. они умели отправлять в gelf формате. В процессе настройки fluentd почему-то не слал больше 30 сообщений, разобраться, почему, нам так и не удалось, при использовании fluentbit в какой-то момент использования docker logging driver gelf и настройки fluentbit c output gelf выяснилось, что при недоступности fluentbit в момент рестарта контейнеров (на прерываемых машинах контейнеры один раз в сутки перезапускаются) они не запустятся и становятся неработоспособными. Мы поняли, что чем проще - тем лучше, и решили, что нужно собирать из файлов. В свою очередь настроили fluentbit для сбора из файлов, но fluentbit почему-то тоже доставлял не все логи, аналогично с fluentd определить, почему так происходит, не удалось. И в итоге мы остановились на filebeat в качестве сборщика логов (&lt;a href="https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html" rel="noopener noreferrer"&gt;https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html&lt;/a&gt;), а graylog в качестве хранилища логов.&lt;/p&gt;

&lt;p&gt;Еще один важный момент, который нужно учесть, это то, что graylog находится в другой инфраструктуре, т.е. прямой сетевой связности между graylog сервером и машинами, на которых запущен filebeat, нет. Т.о. мы связали это все через гейт, на котором у нас находится traefik (смотри диаграмму ниже). Теперь перейдем к настройке. Graylog мы запустили на отдельной машине в обычном docker-compose.yml файле.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FWzf3hrjZEtkmygD2Myb8HKG9vYQdSzdaCS8UdGvXwEfO8hBBqSO_OVuI80kjGd3L26CzZSVCDNPCvxLLeVeIK-0HZS3NNW2dOCEOEtTp8VmHwu0MX--123ayMNjPmAKWZ0IC-2LgrNoAFt2VrbzPi3U7G0EMlDhIgANWdpcAHFQJQOnCAlcXivE_WQ" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FWzf3hrjZEtkmygD2Myb8HKG9vYQdSzdaCS8UdGvXwEfO8hBBqSO_OVuI80kjGd3L26CzZSVCDNPCvxLLeVeIK-0HZS3NNW2dOCEOEtTp8VmHwu0MX--123ayMNjPmAKWZ0IC-2LgrNoAFt2VrbzPi3U7G0EMlDhIgANWdpcAHFQJQOnCAlcXivE_WQ" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Graylog docker-compose.yml&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: "2.3"

services:

 es01:
   image: elasticsearch:7.10.1
   restart: unless-stopped
   network_mode: host
   ulimits:
     memlock:
       soft: -1
       hard: -1
     nofile:
       soft: 65536
       hard: 65536
   environment:
       - "ES_JAVA_OPTS=-Xms5g -Xmx5g"
       - ELASTIC_PASSWORD=test
       - discovery.type=single-node
       - cluster.max_shards_per_node=20000
       - search.max_open_scroll_context=9000
   volumes:
     - /etc/localtime:/etc/localtime:ro
     - /graylog/data/es01:/usr/share/elasticsearch/data
   labels:
     - "SERVICE_CHECK_HTTP=/_cluster/health"
     - "SERVICE_CHECK_INTERVAL=40s"
     - "SERVICE_CHECK_TIMEOUT=3s"
     - "SERVICE_CHECK_DEREGISTER_AFTER=10m"

 mongodb:
   image: mongo:4.2
   restart: unless-stopped
   network_mode: host
   volumes:
     - /graylog/data/mongo:/data/db

 graylog:
   image: graylog/graylog:4.3.3
   restart: unless-stopped
   depends_on:
     - mongodb
     - es01
   network_mode: host
   volumes:
     - /graylog/data/graylog:/usr/share/graylog/data
   environment:
     - GRAYLOG_PASSWORD_SECRET=test
     - GRAYLOG_ROOT_PASSWORD_SHA2=shashasha
     - GRAYLOG_HTTP_EXTERNAL_URI=http://192.168.2.2:9000/
   entrypoint: /usr/bin/tini -- wait-for-it 172.22.202.104:9200 -- /docker-entrypoint.sh
   labels:
     - "SERVICE_TAGS=hostname=graylog,traefik.enable=true,\
        traefik.http.routers.graylog.rule=Host(`graylog.dev`),\
        traefik.http.routers.graylog.entrypoints=websecure,\
        traefik.http.routers.graylog.tls=true"
     - SERVICE_NAME=graylog
     - SERVICE_CHECK_DEREGISTER_AFTER=10m
     - SERVICE_9000_NAME=graylog
     - SERVICE_9000_CHECK_TCP=true
     - SERVICE_9000_CHECK_INTERVAL=40s
     - SERVICE_9000_CHECK_TIMEOUT=5s
     - SERVICE_9000_CHECK_DEREGISTER_AFTER=10m

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Перейдем к конфигурации filebeat&lt;/p&gt;

&lt;p&gt;Конфиг filebeat.yml у нас простейший&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;filebeat.inputs:
- type: container
  paths:
    - /var/lib/docker/containers/*/*.log
  processors:
    - add_docker_metadata:
        match_source_index: 4 
output.logstash:
   hosts: 
     - graylog_host:graylog_port

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Тут все просто, мы просто отправляем все логи всех контейнеров на хост graylog, в свою очередь graylog_host - это у нас хост с traefik, а в конфигурации traefik уже указан сам graylog, т.к. с гейта есть прямая сетевая связность с graylog.&lt;/p&gt;

&lt;p&gt;Т.к. у нас 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 &lt;a href="https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html#_how_does_filebeat_keep_the_state_of_files" rel="noopener noreferrer"&gt;https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html#_how_does_filebeat_keep_the_state_of_files&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;job "filebeat" {
 datacenters = ["a", "b", "c"]
 namespace = "default"
 update {
   max_parallel = 1
   auto_revert = true
   auto_promote = false
   canary = 0
 }
 type = "system"
 group "logging" {
   restart {
     mode = "delay"
     attempts = 2
     interval = "1m"
     delay = "30s"
   }
   task "filebeat" {
     driver = "docker"
     kill_timeout = "30s"
     leader = true
     user = "root"
     env {
       SERVICE_IGNORE = "true"
       GRAYLOG_HOST = "GRAYLOG_HOST"
       GRAYLOG_PORT = "GRAYLOG_PORT"
     }
     config {
       auth {
         username = "test"
         password = "test"
       }
       image = "docker.registry/filebeat:7.10.1"
       force_pull = true
       volumes = [
         "/var/lib/docker/containers:/var/lib/docker/containers:ro",
         "/var/run/docker.sock:/var/run/docker.sock:ro",
         "/filebeat/registry:/filebeat/registry:rw"
       ]
     }
     service {
       name = "filebeat"
       tags = ["filebeat", "logging", "hostname=${attr.unique.hostname}"]
       check_restart {
         grace = "1m"
         ignore_warnings = false
       }
     }
     logs {
       max_files = 3
       max_file_size = 10
     }
     resources {
       memory = 100
       memory_max = 150
     }
   }
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Теперь перейдем к конфигурации traefik, здесь лишь фрагмент конфига traefik.yml, мы добавляем еще один entrypoint для graylog.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;…………
entryPoints:
 web:
   address: ":80"
   forwardedHeaders:
     insecure: true

 websecure:
   address: ":443"
   http:
     middlewares:
       - sslheader@file
       - trimwww@file

 graylog:
   address: ":11111"
…………
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Также не забываем сконфигурировать порт для работы с graylog в конфиге job nomad для traefik, ниже указан фрагмент traefik job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;job "traefik" {
 datacenters = ["a", "b", "c"]

…………………

 type = "system"

 group "traefik" {
   network {
     mode = "host"

     port "http" {
       static = 80
       to = 80
       host_network = "private"
     }

     port "public" {
               static = 443
               to = 443
       host_network = "public"
     }

     port "ui" {
               to = 8080
       host_network = "private"
     }

     port "graylog" {
       static = 11111
       to = "11111"
       host_network = "private"
     }

   }

   task "traefik" {
     driver = "docker"
     kill_timeout = "30s"
     leader = true

………………

     config {
       image = "traefik:v2.8.7"
       force_pull = true
       ports = ["http", "public", "ui",
        "graylog"
       ]
…………………

}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Так выглядят entrypoints в веб-интерфейсе traefik.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2Fmz-VGdWXCb8e5kk2aw3Qu8fcp3HrmCBv7BH5c2zlDJ2Q-B-xZfRd26WpeoDzCoQ-Kn9ldo7fMgoK6DUGWAvJjOLNhXmFQB5-MV_MMr9W2XtgGYeEclMuMV6i57VLj7wHUM-AjcoFKy0uvpg-zjRhauC1xRanuFZGUa62nnjt8mh0e9Lv_M7FiI_CSQ" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2Fmz-VGdWXCb8e5kk2aw3Qu8fcp3HrmCBv7BH5c2zlDJ2Q-B-xZfRd26WpeoDzCoQ-Kn9ldo7fMgoK6DUGWAvJjOLNhXmFQB5-MV_MMr9W2XtgGYeEclMuMV6i57VLj7wHUM-AjcoFKy0uvpg-zjRhauC1xRanuFZGUa62nnjt8mh0e9Lv_M7FiI_CSQ" width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Конфигурация роутера для graylog осуществляется в tcp router, вот как это выглядит в веб-интерфейсе traefik:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2FYsxr2MAe0rRmJ0oB8A13O0N92gb8XX2kVsDvvRpEXv2PXxo6pWrK3o0TZthTuj9oXI61ZL7cULvAW4jpEGBaykShHkKOWJiO-lcL3D8xJOUEH7BTjLKtebrbUlw5ay41UNAXMVu9MiWNnYEdgM5ALW3yDkcMmPw0WpyXvcoIH51mfHj1ZywVkyPvgA" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2FYsxr2MAe0rRmJ0oB8A13O0N92gb8XX2kVsDvvRpEXv2PXxo6pWrK3o0TZthTuj9oXI61ZL7cULvAW4jpEGBaykShHkKOWJiO-lcL3D8xJOUEH7BTjLKtebrbUlw5ay41UNAXMVu9MiWNnYEdgM5ALW3yDkcMmPw0WpyXvcoIH51mfHj1ZywVkyPvgA" width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Также нужно сконфигурировать сервис (в терминологии traefik), куда мы будем отправлять логи в graylog, именно в конфигурации сервиса в traefik указывается сам ip и порт graylog сервера.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2FOCl-uznt6dQa2ele5-nFX-ONbgIOzshTwTHIGLklhWQRFmJJ6am12UCE2lvcwZ8VSWYkVWq5DsabEMRFFPAwhByOU8GzDvvCapa_8HiQ4k7CIDczWzHwugvFNcbq-A8nskxOIS_bTmBhAtWvVij2GXIK__IOunMQWz7jONWL-mGkrdHvPvbz1i38sg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2FOCl-uznt6dQa2ele5-nFX-ONbgIOzshTwTHIGLklhWQRFmJJ6am12UCE2lvcwZ8VSWYkVWq5DsabEMRFFPAwhByOU8GzDvvCapa_8HiQ4k7CIDczWzHwugvFNcbq-A8nskxOIS_bTmBhAtWvVij2GXIK__IOunMQWz7jONWL-mGkrdHvPvbz1i38sg" width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В веб-интерфейсе graylog вот так конфигурируется input (в терминологии graylog), т.е. входная точка, которая будет принимать логи на стороне graylog. Тут видно, что тип input у нас Beats.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2FRe1exwPm1JsctCoXBboA5KERvxyg-4Z6t1KISMzWUIMIASXtHdF42_Y1sWljwjcRY4gUNaJMPVVQ4NNjNxjEYBoaqz1T4G-0KESu9WK7pErdLXupdtG4bIUX1yotqUAx2nwUEGc5QjAAa2XvN_erECMhvUl0ryFhEymRM-OFJJzba6ikGgPIKCvV3w" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2FRe1exwPm1JsctCoXBboA5KERvxyg-4Z6t1KISMzWUIMIASXtHdF42_Y1sWljwjcRY4gUNaJMPVVQ4NNjNxjEYBoaqz1T4G-0KESu9WK7pErdLXupdtG4bIUX1yotqUAx2nwUEGc5QjAAa2XvN_erECMhvUl0ryFhEymRM-OFJJzba6ikGgPIKCvV3w" width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Вот так выглядит сам веб-интерфейс graylog, где мы уже ищем конкретные логи:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FMvroZBekyjT2buttPXlO_nNx39TWqTxUdPz1hJr6CMhgAowPtyEoRo9TnD2eih88Z3ZwU0Ew8dD1KSXOuM2sIGzrX9QwD6Vw42f3G1Dg3NaY6otdBxQg4MF2gP461Il8dNu9PiNixWtIyB-IX7phrq-c8DOSF7dZ5MCJv2DuCCPSZEBC4R2fI100Ag" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FMvroZBekyjT2buttPXlO_nNx39TWqTxUdPz1hJr6CMhgAowPtyEoRo9TnD2eih88Z3ZwU0Ew8dD1KSXOuM2sIGzrX9QwD6Vw42f3G1Dg3NaY6otdBxQg4MF2gP461Il8dNu9PiNixWtIyB-IX7phrq-c8DOSF7dZ5MCJv2DuCCPSZEBC4R2fI100Ag" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Вот так достаточно просто (конечно после нескольких десятков часов исследований) настраивается централизованный сбор логов. Мы пойдем посмотрим, что у нас там в логах, а вы пока stay tuned.&lt;/p&gt;

</description>
      <category>graylog</category>
      <category>infra</category>
      <category>nomad</category>
      <category>traefik</category>
    </item>
    <item>
      <title>Использование Scientist для рефакторинга критических участков Ruby on Rails приложения</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Fri, 30 Sep 2022 12:19:06 +0000</pubDate>
      <link>https://dev.to/rnds/ispolzovaniie-scientist-dlia-riefaktoringha-kritichieskikh-uchastkov-ruby-on-rails-prilozhieniia-3ckl</link>
      <guid>https://dev.to/rnds/ispolzovaniie-scientist-dlia-riefaktoringha-kritichieskikh-uchastkov-ruby-on-rails-prilozhieniia-3ckl</guid>
      <description>&lt;p&gt;&lt;a href="https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html" rel="noopener noreferrer"&gt;Перевод статьи &lt;em&gt;“Using Scientist to Refactor Critical Ruby on Rails Code”&lt;/em&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://blog.appsignal.com/authors/darren-broemmer" rel="noopener noreferrer"&gt;Darren Broemmer&lt;/a&gt; от 18 мая 2022 года.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe73kwndoczkr94whrvp3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe73kwndoczkr94whrvp3.jpeg" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Попросите любого программиста просмотреть ключевые участки продуктового кода, и он обязательно укажет на несколько моментов, требующих рефакторинга. Почему же так много плохого, ненадежного или непонятного кода продолжает работать в продуктовом окружении?&lt;/p&gt;

&lt;p&gt;Ответ прост: инженеры боятся его трогать. Задачи для рефакторинга обнаруживаются и добавляются в бэклог, но редко попадают в текущий спринт.&lt;/p&gt;

&lt;p&gt;Для этого есть множество причин. Код мог быть написан программистом, давно покинувшим команду и теперь в нём никто не разбирается. Бывает что этот код имеет решающее значение для бизнеса и из-за этого никто не хочет нести ответственность за потенциальный сбой или потерю дохода.&lt;/p&gt;

&lt;p&gt;В данном посте мы рассмотрим, как можно использовать гем &lt;a href="https://github.com/github/scientist" rel="noopener noreferrer"&gt;Scientist&lt;/a&gt; для уверенной миграции, рефакторинга и изменения критического продуктового Ruby-кода.&lt;/p&gt;

&lt;p&gt;Но сначала вы спросите — а нельзя ли использовать тесты для поиска ошибок?&lt;/p&gt;
&lt;h2&gt;
  
  
  Это ведь то самое для чего нужны тесты в Rails, верно?
&lt;/h2&gt;

&lt;p&gt;И да и нет. Часто бывает трудно получить полную уверенность в новых изменениях до развертывания. Допустим модульные (Unit) и системные тесты проходят. Этого достаточно?&lt;/p&gt;

&lt;p&gt;Реальность такова, что ничто не может заменить реальный мир, то есть продуктовую эксплуатацию. А если качество тестовых данных плохое или тесты отсутствуют? А будет ли новое ПО работать достаточно хорошо, чтобы справляться с нагрузкой?&lt;/p&gt;

&lt;p&gt;Команды с публичными сервисами иногда обнаруживают, что им нужно решать проблемы «совместимости с ошибками» (bugwards compatibility). Когда ошибка существует в рабочей среде некоторое время, клиенты могут обходить её таким способом, который зависит от привычного неправильного поведения. Клиенты часто используют ваш софт неожиданным образом.&lt;/p&gt;
&lt;h2&gt;
  
  
  с Scientist можно следить за правками в в Ruby и Rails сразу в боевом окружении
&lt;/h2&gt;

&lt;p&gt;Если продуктовое окружение это лучшее место, где можно обрести уверенность в своих правках, можно подумать о том, чтобы понаблюдать за поведением кода именно там. Поначалу это может показаться пугающим, ведь идея «тестирования в бою» противоречит классическим методам разработки программного обеспечения.&lt;/p&gt;

&lt;p&gt;Однако хорошая новость заключается в том, что это можно легко и безопасно сделать в Ruby и Rails с помощью гема &lt;a href="https://github.com/github/scientist" rel="noopener noreferrer"&gt;Scientist&lt;/a&gt;. Название утилиты основано на научном методе проведения экспериментов для проверки гипотез. В данном случае наша гипотеза состоит в том, что новый код работает, а его использование это “эксперимент”.&lt;/p&gt;

&lt;p&gt;Причина, по которой мы можем безопасно использовать этот подход, заключается в том, что в экспериментах по-прежнему используется результат существующего кода. Новый код оценивается только в целях наблюдения и сравнения, как на точность, так и на производительность. Мы уменьшаем проблемы с тестовым покрытием, которые обсуждались ранее, оценивая производительность с использованием реальных данных и параметров в боевом окружении. Эксперименты обычно проводят с определённой частотой, чтобы свести к минимуму влияние на производительность. Однако при желании можно оценить и каждый вызов.&lt;/p&gt;

&lt;p&gt;Давайте теперь кратко рассмотрим, как Scientist работает с техникой “&lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;Branch by Abstraction&lt;/a&gt;”.&lt;/p&gt;
&lt;h2&gt;
  
  
  Паттерн “Branch by Abstraction” в геме Scientist
&lt;/h2&gt;

&lt;p&gt;Работа Scientist начинается с паттерна &lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;Branch by Abstraction&lt;/a&gt;, описанного Мартином Фаулером как «постепенное крупномасштабное изменение программной системы».&lt;/p&gt;

&lt;p&gt;Мы вводим уровень абстракции, чтобы изолировать обновляемый код. Этот уровень решает, какую реализацию использовать, чтобы эксперимент был прозрачен для остальной части системы. Данный метод связан с использованием флага (feature flag), который определяет какая из ветвей кода будет исполнена.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.blog/2016-02-03-scientist/" rel="noopener noreferrer"&gt;Гем Scientist, созданный в Github&lt;/a&gt;, реализует этот паттерн с помощью “эксперимента”. Существующий код называется “контрольным”, а новая реализация — “кандидатом”. Обе части кода выполняются в случайном порядке, но клиенту возвращается только результат ”контрольной” части.&lt;/p&gt;
&lt;h2&gt;
  
  
  Использование Scientist для рефакторинга сервиса в Ruby
&lt;/h2&gt;

&lt;p&gt;Рассмотрим &lt;a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L20" rel="noopener noreferrer"&gt;Ruby-сервис&lt;/a&gt;, который возвращает наибольший простой множитель для заданного числа. Предположим, мы определили способы оптимизации для сокращения необходимого набора кандидатов, что ускорит работу сервиса.&lt;/p&gt;

&lt;p&gt;Однако владельцы сервиса хотят быть уверены, что при оптимизации не вкралась ошибка. Они также хотят увидеть все улучшения производительности. Давайте напишем &lt;a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L3" rel="noopener noreferrer"&gt;следующий код&lt;/a&gt;, для вызова этого метода:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'scientist'

def largest_prime_factor(number)
  science "prime-factors" do |experiment|
    experiment.use { find_largest_prime_factor(number) } # old way
    experiment.try { improved_largest_prime_factor(number) } # new way
  end # returns the control value
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;В этот момент вызывается только выражение &lt;code&gt;use&lt;/code&gt; (контрольное). Но чтобы сделать эксперимент полезным, надо определить пользовательский класс &lt;code&gt;Experiment&lt;/code&gt;, чтобы включить его и опубликовать результаты (в данном случае просто логирование). Scientist генерирует очень полезные данные, но по умолчанию ничего с ними не делает. Эта часть остается на ваше усмотрение.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'scientist/experiment'
require 'pp'

class MyExperiment
  include Scientist::Experiment

  attr_accessor :name

  def initialize(name)
    @name = name
  end

  def enabled?
    true
  end

  def raised(operation, error)
    p "Operation '#{operation}' failed with error '#{error.inspect}'"
    super # will re-raise
  end

  def publish(result)
    pp result
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результаты эксперимента будут зарегистрированы, и мы сможем со временем вносить улучшения на основе анализа. Как только новый код будет соответствовать требованиям и доверие к нему станет высоким, осуществим переход на новую реализацию, просто заменив код делегированием в новую реализацию.&lt;/p&gt;

&lt;h2&gt;
  
  
  LabTech для упрощения использования Scientist в Ruby on Rails
&lt;/h2&gt;

&lt;p&gt;Есть &lt;a href="https://github.com/RealGeeks/lab_tech" rel="noopener noreferrer"&gt;гем LabTech&lt;/a&gt;, который может помочь настроить Scientist в Rails приложении и удобнее обрабатывать результаты.&lt;/p&gt;

&lt;p&gt;Приложения, использующие &lt;code&gt;AppSignal&lt;/code&gt;, могут использовать &lt;a href="https://docs.appsignal.com/ruby/instrumentation/instrumentation.html" rel="noopener noreferrer"&gt;вспомогательный инструментарий&lt;/a&gt; &lt;code&gt;Appsignal.instrument&lt;/code&gt;, чтобы отслеживать, сколько времени требуется для выполнения событий Scientist. Обернув в него код эксперимента, можно увидеть, как события появляются в &lt;code&gt;AppSIgnal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Теперь вернемся к LabTech — пример ниже просто принимает число для разложения.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3yvnwno4vnwlw8hhwlq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl3yvnwno4vnwlw8hhwlq.png" width="447" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Приступить к работе легко, если у вас есть доступ к консоли. Сначала надо добавить гем LabTech в Gemfile и запустить &lt;code&gt;bundle install&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem 'lab_tech'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результаты и конфигурация эксперимента хранятся в БД, поэтому требуется миграция.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails lab_tech:install:migrations db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Уровень абстракции тот же, за исключением того, что используется модуль LabTech. Полный код доступен на GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def largest_prime_factor(number)
    LabTech.science "prime-factors" do |experiment|
      ...
    end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;На данный момент эксперимент отключен, поэтому воспользуемся консолью, чтобы включить его во всех случаях или настроить процент исполнения.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/rails console
LabTech.enable "prime-factors"
LabTech.enable "prime-factors", percent: 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Теперь можно запустить тесты, и эксперимент будет выполнен. Для текстового представления результатов есть следующие команды в консоли Rails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LabTech.summarize_results "prime-factors"
LabTech.summarize_errors "prime-factors"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;После нескольких успешных прогонов и одной синтетической ошибки получим пример того, как выглядит сводка результатов. Существует обзор успехов и ошибок, а также диаграмма ASCII, показывающая различия в производительности.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;----------------------------------------------------------------------------
Experiment: prime-factors
----------------------------------------------------------------------------
Earliest results: 2022-04-27T02:42:45Z
Latest result: 2022-05-01T17:27:39Z (5 days)

3 of 4 (75.00%) correct
1 of 4 (25.00%) mismatched

Median time delta: +0.000s (90% of observations between +0.000s and +0.000s)

Speedups (by percentiles):
      0% [· █] +2.4x faster
      5% [· █] +2.4x faster
     10% [· █] +2.4x faster
     15% [· █] +2.4x faster
     20% [· █] +2.4x faster
     25% [· █] +2.4x faster
     30% [· █] +2.4x faster
     35% [· █] +2.4x faster
     40% [· █] +2.4x faster
     45% [· █] +2.4x faster
     50% [· · · · · · ·· · · · · · · · · █ ·· · · · · ·] +2.4x faster
     55% [· █] +2.4x faster
     60% [· █] +2.4x faster
     65% [· █] +2.4x faster
     70% [· █] +6.9x faster
     75% [· █] +6.9x faster
     80% [· █] +6.9x faster
     85% [· █] +6.9x faster
     90% [· █] +6.9x faster
     95% [· █] +6.9x faster
    100% [· █] +6.9x faster
----------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Для лёгкого и удобного анализа результатов есть гем &lt;a href="https://github.com/ankane/blazer" rel="noopener noreferrer"&gt;Blazer&lt;/a&gt;. Он прост в установке и позволяет выполнять SQL-запросы. Запрос здесь показывает, что реализация-кандидат значительно быстрее оригинала.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1g1anthh3w6y17w0ryxw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1g1anthh3w6y17w0ryxw.png" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В примере нашего сервиса, возвращающего наибольший простой делитель, ускорение в улучшенной реализации происходит за счет эвристики, исключающей некоторые возможные факторы, которые необходимо учитывать. Поскольку мы рассматриваем большие числа и находим простой множитель, мы можем прекратить поиск после того, как доберемся до целевого числа, делённого на этот множитель. Новый код добавляет только &lt;a href="https://github.com/dbroemme/scientist-labtech-example/blob/master/app/helpers/prime_factor_helper.rb#L38" rel="noopener noreferrer"&gt;одну инструкцию&lt;/a&gt; для достижения этой цели.&lt;/p&gt;

&lt;p&gt;Мы также можем увидеть сокращение времени выполнения используя Blazer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9l8lmyh8zn57iw26ua9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9l8lmyh8zn57iw26ua9.png" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Варианты использования и ограничения Scientist
&lt;/h2&gt;

&lt;p&gt;Оптимальное использование Scientist включает задачи поиска, вычисления и код, который не имеет побочных эффектов. Код, включающий транзакционные обновления или внешние интеграции, такие как электронная почта, не совсем вписывается в модель, поскольку изменяемое действие выполняется дважды (как в старой, так и в новой реализации).&lt;/p&gt;

&lt;p&gt;Это не тривиальное ограничение. Оно блокирует несколько вариантов использования. Однако есть обходные пути, если эксперимент очень важен. Стоит подумать имеют ли значение побочные эффекты или дублирование или они не являются проблемой. Например, в некоторых случаях может не иметь значения, будут ли отправлены два электронных письма во время оценки. Другой вариант — сделать так, чтобы новый код определял результат, но не сохранял его. Это не позволит сравнить производительность, но позволит проверить точность.&lt;/p&gt;

&lt;p&gt;Другие ограничения проистекают из того, что Scientist фокусируется на возвращаемых значениях. В некоторых случаях действительные результаты могут отличаться с течением времени, независимо от того, включают ли они просто временные метки в ответ или изменяются сторонние факторы. Во многих случаях мы можем написать пользовательскую логику сравнения в эксперименте, чтобы проверить точность, выходящую за рамки обычного сравнения строк.&lt;/p&gt;

&lt;p&gt;Наконец, ограничение LabTech заключается в том, что он не был портирован на Rails 7 на момент написания.&lt;/p&gt;

&lt;h2&gt;
  
  
  Лучшие практики для эффективных Scientist-экспериментов в Rails
&lt;/h2&gt;

&lt;p&gt;При проведении экспериментов стоит учитывать следующие моменты:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;В Rails-проектах Scientist может быть настроен либо в инициализаторе, либо в оболочке, такой как гем Rails LabTech. Большинство приложений Rails уже имеют базу данных, поэтому LabTech использует ActiveRecord для хранения результатов.&lt;/li&gt;
&lt;li&gt;Чтобы не замедлять разработку и тестирование, включайте эксперимент только в промежуточной (staging) и продуктовой средах.&lt;/li&gt;
&lt;li&gt;Чтобы свести к минимуму любое потенциальное влияние на продуктовую среду, запускайте эксперимент только для некоторого процента запросов. LabTech поддерживает это из коробки как необязательный параметр при включении эксперимента (изначально он отключен по умолчанию). Используя чистый Scientist, эту логику легко реализовать через &lt;code&gt;enabled?&lt;/code&gt; метод.&lt;/li&gt;
&lt;li&gt;Некоторая логика требует больших ресурсов, поэтому хорошей отправной точкой может быть низкая частота дискретизации. По мере того, как вы обретете уверенность в результатах, увеличивайте процент оцениваемых запросов.&lt;/li&gt;
&lt;li&gt;Вы можете добавить атрибуты контекста, чтобы получить максимальную отдачу от результатов. В качестве контекста эксперимента может быть задан хэш с параметрами, который затем становится доступным в опубликованных результатах, например:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;experiment.context :user =&amp;gt; user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Подводя итоги: наблюдайте и контролируйте свое приложение Ruby с помощью Scientist
&lt;/h2&gt;

&lt;p&gt;В этом посте мы рассмотрели, как использовать гем Scientist для изменения, переноса и рефакторинга кода Ruby в продуктовой среде.&lt;/p&gt;

&lt;p&gt;Мы рассмотрели место Scientist в паттерне Branch by Abstraction, и погрузились в рефакторинг. Затем увидели, как LabTech помогает со сбором результатов и конфигурацией Scientist.&lt;/p&gt;

&lt;p&gt;Мы коснулись некоторых ограничений Scientist, прежде чем, наконец, изложить несколько лучших практик.&lt;/p&gt;

&lt;p&gt;Вам необходимо наблюдать и контролировать то, что происходит в системе. Интегрируйте Scientist в процесс разработки, чтобы с большей уверенностью вносить критические изменения в код Ruby.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.appsignal.com/2022/05/18/using-scientist-to-refactor-critical-ruby-on-rails-code.html" rel="noopener noreferrer"&gt;Перевод статьи &lt;em&gt;“Using Scientist to Refactor Critical Ruby on Rails Code”&lt;/em&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://blog.appsignal.com/authors/darren-broemmer" rel="noopener noreferrer"&gt;Darren Broemmer&lt;/a&gt; от 18 мая 2022 года.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>FTP? Нет, не слышал</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Tue, 20 Sep 2022 06:28:10 +0000</pubDate>
      <link>https://dev.to/rnds/ftp-niet-nie-slyshal-503m</link>
      <guid>https://dev.to/rnds/ftp-niet-nie-slyshal-503m</guid>
      <description>&lt;p&gt;Сейчас будет немного боли и радости от победы над &lt;a href="https://ru.wikipedia.org/wiki/FTP" rel="noopener noreferrer"&gt;FTP&lt;/a&gt;. Мы много и упорно работаем со &lt;a href="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" rel="noopener noreferrer"&gt;СМЭВ3&lt;/a&gt;. Никакой магии: обычный SOAP поверх HTTP, быстро, надёжно - все банки и гос. учреждения знают, как это делается. И вдруг (никогда такого не было, и вот опять) оказалось, что нам надо забирать большие файлы с FTP, предоставляемого СМЭВ.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Дисклеймер: постарайтесь воздерживаться от использования FTP - это технология немного устарела и не отвечает современным требованиям удобства и безопасности.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Тем не менее, надо - значит надо. СМЭВ располагается в защищенной инфраструктуре электронного правительства, куда мы ходим через шлюзы, расположенные в определённой инфраструктуре. В этих шлюзах нет ничего необычного - HTTP, балансировка, отказоустойчивость через избыточность.  &lt;/p&gt;

&lt;h2&gt;
  
  
  В чём проблема?
&lt;/h2&gt;

&lt;p&gt;Шлюзы есть, прокси есть, с чего бы нам бояться &lt;a href="https://ru.wikipedia.org/wiki/FTP" rel="noopener noreferrer"&gt;FTP&lt;/a&gt;? Оказывается, бояться есть с чего - FTP использует не один порт, а множество динамически открываемых, причём есть особенный режим (active), когда сервер инициирует соединение к клиенту - это еще со стародавних времён, когда никакого NAT не было. &lt;/p&gt;

&lt;p&gt;В результате мы не можем работать с этим FTP "по простому" - нам требуется как-то обеспечить сетевую связность и наши любимые и привычные инструменты нам в этом помочь не могут 😠 . Надо думать.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FTP (англ. File Transfer Protocol) — протокол передачи файлов по сети, появившийся в 1971 году задолго до HTTP и даже до TCP/IP, благодаря чему является одним из старейших прикладных протоколов. Изначально FTP работал поверх протокола NCP, на сегодняшний день широко используется для распространения ПО и доступа к удалённым хостам. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Дополнительной особенностью в нашем случае является не просто наличие шлюза, через который необходимо обращаться в СМЭВ, а целых 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gateway 1 - внутри облака&lt;/li&gt;
&lt;li&gt;Gateway 2 - в специальной инфраструктуре с сетевой связностью со СМЭВ &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;вот так выглядит наш каскад проксей:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfy0d3uo0ucyhrns2w7k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfy0d3uo0ucyhrns2w7k.png" alt="proxy cascade" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Решение
&lt;/h2&gt;

&lt;p&gt;Если есть проблема, то должно быть и решение. Так и оказалось - есть несколько (наверное даже много) программных продуктов, связанных с проксированием FTP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://3proxy.ru/" rel="noopener noreferrer"&gt;3proxy&lt;/a&gt; - целый набор проксей. Прочитали доку, запустили - не завелось.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Squid_(software)" rel="noopener noreferrer"&gt;Squid&lt;/a&gt; - большой, надёжный, кеширующий прокси, на все случаи жизни. Для нас оказался слишком избыточен в настройке. И в контексте FTP практически нет документации.&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://tdkare.ru/sysadmin/index.php/Ftp-proxy" rel="noopener noreferrer"&gt;ftp-proxy&lt;/a&gt; (ftpproxy) - стандартный пакет в дистрибутиве Debian/Ubuntu, и он взлетел со свитом.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Поскольку всё наше облако оркестрируется с помощью &lt;a href="https://www.nomadproject.io/" rel="noopener noreferrer"&gt;Nomad&lt;/a&gt;, то и этот компонент мы завернули docker.&lt;/p&gt;

&lt;p&gt;Dockerfile&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; debian:bullseye-slim&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update 
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        ftp-proxy gettext

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/logs/
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; ./ftp-proxy.conf.in /tmp/ftp-proxy.conf.in&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DESTINATION_ADDRESS=127.0.0.1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DESTINATION_PORT=21&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PROXY_PORT=21&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PROXY_LOG_LEVEL=DBG&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 21&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; envsubst &amp;lt; /tmp/ftp-proxy.conf.in &amp;gt; /etc/proxy-suite/ftp-proxy.conf &amp;amp;&amp;amp; exec /usr/sbin/ftp-proxy -n -d &lt;/span&gt;
ftp-proxy.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;В конфиге можно задать миллион других настроек, но в нашем случае достаточно только этого:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[-Global-]

DestinationAddress      ${DESTINATION_ADDRESS}
DestinationPort         ${DESTINATION_PORT}

Port                    ${PROXY_PORT}

LogDestination          | cat
LogLevel                ${PROXY_LOG_LEVEL}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  nomad
&lt;/h2&gt;

&lt;p&gt;Мы катаем джобы с помощью ансибла и используем его шаблонизацию, так что тут я приведу уже финальный результат препроцессинга:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="s2"&gt;"ftp-proxy-prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;datacenters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"zone-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"zone-b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"zone-c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;

  &lt;span class="nx"&gt;constraint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;attribute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${meta.gate}"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;max_parallel&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="nx"&gt;health_check&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"checks"&lt;/span&gt;
    &lt;span class="nx"&gt;min_healthy_time&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10s"&lt;/span&gt;
    &lt;span class="nx"&gt;healthy_deadline&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3m"&lt;/span&gt;
    &lt;span class="nx"&gt;progress_deadline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"6m"&lt;/span&gt;
    &lt;span class="nx"&gt;auto_revert&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;auto_promote&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nx"&gt;canary&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"system"&lt;/span&gt;

  &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="s2"&gt;"ftp-proxy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nx"&gt;network&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="s2"&gt;"ftp"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;static&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;
        &lt;span class="nx"&gt;host_network&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="s2"&gt;"ftp-proxy-prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;driver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt;
      &lt;span class="nx"&gt;kill_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"30s"&lt;/span&gt;
      &lt;span class="nx"&gt;leader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;DESTINATION_ADDRESS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.34"&lt;/span&gt;
        &lt;span class="nx"&gt;DESTINATION_PORT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"8878"&lt;/span&gt;
        &lt;span class="nx"&gt;PROXY_PORT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"21"&lt;/span&gt;
        &lt;span class="nx"&gt;PROXY_LOG_LEVEL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DBG"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;restart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;attempts&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
        &lt;span class="nx"&gt;interval&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5m"&lt;/span&gt;
        &lt;span class="nx"&gt;delay&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"15s"&lt;/span&gt;
        &lt;span class="nx"&gt;mode&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"fail"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YOURIMAGE:latest"&lt;/span&gt;
        &lt;span class="nx"&gt;force_pull&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="nx"&gt;network_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"host"&lt;/span&gt;

        &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"USER"&lt;/span&gt;
          &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PASS"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-ftp"&lt;/span&gt;
        &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"ftp-proxy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ftp"&lt;/span&gt;

        &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
          &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ftp"&lt;/span&gt;
          &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2s"&lt;/span&gt;
          &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5s"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;        

        &lt;span class="nx"&gt;check_restart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
          &lt;span class="nx"&gt;grace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10s"&lt;/span&gt;
          &lt;span class="nx"&gt;ignore_warnings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;        
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;logs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;max_files&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
        &lt;span class="nx"&gt;max_file_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
        &lt;span class="nx"&gt;memory_max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
        &lt;span class="nx"&gt;cpu&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nomad job run &lt;span class="nt"&gt;-detach&lt;/span&gt; ftp-proxy.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Конец
&lt;/h2&gt;

&lt;p&gt;Всем спасибо - всё работает, но если у кого-то есть замечания, связанные с данным решением - мы с огромным интересом их рассмотрим 🤞.&lt;/p&gt;

</description>
      <category>nomad</category>
      <category>ftp</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Rapidity: распределённый rate limiting</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 17 Aug 2022 05:14:09 +0000</pubDate>
      <link>https://dev.to/rnds/rapidity-raspriedielionnyi-rate-limiting-48g4</link>
      <guid>https://dev.to/rnds/rapidity-raspriedielionnyi-rate-limiting-48g4</guid>
      <description>&lt;p&gt;Когда ваш продукт начинает активно использоваться, то перед вами обязательно встаёт вопрос масштабирования, а вслед за ним и проблема ограничения доступа к чему-нибудь: &lt;a href="https://en.wikipedia.org/wiki/Rate_limiting" rel="noopener noreferrer"&gt;Rate Limiting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;RNDSOFT не исключение, поэтому в этой статье мы расскажем небольшую историю и поделимся своим инструментом: &lt;a href="https://blog.rnds.pro/030-rapidity?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=rnds" rel="noopener noreferrer"&gt;https://blog.rnds.pro/030-rapidity&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furatxhkufh5ygl3n72pj.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furatxhkufh5ygl3n72pj.jpeg" alt="rapidity" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>distributedsystems</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Rails 7: три мощных ответа JavaScript’у в 2021+</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 17 Aug 2022 05:01:24 +0000</pubDate>
      <link>https://dev.to/rnds/rails-7-tri-moshchnykh-otvieta-javascriptu-v-2021-50oi</link>
      <guid>https://dev.to/rnds/rails-7-tri-moshchnykh-otvieta-javascriptu-v-2021-50oi</guid>
      <description>&lt;p&gt;&lt;a href="https://blog.rnds.pro/026-three-great-answers?utm_source=devto&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;https://blog.rnds.pro/026-three-great-answers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Перевод статьи "Rails 7 will have three great answers to JavaScript in 2021+"&lt;/em&gt;&lt;br&gt;
&lt;em&gt;David Heinemeier Hansson (DHH) от 6 сентября 2021 года.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fkftwj82fdawp5vouog.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fkftwj82fdawp5vouog.jpeg" alt="rails 7" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>webdev</category>
      <category>r</category>
    </item>
    <item>
      <title>Вы хочете песен? Их есть у меня! (Poison Message #2)</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Wed, 17 Aug 2022 04:54:00 +0000</pubDate>
      <link>https://dev.to/rnds/vy-khochietie-piesien-ikh-iest-u-mienia-poison-message-2-d0m</link>
      <guid>https://dev.to/rnds/vy-khochietie-piesien-ikh-iest-u-mienia-poison-message-2-d0m</guid>
      <description>&lt;p&gt;Самое время рассмотреть “достаточно хороший” алгоритм для борьбы с Poison Message. Здесь будет уже специфика RabbitMQ и к Apache Kafka она не применима, точнее применима только частично - но это уже совсем другая история. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;В &lt;a href="https://blog.rnds.pro/018-posion1?utm_source=devto&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;первой части мы разобрали несколько примеров&lt;/a&gt; и сформулировали проблему Poison Message, здесь же рассмотрим сам алгоритм её решения.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://blog.rnds.pro/019-poison2?utm_source=devto&amp;amp;utm_medium=post" rel="noopener noreferrer"&gt;https://blog.rnds.pro/019-poison2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb1h6qlxw7s56y08os3y.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb1h6qlxw7s56y08os3y.jpeg" alt="Poison Message" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>microservices</category>
      <category>eventdriven</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Чистка build-агентов Gitlab</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Thu, 11 Aug 2022 09:52:28 +0000</pubDate>
      <link>https://dev.to/rnds/chistka-build-aghientov-gitlab-2lil</link>
      <guid>https://dev.to/rnds/chistka-build-aghientov-gitlab-2lil</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqe6lcuqnv4lcxaydvacz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqe6lcuqnv4lcxaydvacz.jpeg" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Оригинал: &lt;a href="https://blog.rnds.pro/030-gitlab-janitor?utm_source=devto&amp;amp;utm_medium=feed_rss&amp;amp;utm_campaign=rnds" rel="noopener noreferrer"&gt;https://blog.rnds.pro/030-gitlab-janitor&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Спешим поделиться с вами нашим инструментом для поддержания чистоты и порядка на наших (RNDSOFT) сборочных серверах &lt;a href="https://github.com/RND-SOFT/gitlab-janitor" rel="noopener noreferrer"&gt;gitlab-janitor&lt;/a&gt;. О том, как мы к нему пришли, и каков первый опыт - далее по тексту.&lt;/p&gt;

&lt;p&gt;С каждым годом инфраструктура RNDSOFT, обеспечивающая процессы CI/CD, растёт. Появляются новые проекты, усложняются процессы (pipelines) сборки и тестирования, растет количество сборщиков (build agents), и всё это приносит дополнительные накладные расходы. Сегодня мы рассмотрим конкретную проблему - чистку/освобождение ресурсов на самих сборочных серверах. Мы используем Gitlab и несколько &lt;a href="https://docs.gitlab.com/runner/" rel="noopener noreferrer"&gt;Gitlab runners&lt;/a&gt; с докером (&lt;a href="https://docs.gitlab.com/runner/executors/docker.html" rel="noopener noreferrer"&gt;docker executor&lt;/a&gt;) под капотом. Вот с проблем и начнём.&lt;/p&gt;

&lt;h2&gt;
  
  
  Проблемы
&lt;/h2&gt;

&lt;p&gt;Вся терминология будет опираться на &lt;a href="https://docs.gitlab.com/" rel="noopener noreferrer"&gt;Gitlab&lt;/a&gt;, но всё это применимо и к другим решениям, опирающимся на docker.&lt;/p&gt;

&lt;p&gt;Типовой процесс сборки и доставки состоит из 4х больших этапов (stages), по несколько задач (jobs) в каждом:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;сборка docker-образа (обычно одного на проект, но бывает и больше).&lt;/li&gt;
&lt;li&gt;тестирование. Тут мы используем подход, когда тестируем не сам код, а весь контейнер. Конечно, есть юнит-тесты, интеграционные, графические и прочие, но запускаем мы их непосредственно внутри боевого контейнера. А что? У нас ruby - можем себе позволить 💎 :)&lt;/li&gt;
&lt;li&gt;тэгирование (image promoting). Этап в зависимости от результатов тестов и QA перетегируется во что-то вроде &lt;code&gt;stable&lt;/code&gt; или &lt;code&gt;release&lt;/code&gt;, или как-то еще, в зависимости от конкретного проекта, команды и принятых процессов.&lt;/li&gt;
&lt;li&gt;доставка (deploy). Тут всё как обычно - дев/тест стенды, QA-стенды, динамические стенды для &lt;a href="https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html" rel="noopener noreferrer"&gt;MR&lt;/a&gt; и всякое разное вроде документации.

&lt;p id="kswC"&gt;Давно хочу написать отдельно про процесс image promotiong, про то, как мы таскаем образы между стадиями, но руки не доходят. Если кого сильно интересует - пишите в комментариях, и мне придётся найти в себе силы :)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В результате описанных выше процессов (особенно тестирования) на сборщиках остаётся много мусора, именно это является проблемой. Далее по порядку.&lt;/p&gt;

&lt;h3&gt;
  
  
  Подвисшие контейнеры (dangling containers)
&lt;/h3&gt;

&lt;p&gt;Тут речь, конечно, идёт не о повисании ПО внутри контейнера, а о самих "ненужных" контейнерах. Например, для графических тестов поднимается целый стек: Postgres, Chrome, Selenium, само приложение, контейнер, из которого запускаются тесты, Redis и бог еще знает что. Если весь процесс (pipeline) или непосредственно задача (job) будут прерваны, то никто не погасит за нас эти контейнеры, и они продолжат висеть и потреблять ресурсы. Проблема!&lt;/p&gt;

&lt;h3&gt;
  
  
  Ненужные образы
&lt;/h3&gt;

&lt;p&gt;В процессе сборки имя образа претерпевает примерно следующие изменения: image:$SHA -&amp;gt; image:stable -&amp;gt; image:release. Конечно, это примерно, шагов может больше или именования другие, но сути это не меняет. При последовательных комитах &lt;del&gt;сратые&lt;/del&gt; старые образы остаются лежать мёртвым грузом.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p id="LCgm"&amp;gt;В день мы генерим примерно 75GB образов. ~25GB на каждом из трех сборщиков. И это летом, когда многие в отпуске :)&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Ситуация усугубляется тем, что один и тот же образ находится (с максимальной вероятностью) сразу на всех сборщиках, поскольку разные типы тестов запускаются параллельно и попадают на все сборщики и занимают x3 места. Проблема! Дважды проблема, поскольку с образами всё не так просто и очень интересно - об этом ниже.&lt;/p&gt;

&lt;h3&gt;
  
  
  Безымянные и кеширующие тома (unnamed and cache volumes)
&lt;/h3&gt;

&lt;p&gt;Многие контейнеры при создании аллоцируют в докере временные анонимные тома/диски (мы же всегда про докер говорим, верно?) и оставляют их после себя. Также сам Gitlab, если вы не используете распределённые кеш (shared cache) &lt;a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscaches3-section" rel="noopener noreferrer"&gt;на базе S3&lt;/a&gt;, всё равно создаёт кеш-тома, &lt;a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdocker-section" rel="noopener noreferrer"&gt;если ему явно не запретить это делать&lt;/a&gt; через &lt;code&gt;disable_cache&lt;/code&gt;. Проблема!&lt;/p&gt;




&lt;p&gt;Конечно, эти проблемы не простой "мусор" - старые образы могут ускорить последующую сборку, кеш-тома также теоретически могут ускорять сборку, так что они не совсем бесполезны. Однако, проблемы ускорения и кеширования мы решаем немного иначе - с помощью определённых схем именования образов, используя buildkit, который умеет подтягивать кешированные слои прямо из реестра образов (в нашем случае harbor) и пр.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что же делать?
&lt;/h2&gt;

&lt;p&gt;Проблемы появляются не сразу. Когда-то у нас был один сборщик, там вообще не было проблем с перетягиванием образов между этапами, а образы чистились через &lt;code&gt;docker rmi&lt;/code&gt; на последнем шаге процесса. Когда сборщиков стало больше, и на них докинули ресурсы, пришлось немного усложнить схему - сначала были bash-скрипты, которые по расписанию удаляли образы по шаблону имени. Затем периодически стали использовать &lt;code&gt;docker system prune&lt;/code&gt;. Но эти решения очень негибкие, плохо поддерживаются и масштабируются и обладают фундаментальной проблемой - частыми кеш-промахами (cache miss), что периодически сильно тормозило процессы (pipelines). И вот однажды терпеть это стало невозможно 😜 :)))&lt;/p&gt;

&lt;p&gt;Нам нужно хорошее решение, желательно с баристой и массажисткой! Встречаем - &lt;a href="https://github.com/RND-SOFT/gitlab-janitor" rel="noopener noreferrer"&gt;gitlab-janitor&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Gitlab-janitor
&lt;/h2&gt;

&lt;p&gt;Основными задачами были:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;консолидация всех чисток сборщика в одном месте;&lt;/li&gt;
&lt;li&gt;уменьшение кеш-промахов, насколько это возможно;&lt;/li&gt;
&lt;li&gt;простота работы.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Для чистки места на сборщиках &lt;a href="https://gitlab.com/gitlab-org/gitlab-runner-docker-cleanup" rel="noopener noreferrer"&gt;уже есть проект gitlab-runner-docker-cleanup&lt;/a&gt;, он и явился идейным вдохновителем нашей утилиты, но, к сожалению, не выполнял всех необходимых нам функций.&lt;/p&gt;

&lt;p&gt;Написав своё решение, мы решили опубликовать его в открытом виде - и &lt;a href="https://github.com/RND-SOFT/gitlab-janitor" rel="noopener noreferrer"&gt;зеркало на github&lt;/a&gt;, и &lt;a href="https://hub.docker.com/r/rnds/gitlab-janitor" rel="noopener noreferrer"&gt;образы на docker-hub&lt;/a&gt;, и даже документация с примерами. Ну и не мог я удержаться от разнообразных бейджиков :)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiryls98hvqqnv4pigr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftiryls98hvqqnv4pigr6.png" width="674" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Проще всего запускать его в докере на каждом сборщике (для этого мы используем &lt;a href="https://www.nomadproject.io/" rel="noopener noreferrer"&gt;nomad&lt;/a&gt; 🥂 ):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /persistent/janitor:/store \
  -e REMOVE=true \
  -e INCLUDE="*integr*, *units*" \
  -e EXCLUDE="*gitlab*" \
  -e CONTAINER_DEADLINE="1h10m" \
  -e VOLUME_DEADLINE="4d" \
  -e IMAGE_DEADLINE="4d" \
  -e CACHE_SIZE="10G" \
  -e IMAGE_STORE="/store/images.txt" \
  rnds/gitlab-janitor:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Заметили &lt;code&gt;IMAGE_STORE&lt;/code&gt;? С остальными параметрами всё просто - шаблоны для имён образов, томов, сроки хранения, а вот для образов всё гораздо интереснее!&lt;/p&gt;

&lt;p&gt;Докер не сохраняет в своём локальном хранилище никакой информации о времени загрузки образа, а только обычную дату создания образа. В результате нет возможности определить, какой образ очень нужен и часто используется (хоть и создан давно), а какой свеженький, но больше никогда и никому не будет нужен.&lt;/p&gt;

&lt;p&gt;Для решения этой проблемы в gitlab-janitor сделан простенький механизм - когда образ встречается первый раз, он сохраняется в файл с временной меткой - это считается датой появления. Когда подходит срок - образ удаляется. Однако, если gitlab-janitor увидит созданный контейнер из этого образа - то временная метка в этом файле сбрасывается. Именно для этого и используется параметр &lt;code&gt;IMAGE_STORE&lt;/code&gt;. Данный файл можно монтировать с хост-машины, а можно и нет - в этом случае история образов будет теряться при пересоздании/обновлении контейнера-чистильщика:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selenium/standalone-chrome:latest sha256:c01aea5eb0bf279df5f745e3c277e30d7d9c81f15b9d1d4e829f1075c31ed5b1 1660205645
postgres:10-alpine sha256:d7023df56cb7cbe961f0c402888f7170397c98c099fb76fbe16b5442a236ad51 1660205040
redis:latest sha256:3edbb69f9a493835e66a0f0138bed01075d8f4c2697baedd29111d667e1992b4 1660205645
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;С остальными параметрами, документацией и примерами можно ознакомиться на гитхабе.&lt;/p&gt;

&lt;h2&gt;
  
  
  Результаты
&lt;/h2&gt;

&lt;p&gt;Вот и подъехали результаты объективного контроля за работой gitlab-janitor на трех наших сборщиках:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frj06s6qygsa5amr3c0i5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frj06s6qygsa5amr3c0i5.png" width="800" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Все чистки с помощью &lt;code&gt;cron+bash&lt;/code&gt;, &lt;code&gt;docker prune&lt;/code&gt; и пр. выключены. Параметры, оптимально подходящие под наши процессы, мы еще подбираем (и будем подбирать), но уже видно, что процесс вошел в стабильную фазу, и наш чистильщик выполняет свою работу!&lt;/p&gt;

</description>
      <category>infra</category>
      <category>gitlab</category>
      <category>cleaner</category>
      <category>devops</category>
    </item>
    <item>
      <title>Dev containers: вскрытие</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Mon, 04 Jul 2022 08:31:03 +0000</pubDate>
      <link>https://dev.to/rnds/dev-containers-vskrytiie-20o3</link>
      <guid>https://dev.to/rnds/dev-containers-vskrytiie-20o3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frn4enyt0s67zrlt1vg8z.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frn4enyt0s67zrlt1vg8z.jpeg" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Оригинал: &lt;a href="https://blog.rnds.pro/029-dev-containers-uncovering?utm_source=devto&amp;amp;utm_medium=feed_rss&amp;amp;utm_campaign=rnds" rel="noopener noreferrer"&gt;https://blog.rnds.pro/029-dev-containers-uncovering&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;В этой статье хотелось бы рассказать про разработку внутри docker-контейнера: зачем это нужно, что предоставляет для этого замечательная IDE VSCode, и как это работает.&lt;/p&gt;

&lt;p&gt;Итак, docker - это не только отличная технология для контейнеризации и деплоя приложений, но и возможность развернуть специфическую среду разработки в одно касание. &lt;/p&gt;

&lt;p&gt;Для тех, кто ещё (внезапно!) не знаком с Docker, очень рекомендую сразу пойти и ознакомиться с этой технологией (&lt;a href="https://en.wikipedia.org/wiki/Docker_(software)" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Docker_(software)&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Для чего же это нужно?
&lt;/h2&gt;

&lt;p&gt;Давайте рассмотрим существующие проблемы, которые, как правило, возникают при работе над проектом.&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема 1
&lt;/h3&gt;

&lt;p&gt;Вы - &lt;strong&gt;новый разработчик в компании&lt;/strong&gt; , Вам дали свежеустановленный ПК, дали доступ в репозиторий проекта, с которым придется иметь дело. Предположим, что этот проект на Rails. Для запуска проекта необходимо:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;установить системные dev пакеты&lt;/li&gt;
&lt;li&gt;установить rvm&lt;/li&gt;
&lt;li&gt;установить правильную версию ruby, bundle&lt;/li&gt;
&lt;li&gt;установить nvm&lt;/li&gt;
&lt;li&gt;установить node&lt;/li&gt;
&lt;li&gt;установить и настроить БД, например, Postgres&lt;/li&gt;
&lt;li&gt;возможно, что-то ещё, что требует проект (Redis, Consul и т.д.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В процессе установки могут возникнуть следующие проблемы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;у вас Windows: это сразу +100 к сложности и боли&lt;/li&gt;
&lt;li&gt;у вас слишком новая версия операционки, а в проекте используются устаревшие библиотеки, и при установке ruby пакетов, либо scss (когда же его наконец перепишут на JS) могут возникнуть ошибки компиляции&lt;/li&gt;
&lt;li&gt;возможно, необходимы какие-то ещё дополнительные шаги, которые забыли описать в README, например, это может быть установка корневых сертификатов, прописывание каких-то переменных среды&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В целом видно, что процесс первого запуска проекта непростой. Скорее всего, его придется повторить, когда захочется поработать с домашнего компьютера, а там как раз больше вероятность встретить Windows =)&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема 2
&lt;/h3&gt;

&lt;p&gt;Вы - опытный разработчик и уже давно работаете в компании, &lt;strong&gt;у вас много проектов&lt;/strong&gt; , на разных языках, фреймворках и приходится часто переключаться. Согласитесь, не очень хочется проводить существенное время на развертывание окружения, вспоминая, как это делать.&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема 3
&lt;/h3&gt;

&lt;p&gt;Бывает так, что &lt;strong&gt;проект уже очень старый&lt;/strong&gt; , порос мхом и обновлять его нерентабельно, но поддерживать надо. Там всё очень старое, и на своей современной операционке это просто не запустить.&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема 4
&lt;/h3&gt;

&lt;p&gt;Ещё одна проблема - это &lt;strong&gt;замусоренность хостовой системы&lt;/strong&gt; , когда мы долго уже работаем, много всего разного поставили себе в систему, у нас появляются странные глюки, а остальные разработчики говорят нам, что &lt;a href="https://wikireality.ru/wiki/%D0%A3%D0%9C%D0%92%D0%A0" rel="noopener noreferrer"&gt;УМВР&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Решение
&lt;/h3&gt;

&lt;p&gt;Итак, эти все проблемы решает &lt;strong&gt;контейнеризированное&lt;/strong&gt; окружение разработки. Мы получаем:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;повторяемость среды разработки&lt;/li&gt;
&lt;li&gt;быстрое развертывание&lt;/li&gt;
&lt;li&gt;независимость от хостовой системы, её типа и версии&lt;/li&gt;
&lt;li&gt;возможность полностью очистить свою систему от ненужных рудиментов старых проектов&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Конечно же есть и недостатки:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Повышается сложность понимания что происходит&lt;/strong&gt;. Надо не забывать, что мы находимся внутри контейнера, и некоторые внешние ресурсы могут быть недоступны, такие как файлы, либо сторонние сервисы, запущенные на localhost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Требуется больше системных ресурсов&lt;/strong&gt;. В основном, это дополнительное пространство на диске под контейнеры и образы docker, а в случае с MacOS ещё и дополнительный расход оперативной памяти под виртуальную машину с докером.&lt;/li&gt;
&lt;li&gt;Необходимо учитывать, что &lt;strong&gt;ID пользователя внутри контейнера может быть другим&lt;/strong&gt; , и может возникнуть проблема с доступом к файлам проекта. Забегая вперед, скажу, что эта проблема вполне решаемая.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Существует множество вариантов реализовать такое контейнеризированное окружение:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;написать Dockerfile с нужной версией ОС, установкой всего необходимого, зайти в контейнер и разрабатывать в VIM.&lt;/li&gt;
&lt;li&gt;можно смонтировать исходники в запущенный контейнер и разрабатывать в своей IDE, а запускать в контейнере. Правда, у IDE скорее всего будут проблемы с дополнением кода.&lt;/li&gt;
&lt;li&gt;можно установить IDE внутрь контейнера и пробросить X-ы с хостовой машины (работает только с Linux).&lt;/li&gt;
&lt;li&gt;можно поставить sshd в контейнер и заходить внутрь по ssh из VSCode, IDEA или любой другой IDE, которая поддерживает удалённую разработку.&lt;/li&gt;
&lt;li&gt;либо воспользоваться специализированным решением, которое нам предлагает VSCode. Предлагаю в этой статье подробно рассмотреть это решение как самое, на мой взгляд, продвинутое из того, что я видел.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  VS Code Dev container
&lt;/h2&gt;

&lt;p&gt;Полная документация находится здесь: &lt;a href="https://code.visualstudio.com/docs/remote/containers" rel="noopener noreferrer"&gt;https://code.visualstudio.com/docs/remote/containers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Вкратце, это работает следующим образом:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;в проект добавляется конфигурация в папке .devcontainer&lt;/li&gt;
&lt;li&gt;при открытии такого проекта VS Code предлагает переоткрыть его внутри контейнера&lt;/li&gt;
&lt;li&gt;если мы соглашаемся, то после непродолжительных магических действий со стороны VS Code проект открывается&lt;/li&gt;
&lt;li&gt;мы оказываемся внутри контейнера и получаем ровно то окружение, которое нам необходимо для разработки&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Вот шаги, которые нужно выполнить для добавления к себе в проект конфигурации:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker уже должен быть установлен&lt;/li&gt;
&lt;li&gt;устанавливаем плагин в VS Code &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;добавляем поддержку dev container в наш проект&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Для этого открываем палитру команд в VS Code, выбираем пункт “Remote-Containers: Add Development Configuration Files”.&lt;/p&gt;

&lt;p&gt;Нам предоставляется выбор:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;воспользоваться готовыми конфигурациями (Node.js, Ruby on Rails, C++, Go, PHP и ещё много других)&lt;/li&gt;
&lt;li&gt;добавить существующий в проекте Dockerfile или docker-compose.yml&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Если сильно спешим, то можно воспользоваться готовыми конфигурациями. Но, на мой взгляд, лучше написать свою конфигурацию с нуля, так мы получаем больше контроля над происходящим, и это же так интересно!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Разбираемся в магии&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Отдельно хотелось бы рассмотреть, что же делает VS Code с нашим контейнером, прежде чем его открыть.&lt;/p&gt;

&lt;p&gt;Существует ряд нюансов, на которые следует обратить внимание:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;при монтировании исходников в контейнер ID пользователя и ID группы внутри и снаружи должны совпадать, иначе мы получим в своих исходниках новые файлы, созданные от имени другого пользователя (возможно от рута)&lt;/li&gt;
&lt;li&gt;у Git есть глобальные конфигурационные файлы, в которых задается ряд параметров, таких как имя пользователя, email, тип переноса строк и т.д., желательно, чтобы git внутри контейнера использовал такие же настройки&lt;/li&gt;
&lt;li&gt;также необходимо скопировать некоторые пользовательские настройки для того, чтобы вы чувствовали себя как дома (.ssh/authorized_hosts, gpg конфигурацию)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Именно это делает VS Code при открытии devcontainer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;он изучает версию операционной системы, которая используется в Dockerfile&lt;/li&gt;
&lt;li&gt;оборачивает ваш Dockerfile своим с дополнительными командами по пробросу UID и GID внутрь контейнера&lt;/li&gt;
&lt;li&gt;патчит /etc/passwd, синхронизируя UID и GID внутреннего пользователя с хостовым пользователем&lt;/li&gt;
&lt;li&gt;копирует в него все необходимые конфигурационные файлы из папки пользователя (git, ssh, gpg)&lt;/li&gt;
&lt;li&gt;устанавливает свой бэкенд на ноде (это порядка 450Мб) внутрь контейнера, который будет связываться с хостовым VSCode по рандомному TCP порту&lt;/li&gt;
&lt;li&gt;устанавливает внутрь контейнера все необходимые плагины для VS Code, прописанные в .devcontainer/devcontainer.json#extensions&lt;/li&gt;
&lt;li&gt;монтирует исходники проекта в папку /workspaces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Вуаля! Проект открыт, и можно кодить как обычно.&lt;/p&gt;

&lt;p&gt;В итоге, чтобы открыть любой проект с поддержкой devcontainer и начать быстро кодить, Вам нужна лишь машина на Linux, Windows, MacOS с установленным Docker и VS Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Уже не терпится? Попробуем в деле!
&lt;/h2&gt;

&lt;p&gt;Я заготовил пример проекта, который можно открыть в VSCode и посмотреть, как это работает.&lt;/p&gt;

&lt;p&gt;Для этого я форкнул первый найденный проект “Блог” на Rails (&lt;a href="https://github.com/navrocky/rails-blog-sample-devcontainers" rel="noopener noreferrer"&gt;https://github.com/navrocky/rails-blog-sample-devcontainers&lt;/a&gt; ).&lt;/p&gt;

&lt;p&gt;Как нельзя кстати оказалось, что он уже немного “подзасох”, последний коммит 5 лет назад. Соответственно, в те годы люди сидели на Ruby 2.3, Debian 8 (Jessie). Вот и попробуем всё это завести в нашем 2К22.&lt;/p&gt;

&lt;p&gt;Dev container поддерживает два варианта окружения:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;один &lt;strong&gt;Dockerfile&lt;/strong&gt;. Этот вариант проще и подходит, когда нам не нужны дополнительные сервисы для работы, такие как БД.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;docker-compose.yml&lt;/strong&gt;. Этот вариант позволяет нам запустить как само окружение, так и все необходимые дополнительные сервисы. В примере я использовал именно этот вариант, так как нам нужна ещё сконфигурированная база данных.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Так выглядит типичная конфигурация в проекте:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1v8fbtygkmgjujdz953.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1v8fbtygkmgjujdz953.png" width="484" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Всё находится внутри папки &lt;strong&gt;.devcontainer&lt;/strong&gt;. Файл &lt;strong&gt;devcontainer.json&lt;/strong&gt; является основным и описывает конфигурацию для плагина VSCode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;devcontainer.json&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    // Имя конфигурации
    "name": "Ruby",

    // указание файла docker-compose.yml и сервиса в нем, 
    // который будет использоваться для разработки
    "dockerComposeFile": "./docker-compose.yml",
    "service": "dev",

    // папка внутри контейнера, в которую монтируются исходники проекта
    "workspaceFolder": "/workspace",

    // действие при завершении работы с проектом
    "shutdownAction": "stopContainer",

    // дополнительные расширения VSCode, которые необходимо установить 
    // внутрь контейнера перед началом работы
    "extensions": [
        "castwide.solargraph", "eamodio.gitlens"
    ],

    // пользователь внутри контейнера, под которым будет происходить вход 
    // в контейнер
    "remoteUser": "user"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# берем нужную версию базового дистрибутива. Debian 8 из 2015 года, нам подходит.
FROM debian:8

# устанавливаем все необходимые утилиты
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y gnupg2 ca-certificates curl procps sudo git mc libpq-dev

# добавляем пользователя, под которым будем работать внутри контейнера, заодно даем ему возможность делать sudo
RUN useradd -m -d /home/user -s /bin/bash user &amp;amp;&amp;amp; adduser user sudo &amp;amp;&amp;amp; \
    echo '%sudo ALL=(ALL) NOPASSWD:ALL' &amp;gt;&amp;gt; /etc/sudoers

# дальше все действия производим уже от пользователя
USER user 

# устанавливаем rvm
RUN gpg2 --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB &amp;amp;&amp;amp; \
    curl -ksSL https://get.rvm.io | bash -s stable

# устанавливаем nvm
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

# подгружаем все инициализационные скрипты и переменные среды в bash, чтобы заработал установленный rvm
SHELL ["/bin/bash", "-c", "-l"]

# устанавливаем нужную версию Ruby и Bundler
RUN rvm install ruby-2.4 &amp;amp;&amp;amp; gem install bundler:1.17.3

# создаем пользовательский каталог .bundle, чтобы были правильные права на эту папку при монтировании volume
RUN mkdir -p /home/user/.bundle

# устанавливаем node v5.3.0 и делаем её по умолчанию
RUN source $HOME/.nvm/nvm.sh &amp;amp;&amp;amp; nvm install v5.3.0 &amp;amp;&amp;amp; nvm alias default v5.3.0

# просто висим в бесконечном ожидании из /dev/null, это нужно чтобы контейнер не закрылся после запуска
ENTRYPOINT ["tail", "-f", "/dev/null"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Открываем проект, VSCode предлагает нам открыть его внутри контейнера - соглашаемся:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstdhyigw3w0kev1bwj4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstdhyigw3w0kev1bwj4a.png" width="555" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ждем некоторое время, пока все образы будут загружены, и будет произведена сборка контейнера:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9ehyzn8ipnt29anx430.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9ehyzn8ipnt29anx430.png" width="481" height="83"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Пока ждем, можно нажать show log и посмотреть, что там происходит.&lt;/p&gt;

&lt;p&gt;Всё - проект открыт, и можно приступать к работе:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nvh6wq57bm6orn29uxz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nvh6wq57bm6orn29uxz.png" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Выполняем в терминале VSCode следующие команды:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rake db:setup
rails s -b 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Открываем в браузере наш блог (&lt;a href="http://localhost:3000/blog" rel="noopener noreferrer"&gt;http://localhost:3000/blog&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkb1ho5k2w3ocznpon1bd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkb1ho5k2w3ocznpon1bd.png" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Можно зайти в административную панель (&lt;a href="http://localhost:3000/admin" rel="noopener noreferrer"&gt;http://localhost:3000/admin&lt;/a&gt;) и добавить контент. Пользователь и пароль для входа: &lt;strong&gt;&lt;a href="mailto:admin@example.com"&gt;admin@example.com&lt;/a&gt;&lt;/strong&gt; / &lt;strong&gt;123456&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Вот так просто и быстро можно открыть незнакомый проект и приступить к работе!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  А что же IDEA и её производные, спросите вы?
&lt;/h2&gt;

&lt;p&gt;Там не всё так радужно, но есть свет в конце туннеля.&lt;/p&gt;

&lt;p&gt;Не так давно JetBrains добавила поддержку remote development в свои платные IDE. Community варианты её, к сожалению, не получили. И называется этот продукт JetBrains Gateway (&lt;a href="https://www.jetbrains.com/remote-development/gateway/" rel="noopener noreferrer"&gt;https://www.jetbrains.com/remote-development/gateway/&lt;/a&gt;). Данная штука позволяет зайти на удалённую машину, в нашем случае в контейнер с поднятым ssh демоном, и загрузить туда выбранную вами IDE и запустить её в режиме сервера, а на вашем хосте будет запущен IDE Client, который будет по ssh туннелю общаться с серверной IDE. В целом всё выглядит примерно так же, как и в VS Code, но менее автоматизированно. При открытии проекта необходимо руками поднимать контейнер или docker-compose конфигурацию, настраивать соединение внутри контейнера. Серверная IDE, которая грузится в контейнер, ничем не отличается от десктопной, и в размерах тоже (это примерно 1,5 ГБ), что весьма печально. Интерфейс IDE клиента ощущается тормознее по сравнению с десктопной IDE и местами подглючивает. Но в целом, всё довольно работоспособно.&lt;/p&gt;

&lt;p&gt;Стоит заметить, что эта система пока находится в бете, поэтому держим кулачки и внимательно наблюдаем за дальнейшим развитием событий.&lt;/p&gt;

</description>
      <category>development</category>
      <category>containers</category>
      <category>docker</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Хозяюшке на заметку: тэгируем логи и ошибки</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Mon, 06 Jun 2022 11:40:27 +0000</pubDate>
      <link>https://dev.to/rnds/khoziaiushkie-na-zamietku-teghiruiem-loghi-i-oshibki-4hc8</link>
      <guid>https://dev.to/rnds/khoziaiushkie-na-zamietku-teghiruiem-loghi-i-oshibki-4hc8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp99f3dzbcpat6vj5i55g.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp99f3dzbcpat6vj5i55g.jpeg" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Оригинал: &lt;a href="https://blog.rnds.pro/028-logs-and-errors-tagging?utm_source=devto&amp;amp;utm_medium=feed_rss&amp;amp;utm_campaign=rnds" rel="noopener noreferrer"&gt;https://blog.rnds.pro/028-logs-and-errors-tagging&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;В этой статье будет рассказано, что, возможно, не так с вашими диагностическими сообщениями, как их можно тэгировать, а главное - зачем это делать.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Диагностические сообщения - это текстовые сообщения об ошибках и логи вашей программы. Так что же с ними не так и как это можно улучшить? Но если ты нетерпелив, то можешь поверить на слово, что проблемы есть, пропустить всю воду и перейти сразу к сути статьи и выводам. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Что не так?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Сперва опишу некоторую проблематику, которую я начал осознавать со временем и с опытом.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Проблема №1&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Рассмотрим обработку ошибок. Существует два вида обработки ошибок: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;на исключениях&lt;/li&gt;
&lt;li&gt;с кодами возврата&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В некотором роде идеологически эти два варианта одинаковы, при возникновении ошибки мы имеем код ошибки (класс исключения) и текстовое сообщение, описывающее ошибку.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;В случае с исключениями, мы ещё имеем стек возникновения исключения, цепочку зависимых исключений, какие-то дополнительные значения, прикрепленные к исключению, но это не имеет отношения к дальнейшему повествованию. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Практически во всех языках в стандартной библиотеке уже имеется ряд предопределённых кодов ошибок (классов исключений), такие как ArgumentError, SecurityError, IOError, RuntimeError и т.д. И предполагается, что разработчик для всех без исключения новых ошибок в своей системе будет объявлять новые классы исключений и коды ошибок. Но, как правило, писать столько кода очень затратно по времени и все используют узкий набор стандартных исключений или кодов + небольшое количество собственных . А что же произошло конкретно, описывается в сообщении об ошибке. &lt;/p&gt;

&lt;p&gt;Что же здесь плохого? Плохо то, что с точки зрения кода мы можем анализировать только код ошибки, но не человекочитаемое сообщение об ошибке. Поэтому в данном случае остается только расширять список кодов ошибок (исключений) и это правильно. Но в любом случае в коде большого приложения остается очень много редко используемых веток исполнения, на которые не может быть внятной реакции извне. Пользователь тоже с этим ничего поделать не может, и в таких случаях обычно кидается RuntimeError и пользователю отображается “Что-то пошло не так”. Либо это бывает ошибка ArgumentError, которая может возникнуть практически в любой функции при валидации параметров.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Проблема №2&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ненадолго переключимся на логирование, здесь тоже есть проблема. Обычно в нагруженной системе мы получаем тонны логов, и иногда их как-то нужно анализировать, даже при помощи различных фильтров и инструментов. К примеру, нужно поискать некое сообщение в логах, как часто оно возникает. Но сообщения в логах обычно это человекочитаемые строки, которые часто подвергаются форматированию и локализации. И искать в логах точное соответствие строке порой бывает очень сложно, не спасают даже регулярки. Например: “Client #{client.id} not found in #{list}”. Если мы будем искать это сообщение по частям, то мы найдем множество записей с “Client” или “not found” и дальше уже глазками грепать пересечение. Также это отсутствие формализации сильно усложняет последующие автоматизированные анализы логов на наличие определенных ошибок, вывод статистики.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Проблема №3&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Мы увидели какую-то ошибку или сообщение в логах, но как теперь найти то место в коде, которое её/его вызывает? Хорошо, если у исключения залогировался стектрейс со строками кода… Иначе мы беремся за инструмент полнотекстового поиска в нашей любимой IDE и начинаем искать сперва по нескольким словам, потом регуляркой, если сообщение было отформатировано. Через некоторое время конечно же находим нужное нам место. Но это было не очень просто, не так ли? А если строка была локализована… Ну вы поняли.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Проблема №4&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Это поддержка. Пользователи начинают нам звонить и пытаться на ломаном языке сообщить, что же им выскочило на экране, либо делают скрины с выхлопом ошибки, а там что-то типа “Передан неправильный параметр”. Какой? Где? Стектрейса нет. Тут начинаются гадания на кофейной гуще и дальнейшие переписки, созвоны.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Что делать? Тэгировать!&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Как-то очень давно я обратил внимание на синие экраны смерти Windows, также при работе с WinAPI я наткнулся на каталог кодов ошибок в Windows. Ребята явно заморочились на этот счет, сейчас он переваливает за 12000 записей. Там можно найти отдельный код на любой чих.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26tgc0sai4lp90d6r1aj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26tgc0sai4lp90d6r1aj.png" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Но проблема в том, что нужно прямо брать и вести такой реестр порядковых кодов для своего проекта, чтобы случайно не пересечься с каким-то другим кодом. А если у тебя микросервисы, то желательно, чтобы между ними тоже не было пересечений. Поэтому я для себя быстро накатал незамысловатый скрипт на sh:&lt;/p&gt;

&lt;p&gt;genuid.sh&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh
SHA256=$(uuidgen | sha256sum); echo -n "&amp;lt;${SHA256:0:8}&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Расшифровываю: берем обычный &lt;strong&gt;GUID&lt;/strong&gt; , считаем от него &lt;strong&gt;SHA256&lt;/strong&gt; , берем первые 8 символов, обрамляем в угловые скобки “&amp;lt;”, “&amp;gt;” для пущей красоты.&lt;/p&gt;

&lt;p&gt;Потом повесил вызов этого скрипта с эмуляцией вбития его с клавиатуры на хоткей, спасибо божественному KDE, что там всё для этого есть штатно (ну практически). И начал быстро растыкивать такие коды в тексты сообщений об ошибках и в логи.&lt;/p&gt;

&lt;p&gt;Вот так примерно выглядят сообщения об ошибках:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raise SecurityError.new "&amp;lt;c920fbaf&amp;gt; Hello #{username}, " +
    "you’re not supposed to be here"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;либо:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logger.warn "&amp;lt;dc8a357a&amp;gt; #{username} entered restricted area"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Выводы
&lt;/h2&gt;

&lt;p&gt;Что же я получил с таким тэгированием:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;описанный метод универсален, и выгоду можно получить на любой платформе, будь то бэк, либо фронт, написанные на чём угодно&lt;/li&gt;
&lt;li&gt;мне не надо вести реестр кодов, я практически уверен, что очередной код будет уникален, и он не такой громоздкий как &lt;strong&gt;GUID&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;видя код ошибки в выхлопе на экране, я могу быстро найти её в исходниках простейшим грепом без тайных знаний regexp&lt;/li&gt;
&lt;li&gt;благодаря строгому формату тэга, я могу незамысловатым скриптом собрать все ошибки в проекте с описанием в текстовый файл (он же реестр кодов ошибок) и отдать его, например, фронтендеру&lt;/li&gt;
&lt;li&gt;пользователи мне слали коды ошибок, и могли это делать хоть через SMS&lt;/li&gt;
&lt;li&gt;стало возможным вычленить из текста тэг и обработать программно специфичную ошибку особым образом&lt;/li&gt;
&lt;li&gt;в системе сбора и анализа логов я настраивал фильтры на особые ошибки, по которым необходимо было экстренно реагировать, получал сообщения в телегу&lt;/li&gt;
&lt;li&gt;в отличие от стектрейса с файлом и номером строки &lt;strong&gt;UID&lt;/strong&gt; не изменится. Это означает, что даже если вы сделаете грандиозный рефакторинг, вы всегда найдете ошибку или сообщение по коду, сколько бы лет не прошло, и если она там ещё осталась.&lt;/li&gt;
&lt;li&gt;на сервере при формировании response я выделял &lt;strong&gt;UID&lt;/strong&gt; ошибки в отдельное поле, чтобы клиенту было удобнее. Получалось примерно так:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{  
  "result": "ACCESS_DENIED",  
  "uid": "c920fbaf",  
  "message": "Hello John, you’re not supposed to be here"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Конечно же, есть и некоторые неудобства, недостатки данного тэгирования:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;когда занимаешь копипастой, то частенько коды начинают задваиваться, тут надо быть внимательным и генерить для только что вставленного кода новые тэги&lt;/li&gt;
&lt;li&gt;коды могут просочиться в итоге на экран пользователю в сообщении об ошибке и на фронте надо озаботиться отделением текста от тэга, а полный текст ошибки убрать в секцию “Дополнительно”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Спасибо за внимание, надеюсь, что данный несложный приём ещё кому-нибудь хорошо зайдет. &lt;/p&gt;

&lt;h2&gt;
  
  
  В качестве дополнения
&lt;/h2&gt;

&lt;p&gt;Под Linux я использую &lt;strong&gt;xvkbd&lt;/strong&gt; для эмуляции ввода с клавиатуры.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh
GUID=`genuid.py`
xvkbd -text $GUID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Что существует для эмуляции ввода с клавиатуры и автоматизации под Windows и Mac - мне неведомо, хотелось бы получить отклик в комментариях, и я дополню статью =)&lt;/p&gt;

</description>
      <category>development</category>
      <category>logs</category>
    </item>
    <item>
      <title>Структура тестов Go, для RSpec-нутых</title>
      <dc:creator>Samoilenko Yuri</dc:creator>
      <pubDate>Tue, 03 May 2022 14:43:48 +0000</pubDate>
      <link>https://dev.to/rnds/struktura-tiestov-go-dlia-rspec-nutykh-34l3</link>
      <guid>https://dev.to/rnds/struktura-tiestov-go-dlia-rspec-nutykh-34l3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwf5yxkh7xosckqrfzql.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwf5yxkh7xosckqrfzql.jpeg" width="695" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Оригинал: &lt;a href="https://blog.rnds.pro/027-go-structure-of-tests/?utm_source=teletype&amp;amp;utm_medium=feed_rss&amp;amp;utm_campaign=rnds" rel="noopener noreferrer"&gt;https://blog.rnds.pro/027-go-structure-of-tests/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Для кого статья?
&lt;/h2&gt;

&lt;p&gt;Для Ruby-разработчиков, которые знают что такое &lt;a href="https://blog.rnds.pro/023-rspec-tests-structure" rel="noopener noreferrer"&gt;хорошие тесты на RSpec&lt;/a&gt; (остальных в Go ничего не смутит) и хотят писать тесты на Go соответствующим образом.&lt;/p&gt;

&lt;p&gt;Для Go-разработчиков. На Go можно написать хорошие тесты, но почему-то я таких пока не видел 😇. Почему-то даже в крупных и популярных пакетах используют не лучшие подходы.&lt;/p&gt;

&lt;p&gt;Документация по тестированию на Go не очень богата и оставляет много вопросов (пока не получил опыта работы с RSpec, они у меня не возникали), ну что же, попытаемся на них ответить.&lt;/p&gt;

&lt;h2&gt;
  
  
  Немного предыстории
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rnds.pro/" rel="noopener noreferrer"&gt;RNDSOFT&lt;/a&gt; как-то организовывала &lt;a href="https://www.youtube.com/watch?v=nDFjZ4vFr88" rel="noopener noreferrer"&gt;Ruby meetup&lt;/a&gt;, где я выступал с докладом &lt;strong&gt;"Тесты Go глазами Ruby-разработчика"&lt;/strong&gt; и проводил параллели между тестами в Go и RSpec, рассказывая о трудностях, с которыми сталкиваешься, начиная писать тесты в Go, и как преодолеть их с наименьшей болью.&lt;/p&gt;

&lt;p&gt;И вот сейчас у меня дошли руки и я хочу изложить мои мысли текстом, сконцентрировав внимание именно на структурировании тестов.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что будет?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Посмотрим, что говорит нам &lt;a href="https://pkg.go.dev/testing" rel="noopener noreferrer"&gt;документация&lt;/a&gt; по поводу того, как нужно писать тесты.&lt;/li&gt;
&lt;li&gt;Обратим внимание на то, что документация не даёт нам всех ответов по структурированию тестов.&lt;/li&gt;
&lt;li&gt;Поймём, что для структурирования тестов в реальном проекте нам понадобится некое соглашение.&lt;/li&gt;
&lt;li&gt;Чтобы не изобретать велосипед, определим, какое соглашение можно взять за эталон и работать с ним.&lt;/li&gt;
&lt;li&gt;Разберём функционал, предоставляемый Go, с точки зрения RSpec (только стандартный пакет тестирования. Почему так, поговорим ниже).&lt;/li&gt;
&lt;li&gt;Поймём, как в Go решить те же задачи, которые позволяет решить RSpec, но не выходя за парадигму Go.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Чего не будет?
&lt;/h2&gt;

&lt;p&gt;Как можно скорее хочется спасти мебель от возгорания по вине тех, кто уже начал кричать что-то в духе:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Как вообще можно сравнивать RSpec и Go".&lt;/li&gt;
&lt;li&gt;"Привыкли писать на своём RSpec, так теперь везде вам подавай всё как там? Учитесь писать в парадигме Go".&lt;/li&gt;
&lt;li&gt;"Go строго типизированный, это вам не Ruby, в нём всё по-другому".&lt;/li&gt;
&lt;li&gt;"Go строго типизированный, в нём не нужны такие тесты, как на RSpec".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Выдохните, сосчитайте до десяти, заварите себе горячий напиток 😤☕️🙂.&lt;/p&gt;

&lt;p&gt;Тут не будет:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;попыток писать на Go, как на RSpec,&lt;/li&gt;
&lt;li&gt;сравнения специфический возможностей,&lt;/li&gt;
&lt;li&gt;сравнения особенностей написания тестов.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Я прекрасно понимаю, что языки разные и у них разные подходы. Но когда разработчик пишет, он должен (но не всегда это понимает) решать ряд стандартных задач. И вот именно о том, как взяв в руки Go, обеспечить тот необходимый минимум, с реализацией которого мы (Ruby-разработчики) справляемся, когда используем RSpec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Почему не рассматриваем BDD фреймворки языка Go?
&lt;/h2&gt;

&lt;p&gt;RSpec - это BDD фреймворк и было бы логично сравнивать его с &lt;a href="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)" rel="noopener noreferrer"&gt;BDD&lt;/a&gt; фреймворком в Go, например с пакетами &lt;a href="https://github.com/onsi/ginkgo" rel="noopener noreferrer"&gt;Ginkgo&lt;/a&gt; или &lt;a href="https://github.com/smartystreets/goconvey" rel="noopener noreferrer"&gt;GoConvey&lt;/a&gt;, но делать мы этого не будем.&lt;/p&gt;

&lt;p&gt;А дело собственно в том, что согласно опросу в телеграм канале &lt;a href="https://t.me/gogolang" rel="noopener noreferrer"&gt;Go-go!&lt;/a&gt;, который я проводил при подготовке к докладу, можно сказать, что описанные выше пакеты использует примерно 10%-15% разработчиков. Не говоря уже о том, что достичь тех же результатов можно и при помощи стандартного пакета, начиная с Go 1.7 уж точно.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;К чему мы привыкли в Rspec?&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Тестирование методов
&lt;/h3&gt;

&lt;p&gt;В первую очередь стоит сказать, что это &lt;a href="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)" rel="noopener noreferrer"&gt;BDD&lt;/a&gt; фреймворк и поэтому имеет специфичную для этой парадигмы структуру написания тестов.&lt;/p&gt;

&lt;p&gt;Основными элементами теста на Rspec являются блоки:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;describe - описывает тестируемый класс или метод,&lt;/li&gt;
&lt;li&gt;context - для описания состояний, окружения, контекста (когда пользователь авторизован, когда пользователь не авторизован),&lt;/li&gt;
&lt;li&gt;it - тело теста.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;describe Store do
  describe '.sum' do
     context 'when summing positive numbers' do
        it { expect(described_class.sum(2, 2)).to eq 4 }
     end

     context 'when summing negative numbers' do
        it { expect(described_class.sum(-2, -2)).to eq -4 }
     end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Когда мы описываем метод как &lt;strong&gt;.sum&lt;/strong&gt; или &lt;strong&gt;::sum&lt;/strong&gt; , это означает, что мы тестируем метод класса (статический метод). Если мы описываем метод как &lt;strong&gt;#sum&lt;/strong&gt; , значит мы тестируем метод инстанса (метод, вызываемый на объекте).&lt;/p&gt;

&lt;h3&gt;
  
  
  Тестирование функции
&lt;/h3&gt;

&lt;p&gt;А вот тут ясности поменьше.&lt;/p&gt;

&lt;p&gt;По сути можно сказать, что RSpec - это фреймворк для тестирования ООП приложений. И в рамках тестирования такого приложения, данный вопрос у вас не возникнет. Так например, шанс, что в Rails приложении вам понадобится сделать нечто подобное, стремится у нулю.&lt;/p&gt;

&lt;p&gt;Но проблема лишь в том, что не ясно, как наиболее правильно описать такой тест.&lt;br&gt;&lt;br&gt;
RSpec по сути говорит нам, как наилучшим образом тестировать классы, но не запрещает нам использовать его для тестирования отдельных функций.&lt;/p&gt;

&lt;p&gt;В таком случае, если мы тестируем некий скрипт, в котором есть отдельно стоящая функция &lt;strong&gt;sum&lt;/strong&gt; , то мы можем написать тест как-то так и не переживать по этому поводу:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;describe 'Test some script' do
  describe '#sum' do
    it { expect(sum(2, 2)).to eq 4 }
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Тест не потерял в читаемости. Написать некий текст вместо имени тестируемого класса, вполне допустимо. В полноценном приложении (а не в скрипте), подобная ситуации - это редкость и исключительный случай. Так что сделаем маленькое исключение для него и не будем об этом жалеть.&lt;/p&gt;

&lt;h3&gt;
  
  
  Тестирование приватного метода
&lt;/h3&gt;

&lt;p&gt;Да, подобный трюк лучше не исполнять. Но рассмотрим саму возможность.&lt;/p&gt;

&lt;p&gt;Описание теста для приватного метода ничем не будет отличаться от публичного. Но тестировать придётся иначе, ведь Ruby не позволит просто так вызвать приватный метод. Но тут нам поможет метод &lt;strong&gt;send&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;describe Store do
  let(:store) { described_class.new }

  describe '#sum' do
    it { expect(store.send(:sum, 2, 2)).to eq 4 }
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  А как это в Go?
&lt;/h2&gt;

&lt;p&gt;Прежде чем мы углубимся, я хочу сделать акцент на одном принципиальном для меня отличии RSpec и тестов в Go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RSpec&lt;/strong&gt; даёт каркас для написания тестов и набор рекомендаций, как всем этим пользоваться. Уже после сюда добавляются соглашения &lt;a href="https://www.betterspecs.org/" rel="noopener noreferrer"&gt;Better Specs,&lt;/a&gt; делающие тесты ещё прекраснее.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go&lt;/strong&gt; старается быть простым и однозначным (тот же вопрос с фигурными скобками решён весьма однозначно 🙂). Язык даёт нам много хорошего инструментария и простые, но строгие руководства, как с ним работать. И эти руководства не просто строгие, они не гибкие (в рамках официальной документации).&lt;/p&gt;

&lt;p&gt;Собственно в этом и кроется &lt;strong&gt;ключевая проблема&lt;/strong&gt;. RSpec с виду не так прост, но практически не оставляет вопросов. Есть описание в документации, есть соглашения, но помимо этого есть гибкость, позволяющая покрывать специфичные случаи. А Go однозначен и строг. И разработчик, оказавшись в ситуации строгости, но недосказанности, не может понять, как ему быть в тех или иных случаях, которые не покрывает документация. Само собой, сложности добавляет отсутствие достаточного опыта в языке и контакта с другими разработчиками Go, чтобы интуитивно понимать допустимые и недопустимые решения.&lt;/p&gt;

&lt;h3&gt;
  
  
  Тестирование функции
&lt;/h3&gt;

&lt;p&gt;Тут всё на высоте. Всё просто и понятно. Идём читать документацию и&lt;/p&gt;

&lt;p&gt;видим там следующий шаблон для написания тестов:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func TestXxx(*testing.T)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;По сути, нам показали, как протестировать публичную (начинается с заглавной буквы) функцию.&lt;/p&gt;

&lt;p&gt;Следовательно, если в нашем пакете есть публичная функция Sum, то тест для неё будет выглядеть вот так:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func TestSum(t *testing.T) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Собственно, это всё, что нам рассказывает документация касательно именования тестов.&lt;/p&gt;

&lt;p&gt;Если в вашем приложении есть только функции, у вас нет структур, а если и есть, то они не содержат поведения, то описанного в документации вам достаточно, можете закрывать статью.&lt;/p&gt;

&lt;h3&gt;
  
  
  Тестирование приватной функции
&lt;/h3&gt;

&lt;p&gt;??????????????????????????????????&lt;/p&gt;

&lt;p&gt;Знаю-знаю, что тестировать приватные функции это не правильно, но в реальности случаются, пускай и редко, ситуации, где лучше сделать это, чем не сделать.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Но думаю, что Вы так не делаете. Вы же пишите идеальный код в идеальном приложении, спроектированном по всем канонам, а все разработчики, работавшие над ним, личной вскормлены дядюшкой Бобом.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;В зыке Ruby нет приватных функций, есть только приватные методы, поэтому что делать с такой ситуацией, Ruby-разработчикам не очень понятно.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Тестирование метода
&lt;/h3&gt;

&lt;p&gt;??????????????????????????????????&lt;/p&gt;

&lt;p&gt;Тоже не понять но как это делать. Нужно разбираться.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Всё тестирование в Ruby по сути заточено на тестирование методов, в то время как тестирование в Go, как следует из документации, больше ориентировано на тестирование функций (методы тестировать само собой тоже можно, но документация делает акцент на функциях). Это ещё одна особенность, которая усложняет адаптацию Ruby-разработчика к написанию тестов на Go.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Тестирование в разных обстоятельствах (в разном окружении)
&lt;/h3&gt;

&lt;p&gt;??????????????????????????????????&lt;/p&gt;

&lt;p&gt;Тут ответов тоже нет. Нам говорят о технике табличного тестирования, которая хороша и закрывает часть проблем, но всё же не применима для задачи разделения контекста.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Как мы помним, в RSpec эту задачу решает блок context. В Go это тоже можно сделать, просто ответ лежит не на поверхности.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Тестирование приватной функции
&lt;/h2&gt;

&lt;p&gt;Если написанное выше, касательно тестирования всего приватного, тебя не успокоило и душа всё ещё горит, можешь написать в комментах "Не тестируйте приватные функции".&lt;/p&gt;

&lt;p&gt;Проблема тут в том, что шаблон именования теста, подразумевает, что название функции будет написано с большой буквы, то есть это будет публичная функция.&lt;/p&gt;

&lt;p&gt;Поэтому, все варианты, которыми мы можем описать тест для приватной функции sum, буду нарушать соглашение из официальной документации.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func Testsum(t *testing.T) {
    ...
}

// или

func Test_sum(t *testing.T) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Так написать конечно можно, но какой вариант выбрать и почему? Как объяснять это другим разработчикам. Ответы будут ниже.&lt;/p&gt;

&lt;h2&gt;
  
  
  Тестирование метода
&lt;/h2&gt;

&lt;p&gt;Давайте вспомним нашу функцию &lt;strong&gt;Sum&lt;/strong&gt; и то, как мы писали тест для неё:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func Sum(x, y int) int {
    return x + y
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Но что если &lt;strong&gt;Sum&lt;/strong&gt; - это метод структуры? Можно предположить, что именование теста менять не стоит, и писать надо так же, как сказано в документации применительно к функциям. Это сработает, но глядя на именование теста, Вы не будете понимать, что тестируете. Подобная неоднозначность конечно не приятна, но с ней всё ещё можно мириться.&lt;/p&gt;

&lt;p&gt;Трудности начинаются, когда в пакете присутствуют &lt;a href="https://github.com/spf13/viper/blob/master/viper.go#L1492" rel="noopener noreferrer"&gt;метод&lt;/a&gt; и &lt;a href="https://github.com/spf13/viper/blob/master/viper.go#L1490" rel="noopener noreferrer"&gt;функция&lt;/a&gt; с одинаковым именованием, как например в пакете &lt;a href="https://github.com/spf13/viper" rel="noopener noreferrer"&gt;viper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe9ck5iejcgip30whm73c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe9ck5iejcgip30whm73c.png" width="598" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;К сожалению, в самом пакете viper не удалось найти тест, который бы показал, как решать эту проблему. Придётся выкручиваться самим.&lt;/p&gt;

&lt;h2&gt;
  
  
  Соглашение по именованию тестов из gotests - решение проблем
&lt;/h2&gt;

&lt;p&gt;Ответы на поставленные выше вопросы и не только на них даёт пакет &lt;a href="http://github.com/cweill/gotests" rel="noopener noreferrer"&gt;gotests&lt;/a&gt;. Этот пакет используется для автоматической генерации заготовок тестов, для абсолютно всего, что находится в вашем пакете.&lt;/p&gt;

&lt;p&gt;Взглянув на то, как он работает, мы можем определить используемые им правила именования и позаимствовать их.&lt;/p&gt;

&lt;p&gt;Правила выглядят следующим образом:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TestPubFunc(t *testing.T)&lt;br&gt;&lt;br&gt;
Test_privateFunct(t *testing.T)&lt;br&gt;&lt;br&gt;
TestPubStruct_PubMethod(t *testing.T)&lt;br&gt;&lt;br&gt;
TestPubStruct_privateMethod(t *testing.T)&lt;br&gt;&lt;br&gt;
Test_privateStruct_privateMethod(t *testing.T)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;В чём плюсы:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Подобное именование решает все заявленные проблемы.&lt;/li&gt;
&lt;li&gt;Из названия теста понятно, что тестируется.&lt;/li&gt;
&lt;li&gt;Соглашение отражено в паке, у которого почти 4 тыс. звёзд на GitHub.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Как мне кажется, этих причин достаточно, чтобы внедрить эту практику именования в свою команду. Она будет понятна всем, а кому-то уже знакомой, и не нужно изобретать "велосипед".&lt;/p&gt;
&lt;h2&gt;
  
  
  Тестирование в разном контексте
&lt;/h2&gt;

&lt;p&gt;Рассматривая, как подобное реализовано в RSpec, мы приводили вот такой пример:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;describe Store do
  describe '.sum' do
     context 'when summing positive numbers' do
        it { expect(described_class.sum(2, 2)).to eq 4 }
     end

     context 'when summing negative numbers' do
        it { expect(described_class.sum(-2, -2)).to eq -4 }
     end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Знатоки, вероятно, обратили внимание, что тех же результатов в Go можно добиться через табличное тестирование.&lt;/p&gt;

&lt;p&gt;То есть вот так:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func TestSum(t *testing.T) {
    tCases := []struct{
        x int
        y int
        res int
    }{
        {2, 2, 4},
        {3, 2, 5},
    }
    for _, tCase := range tCases {
        res := Sum(tCase.x, tCase.y)
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Единственное, чего тут не хватает, это ясности относительно того, а какой же случай мы тестируем в текущий момент.ю и какой тестовый набор провалился.&lt;/p&gt;

&lt;p&gt;Документация даёт нам решение и предлагает использовать &lt;a href="https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks" rel="noopener noreferrer"&gt;Subtests&lt;/a&gt; (подтесты).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func TestSum(t *testing.T) {
    tCases := []struct{...}{...}
    for _, tCase := range tCases {
        t.Run(
            fmt.Sprintf(“x=%d y=%d”, tCase.x, tCase.y),
            func(t *testing.T) {
                // test body
            },
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Вот теперь мы будем понимать, какой тестовый набор провалился.&lt;/p&gt;

&lt;p&gt;Вы скажете, что данная ситуация хорошо ложится на концепцию табличного тестирования, но есть и другие. И будете абсолютно правы.&lt;/p&gt;

&lt;p&gt;Наиболее удобный и понятный пример, на мой взгляд, это ситуация с авторизованным и не авторизованным пользователем. Так же можно рассмотреть ситуации с разным состоянием конфигурации приложения, но давайте возьмём пример с пользователем.&lt;/p&gt;

&lt;p&gt;Что делать, есть нам нужно проверить работу экшена контроллера для авторизованного и не авторизованного пользователя?&lt;/p&gt;

&lt;p&gt;Напрямую документация не даёт ответа на этот вопрос, но почему бы нам не воспользоваться функциональностью Subtests? Не вижу причин отказывать себе в этом. Единственное, чего не хватает, это некоего соглашения о том, как описывать в подтестах различные ситуации. Нам не известно такое соглашение в Go, не беда, возьмём соглашение из &lt;a href="https://www.betterspecs.org/" rel="noopener noreferrer"&gt;Better Specs&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func TestDeleteDatabase(t *testing.T) {
    t.Run("When the user is logged in", func(t *testing.T) {
        // test body
    })

    t.Run("When the user is not logged in", func(t *testing.T) {
        // test body
    })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Мне кажется, что выглядит хорошо, не так ли?&lt;/p&gt;

&lt;h2&gt;
  
  
  Итог
&lt;/h2&gt;

&lt;p&gt;В Go нет общепринятого стандарта именования тестов, так как официальная документация описывает лишь тестирование публичных функций, не уделяя внимание остальному, но это соглашение есть в пакете &lt;a href="https://github.com/cweill/gotests" rel="noopener noreferrer"&gt;gotests&lt;/a&gt;. Изучите его и используйте, если не сам пакет, то хотя бы его подход.&lt;/p&gt;

&lt;p&gt;Проблему тестирования в разном контексте прекрасно можно решить при помощи &lt;a href="https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks" rel="noopener noreferrer"&gt;Subtests&lt;/a&gt;, нужно лишь добавить к ним хорошее соглашение по описанию тестируемых случаев, как это сделано в &lt;a href="https://www.betterspecs.org/" rel="noopener noreferrer"&gt;Better Specs&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Огромное количество времени разработчик тратит не на решение задачи, а на выбор из нескольких вариантов решения. Как назвать переменную, метод или функцию, куда положить тот или иной файл, как описывать тесты.&lt;br&gt;&lt;br&gt;
Все эти вопросы решаются соглашениями.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;К сожалению, в Go соглашения - это слабая сторона, во всяком случае в сравнении с экосистемой Ruby. Я полагаю, что локально команды решают этот вопрос, но не хватает именно общепринятых соглашений.&lt;/p&gt;

&lt;p&gt;Отмечу, что разработать соглашение - это лишь малая часть работы, самое главное - это соглашение распространить в сообществе разработчиков.&lt;/p&gt;

&lt;p&gt;По моему мнению, сообщество Go нуждается в большем внедрении стандартов и соглашений, поэтому призываю вас принимать активное участие в решении этой проблемы.&lt;/p&gt;

&lt;p&gt;Всем сил, терпения и чистого кода 💪😎&lt;/p&gt;

</description>
      <category>go</category>
      <category>rspec</category>
      <category>ruby</category>
    </item>
  </channel>
</rss>
