Практика: Создание универсального Subflow для логирования
Введение: DRY-подход к логированию в Node-RED
В ходе разработки сложных систем автоматизации на Node-RED инженеры неизбежно сталкиваются с необходимостью протоколирования событий. На начальном этапе это часто решается локально: после важного узла ставится `function`, который формирует сообщение, и `mqtt out` или `file`, который его записывает. Этот подход работает для простых потоков, но в масштабах целого объекта (умный дом, офис) он приводит к хаосу. Десятки, а то и сотни узлов логирования, разбросанных по разным вкладкам, создают серьезные проблемы:
- Дублирование кода: Логика форматирования сообщения копируется из одного узла в другой. Изменение формата лога (например, добавление нового поля) превращается в трудоемкую задачу по поиску и редактированию всех экземпляров.
- Неконсистентность: В одном месте забыли добавить временную метку, в другом перепутали уровень логирования. Это приводит к "грязным" данным в журнале, которые сложно анализировать.
- Сложность поддержки: Найти и исправить ошибку в логике логирования, если она размазана по всему проекту, практически невозможно. Система становится хрупкой и труднообслуживаемой.
> 💡 Подсказка: Принцип DRY — один из столпов профессиональной разработки. Применяя его в Node-RED, вы переходите от уровня "автоматизатора-любителя" к "инженеру систем автоматизации".
Решением этих проблем является применение принципа DRY (Don’t Repeat Yourself) — «не повторяйся». В контексте Node-RED идеальным инструментом для реализации этого принципа является Subflow (субпоток). Мы можем инкапсулировать всю логику, связанную с формированием и отправкой лога, в один переиспользуемый компонент.
Преимущества централизованного подхода через Subflow:
Цель данного урока — спроектировать, создать и внедрить универсальный, настраиваемый и простой в использовании Subflow для централизованного логирования событий в нашей системе автоматизации.
---
Проектирование Subflow: входы, выходы и переменные окружения
Прежде чем приступить к сборке, необходимо тщательно спроектировать наш будущий компонент. Subflow — это "черный ящик". У него должны быть четко определенные входы, выходы и параметры конфигурации, чтобы любой инженер мог легко его использовать, не вникая во внутреннюю реализацию.
Для создания Subflow перейдите в основное меню Node-RED (`≡`) → `Subflows` → `Create Subflow`.
Определение "контракта" Subflow
Наш Subflow будет принимать на вход стандартный объект `msg`. Чтобы сделать его универсальным, мы определим набор необязательных свойств, которые могут быть переданы для кастомизации записи в журнале.
Входной контракт `msg` для Subflow-логгера:| Свойство | Тип | Описание | Пример |
| :----------- | :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- |
| `msg.payload`| `String`, `Object`, `Number` | Обязательно. Основные данные для записи в журнал. Это может быть как простое текстовое сообщение, так и сложный объект (например, ошибка). | `"Дверь в гостиную открыта"` |
| `msg.level` | `String` | Необязательно. Уровень логирования: `debug`, `info`, `warn`, `error`, `critical`. Если не указан, будет использован уровень по умолчанию. | `"warn"` |
| `msg.source_id`| `String` | Необязательно. Идентификатор источника события. Помогает понять, какой сценарий или устройство сгенерировало запись. | `"SCN-LIGHT-012"` или `"SENSOR-LEAK-01"` |
| `msg.error` | `Object` | Специфично. Если Subflow подключен к узлу `Catch`, это свойство будет содержать объект с информацией об ошибке. Наш Subflow должен уметь его обрабатывать. | `{ "message": "Timeout", ... }` |
Входы и выходы
- Входы: Нам понадобится только один вход для приема всех сообщений, предназначенных для логирования.
- Выходы: Мы также сделаем один выход. Через него Subflow будет передавать полностью отформатированный объект `msg`, готовый к отправке в систему хранения логов (например, в узел `mqtt out`). Это дает гибкость: пользователь Subflow сам решает, куда отправлять логи — в MQTT, в MySQL или в файл.
Переменные окружения (Environment Variables)
Чтобы сделать Subflow по-настояшему универсальным и переносимым между проектами, мы вынесем специфичные для окружения настройки в переменные. Это делается во вкладке `Properties` окна редактирования Subflow.
> ℹ️ Информация: Именование переменных окружения в стиле `PROJECT_FEATURE_VARIABLE` (например, `HI_LOGGING_DEFAULT_LEVEL`) помогает избежать конфликтов и делает конфигурацию интуитивно понятной.
Мы определим две переменные:
* Назначение: Задает базовый MQTT-топик для всех логов. Это позволяет легко отделить логи одного объекта от другого.
* Тип: `string`
* Пример значения: `hi/sites/object-101/logs`
* Назначение: Устанавливает уровень логирования по умолчанию, если `msg.level` не был передан во входном сообщении.
* Тип: `string`
* Пример значения: `info`
Для доступа к этим переменным внутри Subflow (например, в узле `Function`) используется функция `env.get()`:
// Пример доступа к переменной окружения внутри узла Function
const defaultLevel = env.get("LOG_DEFAULT_LEVEL"); // вернет "info"
const topicPrefix = env.get("LOG_MQTT_TOPIC_PREFIX"); // вернет "hi/sites/object-101/logs"
Такой подход позволяет одному и тому же Subflow работать в разных проектах, просто меняя значения переменных окружения при его размещении в потоке, без необходимости редактировать его внутренний код.
---
Практика: Сборка логики внутри Subflow
Теперь, когда у нас есть четкий проект, приступим к сборке. Откройте созданный Subflow для редактирования.
> ⚠️ Внимание: Логирование — сквозная операция. Сложная логика внутри Subflow может стать "бутылочным горлышком" производительности. Всегда стремитесь к максимальной эффективности кода внутри часто вызываемых Subflows.
Шаг 1: Добавление узловПеретащите на рабочую область Subflow следующие узлы:
- Один узел `Switch`
- Один узел `Function`
- Один узел `Change`
Соедините вход `Input` Subflow с входом узла `Switch`.
Шаг 2: Настройка узла `Switch` для определения уровняЭтот узел будет выполнять первичную валидацию и маршрутизацию на основе уровня логирования.
Это сердце нашего Subflow. Здесь происходит основная работа по формированию итоговой записи журнала.
// Получаем переменные окружения
const defaultLevel = env.get("LOG_DEFAULT_LEVEL") || "info";
// 1. Обработка специального случая: сообщение от узла Catch
if (msg.error && msg.error.source) {
// Если это ошибка, переопределяем основные поля
msg.level = 'error';
msg.source_id = msg.error.source.name || msg.error.source.id;
// Формируем детальное сообщение об ошибке
msg.payload = {
error_message: msg.error.message,
original_payload: msg.payload // Сохраняем payload, вызвавший ошибку
};
}
// 2. Определяем финальный уровень и источник
const level = msg.level || defaultLevel;
const source = msg.source_id || "unknown";
// 3. Формируем тело записи лога согласно нашему "контракту"
const logEntry = {
ts: Date.now(), // Временная метка в Unix epoch (ms)
level: level, // Уровень события
source: source, // Источник события
message: msg.payload, // Основное сообщение
controller_id: "HI-Core-SN-00123" // Жестко заданный ID или из env
};
// 4. Заменяем msg.payload на готовую запись
msg.payload = logEntry;
// 5. Сохраняем уровень в msg.level для следующего узла
msg.level = level;
return msg;
Этот код:
- Проверяет, не является ли сообщение ошибкой от узла `Catch`. Если да, он извлекает из `msg.error` максимум полезной информации.
- Устанавливает уровень и источник, используя значения по умолчанию, если они не были предоставлены.
- Создает финальный JSON-объект `logEntry`, соответствующий нашему стандарту логирования.
- Полностью заменяет `msg.payload` этим объектом.
- Сохраняет `msg.level` для использования в следующем узле. Нажмите кнопку Done (Готово), чтобы сохранить настройки узла.
Нам нужно, чтобы итоговый MQTT-топик зависел от уровня сообщения (например, `.../logs/info` или `.../logs/error`).
* Set: `msg.topic`
* to: `env.get("LOG_MQTT_TOPIC_PREFIX") & "/" & msg.level` (выберите тип выражения `J: JSONata`).
Соедините выход узла `Function` с входом узла `Change`, а выход узла `Change` — с выходом `Output` нашего Subflow. Дайте Subflow осмысленное имя (например, "HI-Logger-Universal") и добавьте описание его работы и контракта во вкладку "Appearance".
Нажмите кнопку `Deploy` в верхнем меню или вернитесь на основную вкладку рабочих процессов (Workspace) — теперь ваш новый Subflow появится в палитре слева в разделе `subflows`, и вы сможете использовать его как обычный узел. Теперь наш универсальный Subflow полностью готов к использованию.---
Пример: Интеграция логгера в реальный сценарий
Рассмотрим, как использовать созданный Subflow в двух типичных ситуациях: логирование штатной операции и автоматический перехват ошибки.
> 🔗 Ключевой инструмент: Для автоматического перехвата ошибок мы будем использовать специальный узел `Catch`. Он позволяет отлавливать сбои со всех узлов на текущей вкладке и направлять их в нашу систему логирования.
Логирование штатной операции
Предположим, у нас есть сценарий управления освещением в гостиной. Мы хотим записывать в журнал каждое включение и выключение света.
* `LOG_MQTT_TOPIC_PREFIX`: `hi/sites/cottage-1/logs`
* `LOG_DEFAULT_LEVEL`: `info`
Настройте в нем следующие правила для `msg`:
* Set `msg.level` to `info` (string)
* Set `msg.source_id` to `SCN-LIGHT-LIVINGROOM-001` (string)
* Set `msg.payload` to `"Свет в гостиной включен"` (string)
Теперь при каждом включении света (или при симуляции через узел `Inject`) наш Subflow сгенерирует и отправит в MQTT структурированное сообщение.
Пример сообщения в MQTT Explorer (топик `hi/sites/cottage-1/logs/info`):{
"ts": 1678886400000,
"level": "info",
"source": "SCN-LIGHT-LIVINGROOM-001",
"message": "Свет в гостиной включен",
"controller_id": "HI-Core-SN-00123"
}
Автоматическое логирование ошибок
Это самое мощное применение нашего Subflow.
Теперь, если в любом узле этого потока произойдет ошибка (например, узел `modbus-write` не сможет достучаться до устройства и выдаст `Timeout`), узел `Catch` перехватит ее. Он сформирует объект `msg`, содержащий `msg.error`. Этот `msg` попадет в наш Subflow. Логика внутри нашего узла `Function`, которую мы написали ранее, распознает это сообщение как ошибку и автоматически отформатирует его.
Пример ошибки: в узле `modbus-write` (названном "Управление светом") произошел таймаут. Пример сообщения в MQTT Explorer (топик `hi/sites/cottage-1/logs/error`):{
"ts": 1678886520000,
"level": "error",
"source": "Управление светом",
"message": {
"error_message": "PortNotOpenError: Port Not Open",
"original_payload": true
},
"controller_id": "HI-Core-SN-00123"
}
Как видите, мы получили полную картину инцидента: когда он произошел, где (`Управление светом`), в чем причина (`Port Not Open`) и какое сообщение его вызвало (`true`). И все это — без единой дополнительной строчки кода в основном сценарии.
---
Итоги и лучшие практики
В этом уроке мы совершили важный шаг от хаотичного к системному подходу в логировании. Мы прошли полный цикл: от проектирования контракта данных до практической сборки и интеграции узлов перехвата ошибок. Итогом стал готовый, скомпилированный и универсальный Subflow-компонент, который позволяет централизованно управлять форматом и отправкой всех записей журнала в проекте.
Ключевые результаты:- Создан Subflow, реализующий принцип DRY для логирования.
- Subflow является гибко настраиваемым через переменные окружения, что делает его переносимым между проектами.
- Реализована автоматическая обработка штатных событий (`info`) и непредвиденных ошибок, перехваченных узлом `Catch`.
- Все записи журнала приводятся к единому, предсказуемому формату (контракт логов).
Лучшие практики
- Документируйте Subflow: Всегда заполняйте вкладку `Appearance` в настройках Subflow. Опишите его назначение, входной контракт (`msg.payload`, `msg.level` и т.д.), список переменных окружения и примеры использования. Это превращает ваш Subflow в полноценный компонент, понятный любому члену команды.
- Версионируйте: Если вы вносите существенные изменения в логику Subflow, которые ломают обратную совместимость, сохраняйте его под новым именем (например, `HI-Logger-Universal-v2`).
- Экспортируйте и делитесь: Сохраните ваш Subflow в виде отдельного JSON-файла (`Меню → Export → Clipboard/Library`). Создайте корпоративную библиотеку проверенных Subflows — это значительно ускоряет разработку новых проектов и повышает их качество.
Что дальше?
Созданный нами логгер — это мощная основа. Его можно и нужно развивать. Например, можно добавить второй выход, на который будут отправляться только сообщения с уровнем `critical`. Этот выход можно подключить к узлу отправки Telegram-сообщений или email, чтобы обеспечить мгновенное оповещение о самых серьезных сбоях в системе.
В следующих уроках мы будем активно использовать этот Subflow во всех практических заданиях, чтобы наши сценарии были не только функциональными, но и легко отлаживаемыми и поддерживаемыми.