12 факторов: не писать логи в файлы
В «12 факторах» написано: «логи — это поток событий». Сейчас объясню чуть проще.
Если джуниору поставить задачу записывать какие-то события в лог, скорее всего, его код будет выглядеть как-то так:
with open('logs/events.log') as fp:
fp.writeln('Event has happened')
То есть он просто возьмёт и запишет все события в файл. Такая простая и глупая операция сразу сделает ваше предложение жутко неудобным для всех, кто его обслуживает.
1. Никто не будет знать, пока не прочитает код, что logs/events.log — это артефакт, который надо хранить. Если запустить ваше приложение внутри докер-контейнера, то все ваши логи будут жить не больше, чем время жизни контейнера с приложением.
2. Приложение нельзя будет нормально запустить в нескольких экземплярах. Если у вас работают 5 копий (5 контейнеров или 5 серверов), то никто и никогда не скажет, на каком именно сервере лежит logs/events.log, придётся отсматривать все 5.
3. Файлы с логами нужно ротировать, чтобы не заканчивалось место. А если вы пишете по 100 событий в секунду — ротировать быстро.
Все эти проблемы решают централизованные системы логгинга — graphana loki, papertrail или даже datadog. Когда вы их подключаете, ваше приложение пишет логи не в файл, а через сокет на центральный сервер. Архивация и ротация у вас теперь на одной машине. А самое главное — поиск: любое средство хранения логов даёт вам поиск, который работает гораздо удобнее, чем grep.
В «12 факторах» написано: «логи — это поток событий». Сейчас объясню чуть проще.
Если джуниору поставить задачу записывать какие-то события в лог, скорее всего, его код будет выглядеть как-то так:
with open('logs/events.log') as fp:
fp.writeln('Event has happened')
То есть он просто возьмёт и запишет все события в файл. Такая простая и глупая операция сразу сделает ваше предложение жутко неудобным для всех, кто его обслуживает.
1. Никто не будет знать, пока не прочитает код, что logs/events.log — это артефакт, который надо хранить. Если запустить ваше приложение внутри докер-контейнера, то все ваши логи будут жить не больше, чем время жизни контейнера с приложением.
2. Приложение нельзя будет нормально запустить в нескольких экземплярах. Если у вас работают 5 копий (5 контейнеров или 5 серверов), то никто и никогда не скажет, на каком именно сервере лежит logs/events.log, придётся отсматривать все 5.
3. Файлы с логами нужно ротировать, чтобы не заканчивалось место. А если вы пишете по 100 событий в секунду — ротировать быстро.
Все эти проблемы решают централизованные системы логгинга — graphana loki, papertrail или даже datadog. Когда вы их подключаете, ваше приложение пишет логи не в файл, а через сокет на центральный сервер. Архивация и ротация у вас теперь на одной машине. А самое главное — поиск: любое средство хранения логов даёт вам поиск, который работает гораздо удобнее, чем grep.
Как сделать тестируемым класс, использующий нечистую встроенную функцию PHP?
Очень просто — объявить функцию необязательным параметром конструктора.
final class Service
{
/**
* @psalm-var callable(): int
*/
private $time;
/**
* @psalm-param ?callable(): int $time
*/
public function __construct(?callable $time = null)
{
$this->time = $time ?? 'time';
}
public function do(): void
{
$time = ($this->time)();
}
}
$service = new Service();
$serviceForTesting = new Service(static fn (): int => 1234567890);
Не советую использовать либы-костыли типа php-mock/php-mock.
Очень просто — объявить функцию необязательным параметром конструктора.
final class Service
{
/**
* @psalm-var callable(): int
*/
private $time;
/**
* @psalm-param ?callable(): int $time
*/
public function __construct(?callable $time = null)
{
$this->time = $time ?? 'time';
}
public function do(): void
{
$time = ($this->time)();
}
}
$service = new Service();
$serviceForTesting = new Service(static fn (): int => 1234567890);
Не советую использовать либы-костыли типа php-mock/php-mock.
12 факторов: неизменяемые релизы
Версия приложения, которую можно запустить в проде (или стейджинге), — это релиз. Релиз — иммутабельная штука: нельзя взять и что-нибудь поменять в релизе. Если нужно что-то поменять в коде приложения, делают новый релиз.
Релизы нужны, чтобы однозначно идентифицировать версию приложения в трейсах ошибок (к примеру, см. как клёво работает Sentry с релизами), а также для того, чтобы чётко было понятно, куда откатываться в случае проблем с деплоем: если не зашёл новый релиз — просто запускаем старый.
Релизы называют либо как хеш последнего коммита (типа релиз 19e267f04), либо просто по номерам билдов, типа релиз #19374. Если приложение пакуют в докер, номер релиза обычно записывают в лейбл контейнера.
Версия приложения, которую можно запустить в проде (или стейджинге), — это релиз. Релиз — иммутабельная штука: нельзя взять и что-нибудь поменять в релизе. Если нужно что-то поменять в коде приложения, делают новый релиз.
Релизы нужны, чтобы однозначно идентифицировать версию приложения в трейсах ошибок (к примеру, см. как клёво работает Sentry с релизами), а также для того, чтобы чётко было понятно, куда откатываться в случае проблем с деплоем: если не зашёл новый релиз — просто запускаем старый.
Релизы называют либо как хеш последнего коммита (типа релиз 19e267f04), либо просто по номерам билдов, типа релиз #19374. Если приложение пакуют в докер, номер релиза обычно записывают в лейбл контейнера.
Как пропустить первый элемент итератора в цикле? А пройти пять начиная с третьего?
💩 if (!isset($firstSkipped))
💩 if ($i++ < 2)
Все гораздо проще и лаконичнее с LimitIterator.
foreach (new LimitIterator($iterator, $offset = 2, $limit = 5) as $item) {
// ...
}
Обратите внимание, что первый аргумент LimitIterator имеет тип Iterator. То есть можно передать, например, Generator или ArrayIterator. Однако часто простые обходимые объекты реализуют IteratorAggregate, который не является подтипом Iterator. Как тут быть? Сначала на ум приходит забрать из него итератор вызовом $object->getIterator(). Она неверная, потому что IteratorAggregate::getIterator возвращает супертип Traversable, то есть это опять-таки может быть IteratorAggregate. Правильное решение — обернуть наш объект в IteratorIterator, который превращает любой Traversable в Iterator.
Итак, проход по первым трём элементам любого обходимого объекта будет выглядеть так:
foreach (new LimitIterator(new IteratorIterator($traversable), 0, 3) as $item) {
// ...
}
💩 if (!isset($firstSkipped))
💩 if ($i++ < 2)
Все гораздо проще и лаконичнее с LimitIterator.
foreach (new LimitIterator($iterator, $offset = 2, $limit = 5) as $item) {
// ...
}
Обратите внимание, что первый аргумент LimitIterator имеет тип Iterator. То есть можно передать, например, Generator или ArrayIterator. Однако часто простые обходимые объекты реализуют IteratorAggregate, который не является подтипом Iterator. Как тут быть? Сначала на ум приходит забрать из него итератор вызовом $object->getIterator(). Она неверная, потому что IteratorAggregate::getIterator возвращает супертип Traversable, то есть это опять-таки может быть IteratorAggregate. Правильное решение — обернуть наш объект в IteratorIterator, который превращает любой Traversable в Iterator.
Итак, проход по первым трём элементам любого обходимого объекта будет выглядеть так:
foreach (new LimitIterator(new IteratorIterator($traversable), 0, 3) as $item) {
// ...
}
12 факторов: сервисы — это подключаемые ресурсы
Если попросить джуна настроить отсылку почты, он вполне может впасть в крайность и вызвать /usr/sbin/sendmail. Если попросить сделать файловое хранилище, то он сделает папочку /home/app/files.
Тем самым он подкинет много работы админам: мало того что всё это добро должно быть внутри контейнера с приложением, так это ещё и целая инфраструктура: нужно поддерживать репутацию почтовых доменов; следить, чтобы все файлы были доступны всем инстансам приложения (файл сохранили на одной машине, а прочитать его надо на другой); бэкапить и мониторить всё это, наконец.
Более продвинутый джун уже знает про SES и S3: он затянет в приложение их официальные либы, решив существенную часть проблем. Но не все: к примеру, на openshift уже не смигрируешь, да и на локальной машине отладка и тестирование сильно усложнится — нужно будет либо мoкать хождение в амазон, либо на каждый тест, который, скажем, генерит юзера, класть его аватарку в облако.
Продвинутые ребята рассматривают такие зависимости как внешние ресурсы, поддерживая для них полноценные абстракции. К примеру, в джанге складывают файлы через storage api и django-storages, а почту шлют через django-anymail. Поменял переменную окружения — и вот уже файлы складываются на рамдиск вместо s3, а почта шлётся через sendgrid вместо postmark.
Если попросить джуна настроить отсылку почты, он вполне может впасть в крайность и вызвать /usr/sbin/sendmail. Если попросить сделать файловое хранилище, то он сделает папочку /home/app/files.
Тем самым он подкинет много работы админам: мало того что всё это добро должно быть внутри контейнера с приложением, так это ещё и целая инфраструктура: нужно поддерживать репутацию почтовых доменов; следить, чтобы все файлы были доступны всем инстансам приложения (файл сохранили на одной машине, а прочитать его надо на другой); бэкапить и мониторить всё это, наконец.
Более продвинутый джун уже знает про SES и S3: он затянет в приложение их официальные либы, решив существенную часть проблем. Но не все: к примеру, на openshift уже не смигрируешь, да и на локальной машине отладка и тестирование сильно усложнится — нужно будет либо мoкать хождение в амазон, либо на каждый тест, который, скажем, генерит юзера, класть его аватарку в облако.
Продвинутые ребята рассматривают такие зависимости как внешние ресурсы, поддерживая для них полноценные абстракции. К примеру, в джанге складывают файлы через storage api и django-storages, а почту шлют через django-anymail. Поменял переменную окружения — и вот уже файлы складываются на рамдиск вместо s3, а почта шлётся через sendgrid вместо postmark.
В PhpStorm наконец-то добавят поддержку Psalm и PHPStan 🎊
Предположительно, плагины войдут в релиз 2020.3, так что ждём сентябрьский EAP.
Впервые плагины оформлены в виде опенсорс-проектов на GitHub, так что мы можем наблюдать как на языке со статическим анализом реализуется поддержка внешнего статического анализа для языка без статического анализа 🤣 Если серьезно, то проекты можно использовать как эталоны для собственных плагинов.
Подробнее в блоге JetBrains: https://blog.jetbrains.com/phpstorm/2020/07/phpstan-and-psalm-support-coming-to-phpstorm/.
Как отчаянный псалмовец 😜 очень рад быть к этому причастным.
Во время карантина по просьбе @pronskiy записал для команды обзорный скринкаст по фичам Psalm.
Предположительно, плагины войдут в релиз 2020.3, так что ждём сентябрьский EAP.
Впервые плагины оформлены в виде опенсорс-проектов на GitHub, так что мы можем наблюдать как на языке со статическим анализом реализуется поддержка внешнего статического анализа для языка без статического анализа 🤣 Если серьезно, то проекты можно использовать как эталоны для собственных плагинов.
Подробнее в блоге JetBrains: https://blog.jetbrains.com/phpstorm/2020/07/phpstan-and-psalm-support-coming-to-phpstorm/.
Как отчаянный псалмовец 😜 очень рад быть к этому причастным.
Во время карантина по просьбе @pronskiy записал для команды обзорный скринкаст по фичам Psalm.
12 факторов: экспортировать сервисы через порты
Когда-то, когда сайты модно было делать на ПХП, конфигурация среды размазывалась буквально везде. Какие-то редиректы можно было настроить в .htaccess, логирование нужно было настроить в конфигурации Apache, версию самого Apache нужно было выбрать через контрольную панель хостинга. Сайт, написанный под один хостинг, мог не заработать на другом — где-то ПХП был подключён как модуль Apache, а где-то — через FastCGI, и это требовало разной механики работы с заголовками.
Это породило страшные вещи вроде рекомендаций по разворачиванию на 50 строк или гигантских виртуальных машин на Vagrant. Такой подход делает почти невозможной смену среды выполнения и съедает кучу ресурсов — как разработчика, который читает мануалы или обслуживает виртуалки, так и его машины, которая вынуждена всё это крутить.
Сейчас принято делать приложения, которые хостят себя сами. На node.js так было с самого начала — попробуйте найти хоть один проект, который имеет веб-интерфейс и не содержит внутри простого HTTP-сервера на express.js. У Java такая практика тоже принята давно — там испокон веков существуют Jetty и Tomcat (а сейчас наверняка что-то более модное).
Ну а если пишете на чём-нибудь скриптовом вроде питона, обязательно включите в докер-образ uwsgi\gunicorn — пусть ваше приложение просто выставляет наружу порт, обращаясь к которому можно получить все нужные сервисы. Так вы не только упростите деплой и масштабирование, но и сделаете возможным всякие интересные интеграционные штуки вроде возможности прогнать браузерные тесты относительно свежесобранного приложения прямо в процессе CI.
Когда-то, когда сайты модно было делать на ПХП, конфигурация среды размазывалась буквально везде. Какие-то редиректы можно было настроить в .htaccess, логирование нужно было настроить в конфигурации Apache, версию самого Apache нужно было выбрать через контрольную панель хостинга. Сайт, написанный под один хостинг, мог не заработать на другом — где-то ПХП был подключён как модуль Apache, а где-то — через FastCGI, и это требовало разной механики работы с заголовками.
Это породило страшные вещи вроде рекомендаций по разворачиванию на 50 строк или гигантских виртуальных машин на Vagrant. Такой подход делает почти невозможной смену среды выполнения и съедает кучу ресурсов — как разработчика, который читает мануалы или обслуживает виртуалки, так и его машины, которая вынуждена всё это крутить.
Сейчас принято делать приложения, которые хостят себя сами. На node.js так было с самого начала — попробуйте найти хоть один проект, который имеет веб-интерфейс и не содержит внутри простого HTTP-сервера на express.js. У Java такая практика тоже принята давно — там испокон веков существуют Jetty и Tomcat (а сейчас наверняка что-то более модное).
Ну а если пишете на чём-нибудь скриптовом вроде питона, обязательно включите в докер-образ uwsgi\gunicorn — пусть ваше приложение просто выставляет наружу порт, обращаясь к которому можно получить все нужные сервисы. Так вы не только упростите деплой и масштабирование, но и сделаете возможным всякие интересные интеграционные штуки вроде возможности прогнать браузерные тесты относительно свежесобранного приложения прямо в процессе CI.
12 факторов: хранить состояние вне процесса
Запущенное приложение не должно рассчитывать на внутреннее состояние процесса. Когда-то было принято хранить на диске буквально всё. Кроме сессий в /var/tmp, нормальной практикой было сжимать загруженные в админку изображения в момент их запроса. Типа запросил пользователь фоточку товара 400x400, а у нас в кеше есть только 200x200 — идём в оригинал, сжимаем его в 400x400 и сохраняем на диск. Другим пользователям уже отдаём кешированную версию. Думаю, не нужно рассказывать, чем это плохо, если вы читали мои предыдущие посты про 12 факторов.
Такой подход до сих пор проскакивает в маргинальных инструментах вроде django-webpack-loader: эта штука собирает JS в момент запроса и сохраняет где-то на диске. Конечно же, если вы деплоите JS, который почему-то не собирается, вы об этом узнаете не в CI, а от пользователей, которым прилетит неработающий бандл.
Не делайте так. Все преобразования выполняйте в процессе CI, а промежуточные данные храните в отдельных сервисах — данные в базе, сессии в кеше, а картинки где-нибудь в S3.
Запущенное приложение не должно рассчитывать на внутреннее состояние процесса. Когда-то было принято хранить на диске буквально всё. Кроме сессий в /var/tmp, нормальной практикой было сжимать загруженные в админку изображения в момент их запроса. Типа запросил пользователь фоточку товара 400x400, а у нас в кеше есть только 200x200 — идём в оригинал, сжимаем его в 400x400 и сохраняем на диск. Другим пользователям уже отдаём кешированную версию. Думаю, не нужно рассказывать, чем это плохо, если вы читали мои предыдущие посты про 12 факторов.
Такой подход до сих пор проскакивает в маргинальных инструментах вроде django-webpack-loader: эта штука собирает JS в момент запроса и сохраняет где-то на диске. Конечно же, если вы деплоите JS, который почему-то не собирается, вы об этом узнаете не в CI, а от пользователей, которым прилетит неработающий бандл.
Не делайте так. Все преобразования выполняйте в процессе CI, а промежуточные данные храните в отдельных сервисах — данные в базе, сессии в кеше, а картинки где-нибудь в S3.
Журнал Glamour включил трансгендера, PHP разработчика, активного контрибьютора Symfony, Манро Бергдорф в свою премию «Женщина Года».
Этот трап-активист ещё в 2017 году стал первой трансгендерной моделью L'Oreal, но был уволен за расистские высказывания:
«Честно, у меня больше нет сил говорить о расистском насилии со стороны белых людей. Да, ВСЕХ белых людей. Поскольку большинство из вас даже не осознает, что все ваше существование, привилегии и успех как расы построен на спинах, крови и гибели цветных людей. Все ваше существование пропитано расизмом».
Однако уже в 2020 году, после смерти Джорджа Флойда, L'Oreal решили извиниться перед трапом и возобновили с ним сотрудничество.
Этот трап-активист ещё в 2017 году стал первой трансгендерной моделью L'Oreal, но был уволен за расистские высказывания:
«Честно, у меня больше нет сил говорить о расистском насилии со стороны белых людей. Да, ВСЕХ белых людей. Поскольку большинство из вас даже не осознает, что все ваше существование, привилегии и успех как расы построен на спинах, крови и гибели цветных людей. Все ваше существование пропитано расизмом».
Однако уже в 2020 году, после смерти Джорджа Флойда, L'Oreal решили извиниться перед трапом и возобновили с ним сотрудничество.
Эффективное проектирование агрегатов
Эссе Вона Вернона в трёх коротких частях, которое поможет с пониманием паттерна агрегат.
Прочитав его, вы получите ответы на следующие вопросы:
• как и зачем проектировать маленькие агрегаты;
• что такое истинные инварианты и как их искать;
• почему Value Object предпочтительнее, чем Entity;
• когда нужна мгновенная (immediate), а когда конечная (eventual) согласованность;
• когда позволительно изменять несколько агрегатов в одной транзакции.
По сути, это квинтэссенция нескольких важных глав из синей и красной книги по DDD.
https://dddcommunity.org/library/vernon_2011/
Эссе Вона Вернона в трёх коротких частях, которое поможет с пониманием паттерна агрегат.
Прочитав его, вы получите ответы на следующие вопросы:
• как и зачем проектировать маленькие агрегаты;
• что такое истинные инварианты и как их искать;
• почему Value Object предпочтительнее, чем Entity;
• когда нужна мгновенная (immediate), а когда конечная (eventual) согласованность;
• когда позволительно изменять несколько агрегатов в одной транзакции.
По сути, это квинтэссенция нескольких важных глав из синей и красной книги по DDD.
https://dddcommunity.org/library/vernon_2011/
12 факторов: явные зависимости
Все зависимости вашего приложения должны быть выражены явно. И я говорю не только о списке зависимостей из package.json, Pipfile, Cargo.toml или что там у вас — нужны именно все зависимости.
Если вашему приложению, чтобы запуститься, нужна установленная русская локаль, древняя версия openssl или elasticsearch, слушающий на 9000 порту, и вы не нашли способ понятно выразить это в репозитории, вам будет очень грустно всякий раз, когда придётся сталкиваться с девопсом. Вы будете чертыхаться всякий раз, поднимая окружение для CI, разворачивая проект на локальной машине или добавляя новую тачку в парк серверов.
В README такие требования описывать глупо — никто не будет их там искать: люди обычно не читают документацию. А даже если и читают, то вряд ли вы найдёте в себе силы актуализировать там версии зависимостей. Описывать их имеет смысл на языках, которые позволят автоматически воспроизводить нужный вашему приложению контекст. В худшем случае это какая-нибудь виртуалка на Vagrant (надеюсь никогда больше его не увидеть), чуть получше — плейбуки Ansible. Норм — это Dockerfile и docker-compose.
Все зависимости вашего приложения должны быть выражены явно. И я говорю не только о списке зависимостей из package.json, Pipfile, Cargo.toml или что там у вас — нужны именно все зависимости.
Если вашему приложению, чтобы запуститься, нужна установленная русская локаль, древняя версия openssl или elasticsearch, слушающий на 9000 порту, и вы не нашли способ понятно выразить это в репозитории, вам будет очень грустно всякий раз, когда придётся сталкиваться с девопсом. Вы будете чертыхаться всякий раз, поднимая окружение для CI, разворачивая проект на локальной машине или добавляя новую тачку в парк серверов.
В README такие требования описывать глупо — никто не будет их там искать: люди обычно не читают документацию. А даже если и читают, то вряд ли вы найдёте в себе силы актуализировать там версии зависимостей. Описывать их имеет смысл на языках, которые позволят автоматически воспроизводить нужный вашему приложению контекст. В худшем случае это какая-нибудь виртуалка на Vagrant (надеюсь никогда больше его не увидеть), чуть получше — плейбуки Ansible. Норм — это Dockerfile и docker-compose.
12 факторов: одна кодовая база — много деплоев
Хорошее приложение полностью находится под системой контроля версий и лежит в одном репозитории (мы здесь говорим о приложениях, а не о распределённых системах). Содержимое репозитория раскатывается в любую среду — на прод, на машину разработчика, на стейджинг, если надо.
Если у вас прод использует одну кодовую базу, а дев-машина или стейджинг — немного другую, то рано или поздно у вас на прод проскочит ошибка, которую нельзя воспроизвести нигде, кроме прода. Вот и придётся затягивать упоротые решения вроде rookout или просто писать console.log.
Код на инстансах при этом может отличаться — вполне нормально выкатывать фиче-бранчи с какими-нибудь коммитами, которые ещё не попали на прод, но уже хочется потестировать. Главное, чтобы это были разные версии одного репозитория.
Хорошее приложение полностью находится под системой контроля версий и лежит в одном репозитории (мы здесь говорим о приложениях, а не о распределённых системах). Содержимое репозитория раскатывается в любую среду — на прод, на машину разработчика, на стейджинг, если надо.
Если у вас прод использует одну кодовую базу, а дев-машина или стейджинг — немного другую, то рано или поздно у вас на прод проскочит ошибка, которую нельзя воспроизвести нигде, кроме прода. Вот и придётся затягивать упоротые решения вроде rookout или просто писать console.log.
Код на инстансах при этом может отличаться — вполне нормально выкатывать фиче-бранчи с какими-нибудь коммитами, которые ещё не попали на прод, но уже хочется потестировать. Главное, чтобы это были разные версии одного репозитория.