Информатизация вуза. Терминальный сервер. Настройка пользовательского интерфейса. GPO

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

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

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

  • Клиент 1с или другого приложения – по тематике работы пользователей

  • Набор офисных программ.

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

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

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

Для запрета хранения файлов на терминальном сервере необходимо:

  • Отключить отображение всех локальных дисков и запретить к ним доступ (для пользователей)

  • Отключить отображение всех элементов рабочего стола

  • Создать набор ярлыков необходимого ПО в панели задач

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

Будем отталкиваться от того, что терминальный сервер со всем ПО у нас уже установлен (это тоже можно автоматизировать, но это уже другая история).

На контроллере домена создаем объект групповой политики через оснастку «Управление групповой политикой» и редактируем его.

Отключаем рабочий стол:

Конфигурация пользователя – Административные шаблоны – Рабочий стол – Рабочий стол: Отключить все элементы

Конфигурация пользователя – Административные шаблоны – Рабочий стол: Скрыть и отключить все элементы рабочего стола

Скрываем диски:

Конфигурация пользователя — Административные шаблоны — Компоненты Windows — Проводник Windows: Скрыть указанные диски

Запрещаем доступ к локальным дискам:

Конфигурация пользователя — Административные шаблоны — Компоненты Windows — Проводник Windows: Запретить доступ к дискам через Мой компьютер — этот параметр политики не запрещает пользователям использовать другие программы для доступа к локальным и сетевым дискам. Он также не запрещает использовать оснастку «Управление дисками» для просмотра и изменения характеристик дисков. Поэтому все ПО, которое пишет в temp или в профиль пользователя должно работать…

Можно отключить известные папки, но через групповые политики у меня это не получилось – видимо необходимо это делать через реестр: политика «Отключить известные папки» не заработала – видимо она применяется после того как профиль пользователя создан и уже созданы известные папки… кто в теме поясните в комментариях.

Настройка фильтрации политики:

Так как данная политика применяется к пользователю и должна выполняться только на терминальных серверах то необходимо настроить фильтрацию:

На вкладке область в фильтрах безопасности удалить группу «Прошедшие проверку» так как эта группа включает все компьютеры и всех пользователей.

Добавить группу пользователей и учетки серверов где эту политику необходимо применить.

Политику считывает компьютер поэтому, если учетки компьютера нет в фильтре, то она не применяется.

Эти же группы должны быть и на вкладке делегирование (с этой вкладкой будьте осторожны!!!).

НО!!!! Скрывать диски нужно только пользователям, а администраторам это незачем, поэтому во вкладке «Делегирование нажимаем «Дополнительно» и на группе «Администраторы домена» ставим галку «Запретить» напротив «Применить
групповую политику»:

Есть другой путь фильтрации — на вкладке «Область» – применить WMI-фильтр – он из себя представляет запрос, но, чтобы его применить его надо сначала создать в оснастке «Управление групповой политикой» в подразделении «Фильтры WMI».

Поэтому можно в фильтрах безопасности и во вкладке «Делегирование» оставить группу «Прошедшие проверку», а ниже добавить WMI-фильтр, например, такого содержимого:

SELECT * FROM Win32_ComputerSystem WHERE Name='server-01' OR Name='server-02'

Но я бы применял и Secure и WMI фильтры.

Создания списка ПО в панели задач

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

Export-StartLayout -path c:\ps\StartLayout.xml

В полученном файле содержится информация о ярлыках в формате xml. Теперь немного подредактируем его и добавим список программ на панель задач. Для этого после </DefaultLayoutOverride> добавим следующее:

  <CustomTaskbarLayoutCollection PinListPlacement="Replace" xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout">     <defaultlayout:TaskbarLayout>       <taskbar:TaskbarPinList>         <taskbar:DesktopApp DesktopApplicationLinkPath="C:\Program Files\1cv8\common\1cestart.exe" />         <taskbar:DesktopApp DesktopApplicationLinkPath="C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Excel 2016.lnk" />         <taskbar:DesktopApp DesktopApplicationLinkPath="C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Word 2016.lnk" />         <taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk" />       </taskbar:TaskbarPinList>     </defaultlayout:TaskbarLayout>   </CustomTaskbarLayoutCollection> 

Затем можно проверить получившийся файл командой:

Import-StartLayout -LayoutPath c:\ps\StartLayout.xml  -MountPath c:\

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

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

Теперь создадим политику:

«Конфигурация пользователя», «Административные шаблоны», «Компоненты Windows», “Меню ПУСК и панель задач”: Макет начального экрана – и в поле «Файл макета начального экрана» прописываем путь до xml: \\server\share\ StartLayout.xml

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

Применяем к политику к соответствующему подразделению и добавляем фильтр.

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

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

Конфигурация компьютера — Административные шаблоны — Система — Профили пользователей: Удалять при перезагрузке системы профили пользователей по истечении указанного числа дней.

Теперь давайте собственно создадим политику для ежедневной перезагрузки сервера:

Конфигурация компьютера  — Настройка – Параметры панели управления – Назначенные задания:

Создаем задание:

Настраиваем расписание и создаем действие:

Обратите внимание, так как задание создаем в ветке Конфигурация компьютера, то в задании указываем пользователя: NT AUTHORITY\SYSTEM – но будьте осторожны, у системы большие привилегии, поэтому предварительно проверяйте ваши скрипты. Я пытался запускать это задание из-под пользователя, но оно не выполнялось по какой-то причине – но здесь я могу быть не прав и скорее всего все нормально запускается и из-под пользователя – поправьте в комментариях пожалуйста.

Мы вроде бы запретили пользователям использовать диск C, но политики не запрещают использовать диск программам, и, например, 1с может накешировать очень много, поэтому я использую скрипт, который удаляет большие профили – запуск скрипта в то время, когда нет пользователей на сервере и запуск аналогично заданию на перезагрузку — создадим политику для ежедневного удаление больших профилей пользователей:

Конфигурация компьютера  — Настройка – Параметры панели управления – Назначенные задания:

Создаем задание

Настраиваем расписание, а потом настраиваем действие:

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

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

$h = hostname $NotDeletedUsers=@('All Users', 'shmelevfm', 'shmelevnm', 'Default', 'Все пользователи') $sum=0 gci -force 'C:\Users'-ErrorAction SilentlyContinue | ? { $_ -is [io.directoryinfo] } | % { $len = 0 gci -recurse -force $_.fullname -ErrorAction SilentlyContinue | % { $len += $_.length/1MB } #write "$_.BaseName : $len" | out-file C:\log\deUsers.txt -append if($len -gt 400){     $user=$_.BaseName     Write-Host $user !!!!!!!         if($_.BaseName -NotIn $NotDeletedUsers ){             Write "$h $(get-date -format "dd.MM.yy.HH.mm.ss") Будем удалять профиль пользователя $user : $len"  | out-file \\storage\logs\deUsers.txt -append             Write "__________________  $user  ______________________________" | out-file \\storage\logs\deUsers.txt -append             Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.LocalPath.split('\')[-1] -eq $user } | Remove-CimInstance         }  }else {$sum = $sum + $len}  } “Общий размер профилей”,'{0:N2} GB' -f ($sum) | out-file \\storage\logs\deUsers.txt -append 

После создания политик – необходимо зайти на сервера и выполнить gpupdate /force

Чтобы посмотреть применение политик gpresult /r, gpresult /r /scope:computer

Теперь, благодаря доменным политикам новый терминальный сервер можно настроить в два клика

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


ссылка на оригинал статьи https://habr.com/ru/articles/737834/

BLE под микроскопом. WCH forever :-)

Введение

Итак, приступим. Сразу хочу рассказать о двух русскоязычных площадка на которых мы с коллегами уже активно обсуждаем эти чипы. Вот первая, а здесь вторая площадка, которую я и создал 🙂 Как я обещал, выражаю большую благодарность участнику sed_alex с форума easyelectronics.ru за первоначальную помощь в освоении этого чипа. В этой статье я постараюсь систематизировать наработанные данные и, по возможности, не дублировать материал из форумов. Для ознакомления с семейством, я приобрел на Алиэкспрессе отладочную плату и программатор. Здесь я указал много ссылок, которые помогут вам в выборе. Да вы и сами найдете много интересного на АлиЭкспрессе по запросу СР582. Лично я купил вот эти изделия. На отладочной плате, по центру, есть отдельные отверстия под программатор и UART1, который используется при отладке.

Рис.1   Отладочная плата и программатор

Рис.1 Отладочная плата и программатор

Затем я скачал SDK с примерами со страницы производителя на GitHub а после занялся установкой среды разработки MounRiver Studio. Так как я работаю на платформе Windows, я её и выбрал. На текущий момент можно скачать файл MounRiver_Studio_Setup_V185.zip. Обновления среды появляются регулярно. Среда построена на основе Eclipse, поэтому всё достаточно привычно. Вот как она выглядит

Рис.2   Среда разработки MounRiver Studio

Рис.2 Среда разработки MounRiver Studio

Чтобы загрузить в среду проект надо сделать всего несколько шагов. Выбрать пункт меню File->Import, затем выбрать тип проекта и путь к нужной папке. Нажимаем Finish и новый проект добавляется слева в окне Project Explorer.

Рис.3   Загрузка проекта в среду разработки

Рис.3 Загрузка проекта в среду разработки

Теперь научимся загружать проект в чип под отладчиком. Для этого выбираем пункт меню Run ->Debug Configurations и два раза слева в окне нажимаем на поле GDB OpenOCD MRS Debugging. Слева появится новое поле с названием проекта, а справа — его настройки.

Рис.4   Окно Debug Configurations

Рис.4 Окно Debug Configurations

Иногда окошко С/С++ Application оказывается пустым. Тогда нужно, используя кнопку Browse, найти и выбрать *elf файл своего проекта. Получится что то похожее, как на картинке. Затем открываем закладку SVD Patch и указываем путь до .svd файла нашего микроконтроллера. Без этого отладка работать не будет. Если вы устанавливали MounRiver Studio по умолчанию тогда и путь у вас окажется таким же самым как на картинке: C:\MounRiver\MounRiver_Studio\template\wizard\WCH\RISC-V\CH58X\NoneOS\CH58Xxx.svd

Рис.5   Окно с указателем пути к файлу svd

Рис.5 Окно с указателем пути к файлу svd

Если теперь нажать кнопки Apply а затем Debug, в контроллер начнет прошиваться hex файл вашего проекта. У меня на программаторе начинает мигать синий светодиод. Если этого не происходит. Во-первых проверьте, что проект был предварительно откомпилирован и собран. Во-вторых убедитесь, что на контроллере включен Debug режим. С завода он идет отключенным. Для включения, надо выбрать пункт меню Tools->WCH In-System Programmer. Появится следующее окно программатора.

Рис.6   Окно программатора

Рис.6 Окно программатора

Теперь, при отключенном питании, на отладочной плате зажмите кнопку BOOT и подключите её через USB Data кабель к компьютеру. В окне Download Record должна появиться запись о том, что устройство найдено, так как на картинке. Из этого окна мы можем программировать микроконтроллеры напрямую через USB порт. Так же отсюда мы можем включить Debug порт на контроллере. Для этого нажмите синюю кнопку En-Simulat. На предупреждающем транспаранте нажмите кнопочку «Да». После этого в окошке Download Record появится надпись: «Succ to Enable Debug Interface!» Всё, вы подключили Debug режим. Теперь снова попытайтесь загрузить ваш *.hex в микроконтроллер. Должно всё получится и секунд через 30 через вы увидите такую картинку. Время от времени у меня на контроллере режим Debug отваливается, поэтому иногда приходится повторять эту процедуру.

Рис.7   Окно Debuger-a

Рис.7 Окно Debuger-a

Режим Peripheral

Изучение этого режима я бы начал с WCH\ch583-main\EVT\EXAM\BLE\Peripheral. Все проекты построены сходным образом, поэтому разобравшись с ним, вы легко поймете остальные. В папке APP находятся основные файлы проекта. Файл peripheral_main.с служит для запуска приложения. В нем желательно ничего не менять. Другой файл peripheral.с содержит основную логику проекта, который мы можем модифицировать. Кроме того, есть ещё папка Profile, в которой лежат файлы различных профилей. В нашем проекте находятся файлы devinfoservice.с и gattprofile.с в которых реализованы соответствующие сервисы. В начале каждого из этих файлов находится таблица Profile Attributes. В ней конфигурируются атрибуты данных профилей. Проще говоря, мы задаем элементы таблицы атрибутов, которую мы рассматривали в статьях ATTы GATTы… и ATTы GATTы Продолжение. Разобравшись с этой таблицей, вы поймете как вообще работает GATT механизм. Это будет не совсем просто, но без этого далеко не уехать. В своей предыдущей статья я подробно касался вопроса таблицы атрибутов. В эфире прошивка выглядит следующим образом.

Рис.8 Проект Peripheral

Рис.8 Проект Peripheral

Профили devinfoservice и gattprofile содержат по несколько характеристик. Я не буду подробно их описывать. Это проще и нагляднее сделать самим, используя программу nRF Connect. Если вы хотите создавать свои собственные проекты, вам будет необходимо от чего то отталкиваться. Данные сервисы очень хорошо подходят для этой цели.

Режим OTA

У всех современных BLE контроллеров есть режим OTA — «Over-The-Air», то есть обновление по воздуху. Имеется он так же у WCH. Более того, фирма представила даже целых два способа обновления для контроллеров с разным объемом памяти. Рассмотрим их.

OnlyUpdateApp

Этот способ используется для контроллеров с малым объемом внутреннего FLASH. Это CH581F с памятью в 192 кБайтов. Вся память делится на 4 части. В первой располагается вектор перехода (JumpIAP) на бутлоадер IAP. Во второй части (Peripheral) располагается приложение пользователя, которое и надо обновлять. В третьей части памяти находится сам бутлоадер IAP (In-Application Programming). Ну и наконец, в последней части находится библиотека стека BLE.

Рис.9 OTA OnlyUpdateApp

Рис.9 OTA OnlyUpdateApp

При подаче питания на контроллер, управление передается по нулевому адресу программы, где лежит команда (JumpIAP) перехода на IAP. После этого перехода, внутри IAP анализируется нужно производить обновление или нет. Если обновление не нужно, тогда управление передается в сегмент Peripheral и дальше исполняется программа пользователя. Если же обновлять прошивку нужно, управление остается в сегменте IAP и используя стек BLE из части LIB, контроллер обновляет прошивку пользователя в сегменте Peripheral. Во время обновления, функции самой прошивки Peripheral не доступны.

            Для создания обобщенной прошивки, используются 3 отдельных проекта из папки WCH\ch583-main\EVT\EXAM\BLE с общим названием OnlyUpdateApp. Четвертым файлом является файл стека CH58xBLE_ROM. Объединяются 4 HEX файла при помощи приложения AssemblingFileTool, находящегося в папке WCH\ch583-main\Android Tool.  На андроиде, для обновления прошивки «по-воздуху», используется приложение CH583 OTA Tool, находящегося в WCH\ch583-main\Android Tool\OTA Tool. По адресу WCH\ch583-main\Android Tool лежат pdf документы с описанием режима OTA. Они на китайском, но по картинкам и с помощью переводчика вполне можно понять что и как делать.

BackupUpgrade

Для процессоров с большим объемом памяти, таким как CH582/583, используется другой способ обновления. В этом, втором способе, обновляется не только приложение пользователя, но также сама BLE библиотека. Кроме того, при этом способе сохраняется работоспособность пользовательского кода.  Здесь FLASH память так же делится на 4 части. В первой (JumpIAP) так же лежит команда перехода на IAP. Во второй части лежат приложение пользователя и библиотека BLE.  Третья часть памяти остается пустой. Сюда будет записываться новая прошивка. Ну и в последней части памяти находится сам IAP.

Рис.10 OTA BackupUpgrade

Рис.10 OTA BackupUpgrade

При подаче питания на контроллер, также как и в первом случае, управление передается по нулевому адресу программы, где лежит команда (JumpIAP) перехода на IAP. Далее бутлоадер IAP анализирует надо ли ему включаться в работу или нет. Если нет, тогда управление передается приложению пользователя. Одной из задач этого приложения является заполнение пустого пространства FLASH памяти новой прошивкой. Если загрузка новой прошивки прошла успешно, выставляется флаг события и подается команда перезагрузки.  В этом случае IAP копирует новое приложение пользователя и BLE библиотеку из третьего в второй сегмент FLASH памяти, снимает флаг события и перезагружает процессор.

Немного забегая вперед, хочу уточнить один момент. Основная прошивка с которой мы работаем ( BackupUpgrade_OTA ), по-умолчанию размещается в памяти микроконтроллера по адресу 0х00001000. Это нужно для работы режима OTA, но для тестирования это крайне неудобно. На этапе отладки нужно перенести её по нулевому адресу. Делается это в файле линкера Link.ld. После изменений надо пересобрать весь проект заново, нажав сверху кнопку с двумя стрелками Rebuild (Shift +F7). После всех доработок, верните всё обратно, иначе обобщенную прошивку для обновления «по воздуху» собрать не получится.

Рис.11 Настройки линкера для OTA

Рис.11 Настройки линкера для OTA

 Для создания обобщенной прошивки, используются 3 отдельных проекта из папки WCH\ch583-main\EVT\EXAM\BLE с общим названием BackupUpgrade. Объединяются 3 HEX файла при помощи того же приложения AssemblingFileTool.  Для обновления прошивки «по-воздуху», используется приложение CH583 OTA Tool, как и в первом случае. Для меня интересен второй способ обновления, который позволяет не терять связь с функциями прошивки пользователя. Рассмотрим команды, которые в нем применяются.

Команды bootloader-a

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

1.     IAP_INFO – команда запроса конфигурации устройства, где 0х84 – ID команды, 0х12 – длина посылки после текущего байта. Вот как выглядит полная команда. 84 12 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 В ответ контроллер отправляет посылку со своей конфигурацией. Например такую: 02 00 60 03 00 00 10 83 00 44 00 20 B8 44 00 20 34 0B 00 20 0х02-второй вариант обновления (с BLE стеком), 0х00036000 = 216к размер памяти под новую прошивку, 0х1000 – размер сегмента памяти, 0х0083 – процессор ch583

2.     IAP_ERASE – команда стеку на стирание сегментов памяти, где 0х81 – ID команды, 0х1000 – начальный адрес сегментов, 0х0025=37 сегментов памяти надо стереть. 81 00 00 01 25 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 В ответ стек отправляет нулевой байт, в случае успеха или ненулевой байт ошибки.

3.     IAP_PROM – команда записи данных во FLASH. Она состоит из заголовка и данных. 0х80 – ID команды, 0хF0 = 240 длина блока данных, 0х1000 – адрес блока памяти. 80 F0 00 01 После этой команды передается блок данных длиной 240 байт.

4.     IAP_VERIFY – команда верификации. Она очень похожа на команду IAP_PROM, отличается только ID заголовком, который равен 0х82.

5.     IAP_END – команда успешного окончания заливки новой прошивки. Вот она: 83 12 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Она похожа на команду IAP_INFO. После её получения выставляется флаг новой прошивки и дается команда перезагрузки процессора.

Со стороны андроида, при обновление прошивки «по воздуху», мы наблюдаем как сначала программа прошивается в контроллер, затем верифицируется и только после этого мы получаем надпись update success! На этом обновление прошивки закончено.

Рис.12 Программа CH583 OTA Tool на андроиде

Рис.12 Программа CH583 OTA Tool на андроиде

Режим Central

Среди примеров использования контроллера в BLE режиме, есть такие, когда чип будет играть роль центрального устройства. Т.е. будет работать как телефон сканируя эфир и присоединяя к себе периферийные устройства. Самым простым примером является проект Central. При сканировании, программа заполняет внутренний буфер из 10 ячеек МАС адресами найденных устройств. Потом ищет среди них устройство с заданным адресом. Если такое устройство нашлось, контроллер присоединяет к себе периферическое устройство и начинает запрашивать у него сервисы, характеристики и дескрипторы.

По умолчанию он ищет такие атрибуты: сервис Simple Profile Service UUID = 0xFFE0, а среди характеристик Key Pressed UUID = 0xFFE1, и затем уже CCCD = 0x2902. Это очень удачный пример. Дело в том, что у антипотеряшки iTag, которую я упоминал в прошлой статье, имеются сервисы и характеристики с такими номерами. Вот они в программе nRF Connect.

Рис.13  Антипотеряшка iTag

Рис.13 Антипотеряшка iTag

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

Connected…
Found Profile Service handle : 10 ~ 26
Found Characteristic 1 handle : 12
Found client characteristic configuration handle : 13

Мы видим, что все три атрибута найдены. И тут в пору кричать «Ура!!!», но как говорит один мой хороший знакомый — «Да, но нет» 🙂 Дело в том, что функция запроса атрибутов в стеке WCH сделана откровенно плохо. Возможно я в чем то не разобрался, но у меня она ведет себя не предсказуемо. Может на каких то устройствах выдать всё что положено, а на других не показывает ничего. Тут надо уточнить, что в стеке имеется целый набор команд для устройства Central. Мы можем запрашивать периферийное устройство на наличие у него конкретного сервиса или характеристики (по номеру UUID), так и запрашивать все сервисы или характеристики из диапазона адресов. В приведенном выше примере запрашиваются конкретные UUID атрибутов. Из ответа видно, что сервис занимает диапазон адресов от 0х0010 до 0х0026, а характеристика имеет адрес 0х0012. Следом за ней с адресом 0х0013 идет дескриптор. Именно по этим адресам мы будем обращаться, когда захотим что то прочитать или записать в периферийное устройство. Сервисы запрашиваются так:

GATT_DiscAllPrimaryServices()            обнаружение  всех  сервисов устройства GATT_DiscPrimaryServiceByUUID()     обнаружение  конкретного  сервиса  по  UUID

Если же мы теперь запросим у устройства iTag все сервисы, то получим следующее:

Connected…
Found Profile Service handle : 1 ~ 4
Found Profile Service handle : c ~ f
Found Profile Service handle : 2d ~ ffff
Found Profile Service handle : 206f ~ 7c0

Что смущает в ответе. Во-первых, всегда в списке сервисов, в самом конце, присутствует невалидный сервис. У нас он имеет диапазон адресов 206f ~ 7c0. Я так и не понял что это. Во-вторых, кроме него, найдено всего три сервиса. А на рисунке 13, в центре, мы видим что имеется целых 6 сервисов. Кроме того, если запросить наличие характеристик из всего диапазона адресов, стек не выдаст нам вообще ничего. И это совсем не понятно. Команды для запросов характеристик следующие:

GATT_DiscAllChars()                         обнаружение  всех  характеристик  из  диапазона GATT_DiscCharsByUUID()                 обнаружение  характеристик  по  UUID     

Но есть и хорошие новости. Если вам известны адреса характеристик, то при обращении к ним стек отрабатывает чтение или запись вполне стабильно. В моем случае устройство с функцией Central, ищет в эфире другое моё устройство с функцией Peripheral. Ну а так как я на периферическом устройстве сам формирую таблицу атрибутов, то адреса всех характеристик мне известны. Команды для чтения и записи характеристик следующие:

GATT_ReadCharValue()                    чтение значения характеристики по указателю  GATT_WriteCharValue()                    запись значения характеристики по   указателю

Ну и напоследок несколько строк о флагах стека. При любом изменении своего состояния стек генерируется событие Central_ProcessEvent(). Разбор производится внутри программы пользователя. Основными событиями являются: 

  • старт задачи пользователя (tmos_start_task)

  • старт устройства, потеря связи и обновление параметров

  • чтение уровня RSSI присоединенного устройства

  • передача сообщения *pMsg  из стека в приложение

    Сообщения *pMsg  в свою очередь бывают следующими:

  • получение запроса о сервисах и характеристиках

  • подтверждение записи данных в характеристику

  • получение запрошенных данных из характеристики

  • получение данных нотификации

Что бы правильно отрабатывать события и сообщения, необходимо научится их правильно читать. В файле CH58xBLE_LIB.H описаны команды обращения к стеку. Там же описано какие флаги выставляются при соответствующих событиях и сообщениях.

Заключение

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

Печерских Владимир

Сотрудник Группы Компаний «Цезарь Сателлит»


ссылка на оригинал статьи https://habr.com/ru/articles/729818/

Как не потерять аккредитацию в 2023 году?

До 1 июня у ИТ-компаний есть возможность получить переаккредитацию. Разбираемся, как правильно подать заявку.

Оглавление:

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

  2. Критерии, которые проверять не будут;

  3. Критерии, которые проверять будут;

  4. Кто обладает иммунитетом от проверки;

  5. Сроки;

  6. Выводы.

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

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

  • Отсрочка от срочной службы и мобилизации для сотрудников;

  • Льготная ипотека для сотрудников;

  • Уменьшение страховых взносов за сотрудников с 30% до 7,6% и уменьшение налога на прибыль с 20% до 0%. ;

  • Во многих регионах введены льготные ставки по УСН. Для УСН (доходы) 1% вместо 6%, а для УСН (доходы — расходы) 5% вместо 15%;

  • и др.

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

Важно! Форма будет активна до 01.06.23 включительно. Не подали до 01.06 — потеряли аккредитацию. Лучше подать заявление с неточностями и потом доуточнить сведения. Будет возможность корректировки данных. Если есть несоответствия, Минцифры предварительно проинформируют. 

  1. Критерии, которые проверять не будут

  • ОКВЭДы, 

  • задолженности по налогам, 

  • судимость руководителя.

  1. Критерии, которые проверять будут

  • Наличие согласия на раскрытие сведений, составляющих налоговую тайну

Минцирфы создали бот для проверки статуса вашего согласия: https://t.me/it_agreement_check_bot 

Если срок действия согласия до конца 2023 г., то лучше до конца года подать новое согласие  (инструкция)

До 01.06 от Минцифр придут рассылки всем, у кого какие-то проблемы с согласиями.

  • Средний уровень з/п ( пп.б п.4 Постановления №1729)

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

Если зарплата не соответствует уровню при подтверждении, то слетит аккредитация?  — Да! Слетит. Это обязательное условие (кроме тех, кто использует льготы и тех, у кого есть ПО в реестре и выручка от его продажи).

При расчете в Калькуляторе на ГУ Минцифры сравнивали наименьший показатель (либо с РФ, либо с регионом регистрации). Если в Калькуляторе видно, что у вас есть зп ниже среднего, можно доплатить премии и уточнить РСВ, и ,как следствие, скорректировать бухгалтерскую отчетность. Довыплатить премии и сдать уточненные РСВ можно до 01.06.

Как происходит проверка средней ЗП по периодам? — При плановой проверке в июле будут анализироваться данные компании за 4 квартал 2022 года и 1 квартал 2023 года и сравниваться с данными Росстата соответственно за 3 квартал 2022 года и 4 квартал 2022 года.

  • Доход (пп.в п. 4 Постановления №1729):

Доход от ИТ-деятельности должен составлять не менее 30% от общей выручки. Проверяется выручка компании в течение года, предшествующего дате проведения проверки. Если компания создана в том же году, в котором проверяется, то проверяют текущий год

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

Если у стартапа пока нет выручки, то все равно проходят подтверждение. М.Шадаев не пояснил, что нужно указывать в справке о доходах в этом случае. Вероятно: 0 руб. 

  • Сайт (пп. б п.4 Постановления №1729)

Должна быть информация о компании (реквизиты), о ее ит-деятельности, о регистрации в РРПО (при наличии).

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

Если 1 сайт используется для 2 ваших компаний, то это допустимо, но должна быть понятная консолидация и разделение информации под каждую компанию

Исключение:

Есть ПО, зарегистрированное в российском реестре

если доход от ПО  есть,

подтверждаем доход

Если дохода от ПО нет,

подтверждаем средний уровень зп

  1. Кто обладает иммунитетом от проверки (п.27 Постановления №1729)

Ничего делать не нужно тем, кто применял налоговые ит-льготы в 1 квартале 2023. Хотя подтверждать аккредетацию не нужно, согласие о раскрытии налоговой тайны все равно должно быть направлено.

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

  1. Сроки

В любом случае от Минцифр будет фидбек: подтверждена или не подтверждена аккредитация

До 1 июня — оповещение от Минцифр в случае проблем с соглашением о налоговой тайне.

10 июня — отбивка по формальным критериям (все, кроме сайта), но это еще не окончательное решение. Далее могут быть запросы с уточнениями от Минцифр.

До 1 июля — комментарий Минцифр по сайту.

  1. Выводы:

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

  • При плановой проверке в июле будут анализироваться данные компании о з/п за 4 квартал 2022 года и 1 квартал 2023 года

  • Если получаете доход от ПО, зарегистрированного в реестре, то з/п проверять не будут, но доход подтвердить надо. Вы рассчитываете эти показатели сами, вводите данные в форму на Госуслугах.

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

Статью подготовили:

Герман Ященко

Партнер команды AG-Legal

Лилия Князева

Юрист команды AG-Legal


ссылка на оригинал статьи https://habr.com/ru/articles/737980/

Полное руководство по логированию в Node.js с помощью Pino

Это перевод статьи «A Complete Guide to Pino Logging in Node.js», опубликованной 14 апреля 2023-го года Better Stack Team. Но сперва предисловие переводчика.

Предисловие переводчика

Почему именно Pino? Об этом можно посмотреть видео основателя проекта, Маттео Коллина (Matteo Collina), «The Cost of Logging» о цене, которую приходится платить многопользовательским приложениям за кажущуюся совершенно безобидной функцию ведения логов. Суть его выступления в том, что при выборе медленного инструмента логирования и увеличении количества запросов в секунду, оверхед на запись логов становится неприлично высоким. Каждую секунду приложение может тратить большую часть времени только на запись логов.

Чтобы справиться с этой проблемой, и был создан Pino, асинхронный JSON-логгер. В первую очередь именно его асинхронность и позволяет ему быть очень быстрым, создавая минимальный оверхед в системе. Также для максимального ускорения были применены и некоторые другие приёмы, о которых упоминается в видео.

Кроме того, этот логгер является встроенным в набирающем популярность веб-фреймворке Fastify, ведущим разработчиком которого является всё тот же Маттео Коллина. Fastify был создан со всё той же целью — минимизировать оверхед для Node.js-приложений.

Далее перевод оригинальной статьи от Better Stack Team.

О Pino

Pino — это мощная платформа ведения логов для Node.js, обладающая молниеносной скоростью и широкими возможностями. Фактически именно скорость Pino и завоевала ему место логгера по-умолчанию в open-source веб-сервере Fastify. Также Pino способен очень просто интегрироваться с другими Node.js-фреймворками, что делает его лучшим выбором для всех разработчиков, ищущих надёжное и гибкое решение для ведения логов.

В Pino есть все стандартные возможности, которые ожидаются от логгера [кроме разве что ротации логов, которая отдана на откуп внешним приложениям, таким как logrotate — прим. переводчика], такие как настраиваемые уровни логирования, настройки форматирования и всевозможные виды транспортов.

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

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

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

Предварительные требования

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

Начало работы с Pino

Чтобы получить от данного пособия максимум и протестировать все возможности Pino, обсуждаемые далее, создайте новое Node.js-приложение. Для этого выполните следующие команды:

mkdir pino-logging && cd pino-logging npm init -y

После этого установите последнюю версию Pino с помощью команды ниже. Примеры, приведённые в статье, совместимы с Pino версии 8, последней на момент написания статьи.

npm install pino

Создайте файл logger.js в корневой папке проекта и наполните его следующим содержимым:

// logger.js const pino = require('pino');  module.exports = pino({});

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

//index.js const logger = require('./logger');  logger.info('Привет, мир!');

После того, как сохраните файл, запустите его командой:

node index.js

Вы должны увидеть следующий вывод:

{"level":30,"time":1677506333497,"pid":39977,"hostname":"fedora","msg":"Привет, мир!"}

Первое, что можно заметить — вывод структурирован в формате JSON, в доминирующем формате промышленного структурированного логирования. Кроме самого сообщения в записи лога присутствуют следующие поля:

  • Поле level означает уровень серьёзности события.

  • Поле time содержит время в миллисекундах, прошедшее, начиная с 01.01.1970 00:00:00 UTC.

  • Поле hostname означает имя машины, на которой запущен процесс.

  • Поле pid означает ID запущенного процесса.

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

Форматированный вывод JSON-логов на этапе разработки

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

1. Использовать jq

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

node index.js | jq

Вывод:

{   "level": 30,   "time": 1677669391146,   "pid": 557812,   "hostname": "fedora",   "msg": "Привет, мир!" }

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

node index.js | jq 'del(.time,.hostname,.pid)'

Результат:

{   "level": 30,   "msg": "Привет, мир!" }

Также можно использовать белый список полей, который может быть полезен ещё и в случае, если нужно поменять поля местами:

node index.js | jq '{msg,level}'

Вывод:

{   "msg": "Привет, мир!",   "level": 30 }

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

2. Использовать pino-pretty

Команда разработки Pino создала аналогичный jq инструмент — pino-pretty — для преобразования строк JSON в удобочитаемый человеком обычный текст.

Установка pino-pretty:

npm install pino-pretty --save-dev

Передача JSON-логов в pino-pretty: 

node index.js | npx pino-pretty

Вы заметите, что логи сменили свой формат, обрели цвет и стали более удобочитаемыми:

[12:33:00.352] INFO (579951): Привет, мир!

Если хотите изменить форматирование вывода, обратитесь к соответствующей документации по транспорту pino-pretty.

Уровни логирования

В Pino уровни логирования по-умолчанию это (по возрастанию серьёзности): tracedebuginfowarnerror, и fatal. Для каждого из них существует соответствующий метод логгера:

// index.js const logger = require('./logger');  logger.fatal('fatal'); logger.error('error'); logger.warn('warn'); logger.info('info'); logger.debug('debug'); logger.trace('trace');

После запуска index.js должен получиться следующий результат:

{"level":60,"time":1643664517737,"pid":20047,"hostname":"fedora","msg":"fatal"} {"level":50,"time":1643664517738,"pid":20047,"hostname":"fedora","msg":"error"} {"level":40,"time":1643664517738,"pid":20047,"hostname":"fedora","msg":"warn"} {"level":30,"time":1643664517738,"pid":20047,"hostname":"fedora","msg":"info"}

Обратите внимание, что поле level представлено в виде числа, увеличивающегося на 10 каждый раз с повышением уровня серьёзности события. Также вы можете обнаружить, что в списке событий нет записей, сгенерированных функциями debug() и trace() — это потому, что по-умолчанию минимальным уровнем вывода в Pino является info, который подавляет вывод событий более низкого уровня.

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

// logger.js const pino = require('pino');  module.exports = pino({   level: process.env.PINO_LOG_LEVEL || 'info', });

В данном примере, если установлена переменная окружения PINO_LOG_LEVEL, будет использоваться её значение, а иначе уровень логирования по-умолчанию будет установлен в значение info. В примере ниже минимальный уровень логирования устанавливается в error, что значит, что все события ниже этого уровня будут проигнорированы:

PINO_LOG_LEVEL=error node index.js

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

set PINO_LOG_LEVEL=error node index.js

Вывод:

{"level":60,"time":1643665426792,"pid":22663,"hostname":"fedora","msg":"fatal"} {"level":50,"time":1643665426793,"pid":22663,"hostname":"fedora","msg":"error"}

Изменить минимальный уровень логирования можно в любом месте приложения через объект logger и его свойство level:

// index.js const logger = require('./logger');  logger.level = 'debug'; // после этого будут отбрасываться только сообщения                          // уровня trace . . .

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

app.get('/changeLevel', (req, res) => {   const { level } = req.body;   // проверить, что уровень логирования задан, затем изменить его   logger.level = level; });

Настройка уровней логирования

Вы не ограничены теми уровнями логирования, которые Pino предоставляет по-умолчанию. Можно легко добавить любые другие уровни, создав объект, содержащий номера приоритетов для каждого из уровней, и присвоив его свойству customLevels логгера. Например, в коде ниже добавляется уровень notice, более серьёзный, нежели info, но менее серьёзный, нежели warn

// logger.js const pino = require('pino');  const levels = {   notice: 35, // Подойдёт любое число между 30 (info) и 40 (warn) };  module.exports = pino({   level: process.env.PINO_LOG_LEVEL || 'info',   customLevels: levels, });

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

// index.js const logger = require('./logger');  logger.warn('warn'); logger.notice('notice'); logger.info('info');

Результат:

{"level":40,"time":1678192423827,"pid":122107,"hostname":"fedora","msg":"warn"} {"level":35,"time":1678192423828,"pid":122107,"hostname":"fedora","msg":"notice"} {"level":30,"time":1678192423828,"pid":122107,"hostname":"fedora","msg":"info"}

Если вы хотите полностью заменить уровни Pino, например, стандартными уровнями Syslog, добавьте опцию useOnlyCustomLevels:

// logger.js const pino = require('pino');  const levels = {   emerg: 80,   alert: 70,   crit: 60,   error: 50,   warn: 40,   notice: 30,   info: 20,   debug: 10, };  module.exports = pino({   level: process.env.PINO_LOG_LEVEL || 'info',   customLevels: levels,   useOnlyCustomLevels: true, });

Настройка полей, присутствующих по-умолчанию

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

Использование строковых названий уровней логирования

Зададим какому-нибудь уровню логирования строковое имя вместо его числового значения. Сделать это можно с помощью опции formatters:

// logger.js . . . module.exports = pino({ level: process.env.PINO_LOG_LEVEL || 'info',   formatters: {     level: (label) => {       return { level: label.toUpperCase() };     },   }, });

Код выше переименовывает уровни и меняет регистр их новых строковых имён на верхний:

{"level":"ERROR","time":1677673626066,"pid":636012,"hostname":"fedora","msg":"error"} {"level":"WARN","time":1677673626066,"pid":636012,"hostname":"fedora","msg":"warn"} {"level":"INFO","time":1677673626066,"pid":636012,"hostname":"fedora","msg":"info"}

Также можно переименовать само название свойства level на что-нибудь возвращаемое произвольной функцией:

// logger.js module.exports = pinoLogger({   level: process.env.PINO_LOG_LEVEL || 'info',   formatters: {     level: (label) => {       return { severity: label.toUpperCase() };     },   }, });

Вывод:

{"severity":"ERROR","time":1677676496547,"pid":693683,"hostname":"fedora","msg":"error"} {"severity":"WARN","time":1677676496547,"pid":693683,"hostname":"fedora","msg":"warn"} {"severity":"INFO","time":1677676496547,"pid":693683,"hostname":"fedora","msg":"info"}

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

По-умолчанию, временная метка создаётся в формате количества миллисекунд, прошедших с 01.01.1970 00:00:00 UTC (как в выводе функции Date.now()). Вы можете переопределить это поле (timestamp) при создании экземпляра логгера. Мы рекомендуем выводить временные метки в формате ISO-8601:

// logger.js const pino = require('pino');  module.exports = pino({   level: process.env.PINO_LOG_LEVEL || 'info',   formatters: {     level: (label) => {       return { level: label.toUpperCase() };     },   },   timestamp: pino.stdTimeFunctions.isoTime, });

Результат:

{"level":"INFO","time":"2023-03-01T12:36:14.170Z","pid":650073,"hostname":"fedora","msg":"info"}

Также вы можете переименовать свойство time в timestamp с помощью функции, которая возвращает частичное JSON-представление текущего времени (предваряемое запятой) как в примере ниже:

pino({   timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, })

Вывод:

{"label":"INFO","timestamp":"2023-03-01T13:19:10.018Z","pid":698279,"hostname":"fedora","msg":"info"}

Изменение встроенных полей лога

В Pino существуют ещё два встроенных поля, которые по-умолчанию добавляются к каждой записи лога: ID процесса (pid) и имя хоста (hostname). Вы можете переопределить их с помощью функции bindings в разделе formatters. Например, давайте переименуем hostname в host:

// logger.js const pino = require('pino');  module.exports = pino({   level: process.env.PINO_LOG_LEVEL || 'info',   formatters: {     bindings: (bindings) => {       return { pid: bindings.pid, host: bindings.hostname };     },     level: (label) => {       return { level: label.toUpperCase() };     },   },   timestamp: pino.stdTimeFunctions.isoTime, });

Вывод:

{"level":"INFO","time":"2023-03-01T13:24:28.276Z","process_id":707519,"host":"fedora","msg":"info"}

Вы можете удалить любые встроенные поля из возвращаемого объекта, а также добавить любые поля по вашему желанию — и они будут присутствовать в каждой записи лога. Ниже приведён пример, в котором в каждую запись лога добавляется версия Node.js, используемой для запуска приложения:

bindings: (bindings) => {   return {     pid: bindings.pid,     host: bindings.hostname,     node_version: process.version,   }; },

Вывод:

{"level":"INFO","time":"2023-03-01T13:31:28.940Z","pid":719462,"host":"fedora","node_version":"v18.14.0","msg":"info"}

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

Добавление контекстуальных данных

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

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

В Pino основным способом добавления контекстуальных данных является определение параметра mergingObject в вызываемом методе логгера:

logger.error(   { transaction_id: '12343_ff', user_id: 'ivanov' },   'Транзакция провалилась' );

Фрагмент выше породит следующий результат:

{"level":"ERROR","time":"2023-03-01T13:47:00.302Z","pid":737430,"hostname":"fedora","transaction_id":"12343_ff","user_id":"ivanov","msg":"Транзакция провалилась"}

Бывает также очень полезным вставлять контекстуальные данные во все логи, создаваемые той или иной функцией, модулем или сервисом, чтобы не повторять их при каждом вызове логгера. В Pino это делается при помощи дочерних логгеров (child loggers):

// index.js const logger = require('./logger');  logger.info('Запуск программы');  function getUser(userID) {   const childLogger = logger.child({ userID });   childLogger.trace('getUser запущен');      // получить данные пользователя и вернуть их из функции      childLogger.trace('getUser завершён'); }  getUser('ivanov');  logger.info('Завершение программы');

Выполните этот код с установленным минимальным уровнем логирования trace:

PINO_LOG_LEVEL=trace node index.js

Примечание переводчика. Для Windows-систем команды будут следующими:

set PINO_LOG_LEVEL=trace  node index.js

Результат:

{"level":"INFO","time":"2023-03-01T14:15:47.168Z","pid":764167,"hostname":"fedora","msg":"Запуск программы"} {"level":"TRACE","time":"2023-03-01T14:15:47.169Z","pid":764167,"hostname":"fedora","userID":"ivanov","msg":"getUser запущен"} {"level":"TRACE","time":"2023-03-01T14:15:47.169Z","pid":764167,"hostname":"fedora","userID":"ivanov","msg":"getUser завершён"} {"level":"INFO","time":"2023-03-01T14:15:47.169Z","pid":764167,"hostname":"fedora","msg":"Завершение программы"}

Обратите внимание, что свойство userID присутствует только внутри контекста функции getUser(). Использование дочерних логгеров позволяет добавлять контекстуальные данные без добавления их в самой точке логирования. Также это позволяет проще фильтровать и анализировать логи, основываясь на таких данных как ID пользователя, имя функции или других сопутствующих деталях.

Логирование ошибок

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

Метод error() логгера может в качестве первого аргумента принять объект ошибки. А сообщение об ошибке в этом случае станет вторым аргументом:

// index.js const logger = require('./logger');  function alwaysThrowError() {   throw new Error('обработка ошибки'); }  try {   alwaysThrowError(); } catch (err) {   logger.error(err, 'Возникла непредвиденная ошибка в процессе обработки запроса'); }

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

{   "level": "ERROR",   "time": "2023-03-01T14:28:17.821Z",   "pid": 781077,   "hostname": "fedora",   "err": {     "type": "Error",     "message": "обработка ошибки",     "stack": "Error: обработка ошибки\n    at alwaysThrowError (/home/ayo/dev/betterstack/community/demo/pino-logging/main.js:4:9)\n    at Object.<anonymous> (/home/ayo/dev/betterstack/community/demo/pino-logging/main.js:8:3)\n    at Module._compile (node:internal/modules/cjs/loader:1226:14)\n    at Module._extensions..js (node:internal/modules/cjs/loader:1280:10)\n    at Module.load (node:internal/modules/cjs/loader:1089:32)\n    at Module._load (node:internal/modules/cjs/loader:930:12)\n    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)\n    at node:internal/main/run_main_module:23:47"   },   "msg": "Возникла непредвиденная ошибка в процессе обработки запроса" }

Обработка неотловленных исключений и необработанных отказов промисов

В Pino нет специального механизма для логирования неотловленных исключений или отказов промисов, поэтому вы должны самостоятельно прослушивать события uncaughtException и unhandledRejection и ещё до выхода из приложения логировать эти исключения с уровнем FATAL (после попытки мягкого завершения): 

process.on('uncaughtException', (err) => {   // залогировать исключение   logger.fatal(err, 'обнаружено неотловленное исключение');      // мягко завершить работу сервера      server.close(() => {     process.exit(1); // затем выйти   });    // если не удалось завершить работу мягко в течение 1 секунды,    // завершить процесс полностью   setTimeout(() => {     process.abort(); // выйти немедленно и создать файл дампа   }, 1000).unref()      process.exit(1); }); 

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

Транспорты

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

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

Ниже приведён пример использования встроенного транспорта pino/file, который перенаправляет логи в файл (или файловый дескриптор):

// logger.js const pino = require('pino');  const fileTransport = pino.transport({   target: 'pino/file',   options: { destination: `${__dirname}/app.log` }, });  module.exports = pino(   {     level: process.env.PINO_LOG_LEVEL || 'info',     formatters: {       level: (label) => {         return { level: label.toUpperCase() };       },     },     timestamp: pino.stdTimeFunctions.isoTime,   },    fileTransport );

Другой способ записать логи в файл (или файловый дескриптор) заключается в использовании pino.destination() API:

// logger.js const pino = require('pino');  module.exports = pino(   {     level: process.env.PINO_LOG_LEVEL || 'info',     formatters: {       level: (label) => {         return { level: label.toUpperCase() };       },     },     timestamp: pino.stdTimeFunctions.isoTime,   },    pino.destination(`${__dirname}/app.log`) );

Обратите внимание, что транспорт pino/file под капотом использует pino.destination(). Основная разница между ними в том, что первый выполняется в отдельном воркере, а второй — в основном потоке. Если логировать только в стандартный вывод или локальные файлы, то использование pino/file может внести некоторый оверхед, так как сперва данные должны быть вынесены из основного потока. Возможно, в таких случаях стоит использовать именно pino.destination(). Pino/file рекомендуется использовать только в случае логирования сразу в несколько точек назначения, например, и в файл и в стороннюю службу управления логами.

Кроме того, Pino поддерживает «устаревшие транспорты», которые выполняются в отдельном от Node.js процессе. Для бо́льших подробностей обратитесь к соответствующей документации.

Логирование в несколько точек назначения

Запись логов одновременно в несколько точек назначения очень распространено, и в транспортах Pino начиная с 7-й версии эта возможность также поддерживается. Для этого вам необходимо создать массив targets и поместить все объекты транспортов внутрь него:

// logger.js const pino = require('pino');  const transport = pino.transport({   targets: [     {       target: 'pino/file',       options: { destination: `${__dirname}/app.log` },     },     {       target: 'pino/file', // по-умолчанию логирует в стандартный вывод     },   ], });  module.exports = pino(   {     level: process.env.PINO_LOG_LEVEL || 'info',     timestamp: pino.stdTimeFunctions.isoTime,   },    transport );

Выше приведена настройка Pino на логирование одновременно и в стандартный вывод и в файл app.log. Обратите внимание, что функцию formatters.level невозможно использовать при логировании во множественные точки назначения, поэтому она и не была использована в коде выше. Если всё же попытаться её использовать, то произойдёт следующая ошибка: 

Error: option.transport.targets do not allow custom level formatters

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

const transport = pino.transport({   targets: [     {       target: 'pino/file',       options: { destination: `${__dirname}/app.log` },     },     {       target: 'pino-pretty',     },   ], }); 

Запуск:

node index.js && echo $'\n' && cat app.log

Примечание переводчика. Для Windows-систем команда запуска будет следующей:

node index.js && echo: && type app.log

Вывод:

[14:33:41.932] INFO (259060): info [14:33:41.933] ERROR (259060): error [14:33:41.933] FATAL (259060): fatal {"level":30,"time":"2023-03-03T13:33:41.932Z","pid":259060,"hostname":"fedora","msg":"info"} {"level":50,"time":"2023-03-03T13:33:41.933Z","pid":259060,"hostname":"fedora","msg":"error"} {"level":60,"time":"2023-03-03T13:33:41.933Z","pid":259060,"hostname":"fedora","msg":"fatal"}

Конфиденциальность при логировании

Одна из важнейших лучших практик при логировании — это контроль над утечкой в логи конфиденциальной информации. Примерами таких данных могут быть:

  • Финансовые данные, такие как номера банковских карт, пин-коды, номера банковских счетов и т. д.

  • Пароли или другие секретные данные приложения.

  • Любые данные, которые могут помочь идентифицировать личность, такие как адреса электронной почты, имена, телефонные номера, почтовые адреса, идентификационные номера и так далее.

  • Медицинские записи.

  • Данные биометрии и прочее.

Запись чувствительной информации в логи может привести к утечкам данных, краже личности, несанкционированному доступу или другим зловредным действиям, которые могут полностью разрушить репутацию вашего бизнеса. Также это может привести к штрафам и другим санкциям в отношении вашего бизнеса от таких органов как GDPR, PCI и HIPPA [европейские и американские регуляторы в области защиты конфиденциальной информации — примечание переводчика]. Чтобы предотвратить подобные происшествия, необходимо всегда проверять ваши логи с целью предотвращения случайного попадания в них чувствительных данных.

Существует несколько практик, которые помогут вам не допустить попадания такой информации в логи. Их все невозможно обсудить в рамках данной статьи, поэтому мы сосредоточимся только на практике редактирования логов — технике выявления и удаления конфиденциальных данных с сохранением информации, необходимой для диагностики или анализа. В Pino для реализации этой функции используется npm-пакет fast-redact.

Например, у нас есть объект user со следующей структурой:

const user = {   id: 'ivanov',   name: 'Иван Иванов',   address: 'Улица Сезам, 123',   passport: {     number: '123 456',     issued: 2023,     expires: 2027,   },   phone: '123-456-789', };

Если залогировать этот объект как есть, то он раскроет такие чувствительные данные как имя пользователя, его адрес, паспортные данные и телефонный номер: 

logger.info({ user }, 'Пользователь обновлён');

Вывод:

{   "level": "info",   "time": 1677660968266,   "pid": 377737,   "hostname": "fedora",   "user": {     "id": "ivanov",     "name": "Иван Иванов",     "address": "Улица Сезам, 123",     "passport": {       "number": "123 456",       "issued": 2023,       "expires": 2027     },     "phone": "123-456-789"   },   "msg": "Пользователь обновлён" }

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

// logger.js const pino = require('pino');  module.exports = pino({   level: process.env.PINO_LOG_LEVEL || 'info',   formatters: {     level: (label) => {       return { level: label };     },   },   redact: ['user.name', 'user.address', 'user.passport', 'user.phone'], });

Опция redact определяет массив полей, данные которых должны вырезаться при записи логов. Данный пример приведёт к тому, что из логов будут вырезаны данные полей name, address, passport и phone в любом объекте user. Вместо них будет отображена надпись [Redacted]. В результате в логах можно будет прочитать только значение поля id

{   "level": "info",   "time": 1677662887561,   "pid": 406515,   "hostname": "fedora",   "user": {     "id": "ivanov",     "name": "[Redacted]",     "address": "[Redacted]",     "passport": "[Redacted]",     "phone": "[Redacted]"   },   "msg": "Пользователь обновлён" }

Вы можете изменить надпись-заполнитель с помощью следующей настройки:

module.exports = pino({   redact: {     paths: ['user.name', 'user.address', 'user.passport', 'user.phone'],     censor: '[ТАЙНА]',   }, });

Вывод:

{   "level": "info",   "time": 1677663111963,   "pid": 415221,   "hostname": "fedora",   "user": {     "id": "ivanov",     "name": "[ТАЙНА]",     "address": "[ТАЙНА]",     "passport": "[ТАЙНА]",     "phone": "[ТАЙНА]"   },   "msg": "Пользователь обновлён" }

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

module.exports = pino({   redact: {     paths: ['user.name', 'user.address', 'user.passport', 'user.phone'],     censor: '[ТАЙНА]',     remove: true,   }, });

Результат:

{   "level": "info",   "time": 1677663213497,   "pid": 419647,   "hostname": "fedora",   "user": {     "id": "ivanov"   },   "msg": "Пользователь обновлён" }

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

// здесь наш фильтр сработает logger.info({ user }, 'Пользователь обновлён'); // а здесь не сработает logger.info({ nested: { user } }, 'Пользователь обновлён'); logger.info(user, 'Пользователь обновлён');

Чтобы избежать этого, обновим фильтр:

module.exports = pino({   redact: {     paths: [       'name',       'address',       'passport',       'phone',       'user.name',       'user.address',       'user.passport',       'user.phone',       '*.user.name', // * — шаблон с уровнем вложенности 1       '*.user.address',       '*.user.passport',       '*.user.phone',     ],     remove: true,   }, });

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

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

Логирование HTTP-запросов

Pino можно использовать для логирования HTTP-запросов в любом Node.js-приложении вне зависимости от того, каким фреймворком вы пользуетесь. Пользователи Fastify уже могли заметить, что Pino встроен в этот фреймворк. По-умолчанию отключён, поэтому сперва его нужно включить:

const fastify = require('fastify')({   logger: true })

После этого Pino начнёт логировать все входящие запросы в следующем виде:

{"level":30,"time":1675961032671,"pid":450514,"hostname":"fedora","reqId":"req-1","res":{"statusCode":200},"responseTime":3.1204520016908646,"msg":"запрос обработан"}

Если вы используете другой фреймворк, посетите страницу экосистемы Pino, чтобы найти специфичную для него интеграцию. Пример далее показывает как использовать npm-пакет pino-http, чтобы логировать HTTP-запросы в Express:

// index.js const express = require('express'); const logger = require('./logger'); const axios = require('axios'); const pinoHTTP = require('pino-http');  const app = express();  app.use(   pinoHTTP({     logger,   }) );  app.get('/crypto', async (req, res) => {   try {     const response = await axios.get('https://api2.binance.com/api/v3/ticker/24hr');     <span class="token keyword" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(166, 38, 164);">const</span> tickerPrice <span class="token operator" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(166, 38, 164);">=</span> response<span class="token punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(56, 58, 66);">.</span>data<span class="token punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(56, 58, 66);">;</span>       res     <span class="token punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(56, 58, 66);">.</span><span class="token function" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(64, 120, 242);">json</span><span class="token punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(56, 58, 66);">(</span>tickerPrice<span class="token punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(56, 58, 66);">)</span><span class="token punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(56, 58, 66);">;</span>   } catch (err) {     logger.error(err);     res.status(500).send('Internal Server Error');   } });  app.listen('4000', () => {   console.log('Сервер запущен на порту 4000'); });

Убедитесь, что файл logger.js настроен на логирование одновременно и в консоль и в файл:

// logger.js const pino = require('pino');  const transport = pino.transport({   targets: [     {       target: 'pino/file',       options: { destination: </span><span class="token interpolation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important;"><span class="token interpolation-punctuation punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(202, 18, 67);">${</span>__dirname<span class="token interpolation-punctuation punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(202, 18, 67);">}</span></span><span class="token string" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(80, 161, 79);">/server.log</span><span class="token template-punctuation string" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(80, 161, 79);"> },     },     {       target: 'pino-pretty',     },   ], });  module.exports = pino(   {     level: process.env.PINO_LOG_LEVEL || 'info',     timestamp: pino.stdTimeFunctions.isoTime,   },   transport );

Затем установите все необходимые зависимости командой ниже:

npm install express axios pino-http

Запустите сервер на порту 4000 и с помощью curl сделайте GET-запрос на роут /crypto:

node index.js curl http://localhost:4000/crypto

В консоли сервера вы увидите цветной форматированный вывод логгера по этому HTTP-запросу:

[15:30:54.508] INFO (291881): запрос обработан req: {   "id": 1,   "method": "GET",   "url": "/crypto",   "query": {},   "params": {},   "headers": {     "host": "localhost:4000",     "user-agent": "curl/7.85.0",     "accept": "/"   },   "remoteAddress": "::ffff:127.0.0.1",   "remotePort": 36862 } res: {   "statusCode": 200,   "headers": {     "x-powered-by": "Express",     "content-type": "application/json; charset=utf-8",     "content-length": "1099516",     "etag": "W/"10c6fc-mMUyGYJwdl+yk7A7N/rYiPWqFjo""   } } responseTime: 2848

А файл server.log будет содержать сырой JSON:

cat server.log

Примечание переводчика. Для Windows-систем команда будет следующей:

type server.log

Вывод:

{"level":30,"time":"2023-03-03T14:30:54.508Z","pid":291881,"hostname":"fedora","req":{"id":1,"method":"GET","url":"/crypto","query":{},"params":{},"headers":{"host":"localhost:4000","user-agent":"curl/7.85.0","accept":"/"},"remoteAddress":"::ffff:127.0.0.1","remotePort":36862},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","content-type":"application/json; charset=utf-8","content-length":"1099516","etag":"W/"10c6fc-mMUyGYJwdl+yk7A7N/rYiPWqFjo""}},"responseTime":2848,"msg":"запрос обработан"}

Прочитав документацию по API pino-http вы можете настроить форматирование вывода, как вам нужно.

Централизованный мониторинг логов

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

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

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

Теперь, когда вы настроили Pino в вашем Node.js-приложении, вашим следующим шагом может стать перенаправление логов в систему управления логами. Благодаря этому вы начнёте пожинать плоды структурированного логирования. Logtail — одно из решений, которое управляет логами, анализирует и визуализирует их, а также посылает уведомления в случае обнаружения определённых вами моделей поведения.

Существует несколько способов, с помощью которых можно вывести логи из Node.js-приложения в Logtail. Самый простой из них — использовать соответствующий транспорт Pino:

// logger.js const transport = pino.transport({   targets: [     {       target: 'pino/file',       options: { destination: </span><span class="token interpolation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important;"><span class="token interpolation-punctuation punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(202, 18, 67);">${</span>__dirname<span class="token interpolation-punctuation punctuation" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(202, 18, 67);">}</span></span><span class="token string" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(80, 161, 79);">/app.log</span><span class="token template-punctuation string" style="box-sizing: inherit; border-width: 0px; border-style: solid; border-color: currentcolor; --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; --tw-translate-y: 0; --tw-rotate: 0; --tw-skew-x: 0; --tw-skew-y: 0; --tw-scale-x: 1; --tw-scale-y: 1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: rgb(59 130 246 / 0.5); --tw-ring-offset-shadow: 0 0 #0000; --tw-ring-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.36) 0px 0px 0px !important; display: inline !important; appearance: none !important; color: rgb(80, 161, 79);"> },     },     {       target: '@logtail/pino',       options: { sourceToken: '<your_logtail_source_token>' },     },     {       target: 'pino-pretty',     },   ], });

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

Примечание переводчика. Logtail — это платное решение от Better Stack Team. В качестве бесплатной альтернативы можно использовать Elastic Stack (ELK), в который они приходят через транспорт pino-elasticsearch или Logstash (часть ELK) через транспорт pino-socket. Также можно использовать транспорты и в другие базы данных и инструменты для аналитики, которые с ними работают.

Заключительные мысли и последующие шаги

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

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

Благодарим за внимание и счастливого вам логирования!


ссылка на оригинал статьи https://habr.com/ru/articles/737982/

Автоматическая рассылка сообщений WhatsApp (развитие)

Доброго времени суток всем!

На написание данной статьи меня вдохновила эта публикация.

К сожалению, в этой статье не было способа для отправки файлов в Whatsapp и я решил поправить это.

Итак, необходимо:

  • Python (у меня 3.10.10)

  • библиотеки к нему (полный список их чуть ниже)

  • зарегистрированный номер телефона в Whatsapp

  • Windows компьютер

  • … ну и желание

Теперь обещанный requrements.txt

async-generator==1.10 attrs==23.1.0 certifi==2023.5.7 cffi==1.15.1 charset-normalizer==3.1.0 colorama==0.4.6 exceptiongroup==1.1.1 h11==0.14.0 idna==3.4 outcome==1.2.0 packaging==23.1 PyAutoIt==0.6.5 pycparser==2.21 PySocks==1.7.1 python-dotenv==1.0.0 requests==2.30.0 selenium==4.9.1 sniffio==1.3.0 sortedcontainers==2.4.0 tqdm==4.65.0 trio==0.22.0 trio-websocket==0.10.2 urllib3==2.0.2 webdriver-manager==3.8.6 wsproto==1.2.0

Установка вышеуказанных пакетов, за исключением PyAutoIt, производится через

pip install -r requrements.txt 

и проблем не вызовет.

Установка PyAutoit нужна потому, что Selenium, на момент написания статьи, не может послать текст и команды за пределы браузера. А нужно указать Windows Explorer что за файл подлежит отправке. К слову, AutoIt — прекрасный скриптованный язык для тех, кто хочет автоматизировать работу с приложениями, которые не имеют API. Он запросто может находить, вводить текст и нажимать на кнопочки в windows приложениях.

Поэтому устанавливаем его отдельно.

  1. Скачиваем дистрибутив отсюда

  2. Раскрываем архив, если тащили не через git clone

  3. Переходим в папку pyautoit-master

  4. И запускаем python setup.py install… впрочем в README.md этого пакета написано больше ;).

Ну вот, теперь мы готовы запустить файл whatsapp.py. Вот он

from selenium import webdriver from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.support.ui import WebDriverWait from time import sleep import argparse import autoit  options = webdriver.ChromeOptions() # если у вас другой браузер, например FireFox, мы просто пишем options = webdriver.FirefoxOptions() . # if you have the Mozilla Fifefox just write                   options = webdriver.FirefoxOptions() options.add_argument('--allow-profiles-outside-user-dir') options.add_argument('--enable-profile-shortcut-manager') # УКАЖИТЕ ПУТЬ ГДЕ ЛЕЖИТ ВАШ python ФАЙЛ. Советую создать отдельную папку для него # Specify the path to where your python file is located. I suggest you create a separate folder for it options.add_argument(r'user-data-dir=x:\\yyyyyy\\zzzzzz\\')  options.add_argument('--profile-directory=Profile 1') options.add_argument('--profiling-flush=n') options.add_argument('--enable-aggressive-domstorage-flushing')  # эти опции нужны чтобы подавить любые сообщения об ошибках  SSL, сертификатов и т.п. Но работает только последняя :( # these options need to disabled any messages about bad ssl, certification & etc  options.add_argument('--ignore-certificate-errors-spki-list') options.add_argument('--ignore-certificate-errors') options.add_argument('--ignore-ssl-errors') options.add_argument('log-level=3') # INFO = 0,  # WARNING = 1,  # LOG_ERROR = 2,  # LOG_FATAL = 3. # default is 0.  # Константы для Selenium # Constants for Selenium xpath = "//button[@data-testid='compose-btn-send']" xpathAttach = "//div[@data-testid='conversation-clip']" # xpathFile = "//button[@data-testid='attach-document']" #xpathSendButton = "//div[@data-testid='send']" # data-testid = "send" cssIdOfDocument = "[aria-label='Документ']" cssIdOfSendButton = "[aria-label='Отправить']"  # Константы для AutoIt # Constant for Autoit idExplorerOpen = "[CLASS:#32770]" idInputLine = "[CLASS:Edit; INSTANCE:1]" idOpenButton = "Button1"  # номера телефонов для отправки # numbers of  phones to send numbers = ["+7xxxxxxxxxx", "+7xxxxxxxxxx", "+7xxxxxxxxxx"]  # текст по умолчанию # default text text = "Ничего нет"  # Мы запускаем браузер # We are starting a browser driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) # ждём его загрузки # it takes some time to load it wait = WebDriverWait(driver, 30)   def main(args):     for number in numbers: # создаём url страницы с телефонныи номером и текстом для отправки # create a url page with a phone number and text to send         url = f"https://web.whatsapp.com/send?phone={number}&text={args.text}" # идём туда # go to there         driver.get(url) # ждём загрузки страницы Whatsapp # we are waiting for Whatsapp page to load         wait.until(EC.element_to_be_clickable((By.XPATH, xpath)))          if not (args.file is None) : # нужно отправить файл # ищем кнопку Attach # need to send file # now we look for the Attach Button and click on it            driver.find_element(By.XPATH, xpathAttach).click() # затем ищем кнопку "Документ" и щелкаем на ней. Используется атрибут CCS, потому что у него нет ID # then  we look for the Document Button and click on it. Used CCS attribute because it doesn't have any ID            driver.find_element(By.CSS_SELECTOR, cssIdOfDocument).click()  # подождать, когда активируется окно проводника Windows # wait when the Windows's explorer window to activate            autoit.win_wait_active(idExplorerOpen, 5) # затем отправляем путь к файлу в строку ввода # then we are sending the path of the file to input line            autoit.control_send(idExplorerOpen, idInputLine, args.file) # и нажимаем кнопку "Открыть"  # and are clicking the Open Button             autoit.control_click(idExplorerOpen, idOpenButton) # немного ждём загрузки файла # it takes some time to load a file            sleep(5) # ищем кнопку "Отправить" и нажимаем на нее. Используется атрибут CCS, потому что у него нет ID # we look for the Send Button and click on it. Used CCS attribute because it doesn't have any ID            driver.find_element(By.CSS_SELECTOR, cssIdOfSendButton).click()                     else: # теперь ищем кнопку "Отправить" и нажимаем на нее # now we look for the Send Button and click on it            driver.find_element(By.XPATH, xpath).click() # требуется некоторое время для отправки файла или сообщения # it takes some time to send a file or a message         sleep(5) # закрыть все # close all     driver.quit()   if __name__ == '__main__': # мы разбираем параметры командной строки # we are parsing command line parameters      parser = argparse.ArgumentParser(description='Send information by Whatsapp')      parser.add_argument('--text', help='Text for send', required=False, default = text)      parser.add_argument('--file', help='File for send', required=False)      args = parser.parse_args() # начать отправку # start sending      main(args)     

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

Приложение запускается так

python whatsapp.py --text "Я посылаю тебе ... или тебя ;)" 

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

или так

python --text "Это очень важный файл" --file "x:\yyyyy\zzzzz\fileToSend.ext"

если нужно отправить файл с пояснением

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

Дальше оно обычно запускается и оправляет всё без вопросов.

Соглашусь с автором поста, который меня вдохновил и процитирую его:

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

Всем удачи!


ссылка на оригинал статьи https://habr.com/ru/articles/737990/