ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Практика: Создание универсального Subflow для логирования

Практика: Создание универсального Subflow для логирования

Урок 6 · Сценарии умного дома: режимы, состояния, приоритеты · 30 мин · theory

Введение: DRY-подход к логированию в Node-RED

В ходе разработки сложных систем автоматизации на Node-RED инженеры неизбежно сталкиваются с необходимостью протоколирования событий. На начальном этапе это часто решается локально: после важного узла ставится `function`, который формирует сообщение, и `mqtt out` или `file`, который его записывает. Этот подход работает для простых потоков, но в масштабах целого объекта (умный дом, офис) он приводит к хаосу. Десятки, а то и сотни узлов логирования, разбросанных по разным вкладкам, создают серьезные проблемы:

> 💡 Подсказка: Принцип DRY — один из столпов профессиональной разработки. Применяя его в Node-RED, вы переходите от уровня "автоматизатора-любителя" к "инженеру систем автоматизации".

Решением этих проблем является применение принципа DRY (Don’t Repeat Yourself) — «не повторяйся». В контексте Node-RED идеальным инструментом для реализации этого принципа является Subflow (субпоток). Мы можем инкапсулировать всю логику, связанную с формированием и отправкой лога, в один переиспользуемый компонент.

Преимущества централизованного подхода через Subflow:

  • Единый формат: Все логи, проходящие через Subflow, гарантированно соответствуют единому, заранее определенному «контракту» (структуре JSON), который мы спроектируем далее в этом уроке.
  • Простота изменений: Нужно добавить ID контроллера в каждую запись журнала? Достаточно отредактировать один узел `function` внутри 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", ... }` |

    Входы и выходы

    Переменные окружения (Environment Variables)

    Чтобы сделать Subflow по-настояшему универсальным и переносимым между проектами, мы вынесем специфичные для окружения настройки в переменные. Это делается во вкладке `Properties` окна редактирования Subflow.

    > ℹ️ Информация: Именование переменных окружения в стиле `PROJECT_FEATURE_VARIABLE` (например, `HI_LOGGING_DEFAULT_LEVEL`) помогает избежать конфликтов и делает конфигурацию интуитивно понятной.

    Мы определим две переменные:

  • `LOG_MQTT_TOPIC_PREFIX`:
  • * Назначение: Задает базовый MQTT-топик для всех логов. Это позволяет легко отделить логи одного объекта от другого.

    * Тип: `string`

    * Пример значения: `hi/sites/object-101/logs`

  • `LOG_DEFAULT_LEVEL`:
  • * Назначение: Устанавливает уровень логирования по умолчанию, если `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 следующие узлы:

    Соедините вход `Input` Subflow с входом узла `Switch`.

    Шаг 2: Настройка узла `Switch` для определения уровня

    Этот узел будет выполнять первичную валидацию и маршрутизацию на основе уровня логирования.

  • Откройте узел `Switch` и назовите его "Определить уровень".
  • В поле `Property` укажите `msg.level`.
  • Создайте правила для каждого известного нам уровня (`info`, `warn`, `error`, `critical`). Используйте оператор `==` (string).
  • Последним правилом добавьте `otherwise`. Это будет ветка для сообщений, у которых `msg.level` не указан или имеет некорректное значение.
  • Соедините все выходы узла `Switch` с единственным входом узла `Function`. Такая схема позволяет нам обрабатывать все уровни в одном месте, но дает возможность в будущем легко направить, например, `critical` события по отдельному пути.
  • Шаг 3: Написание кода в узле `Function`

    Это сердце нашего Subflow. Здесь происходит основная работа по формированию итоговой записи журнала.

  • Откройте узел `Function` и назовите его "Сформировать запись лога".
  • Вставьте следующий код:
  • // Получаем переменные окружения
    

    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;

    Этот код:

    Шаг 4: Динамическая установка MQTT-топика в узле `Change`

    Нам нужно, чтобы итоговый MQTT-топик зависел от уровня сообщения (например, `.../logs/info` или `.../logs/error`).

  • Откройте узел `Change` и назовите его "Установить MQTT топик".
  • Создайте одно правило:
  • * Set: `msg.topic`

    * to: `env.get("LOG_MQTT_TOPIC_PREFIX") & "/" & msg.level` (выберите тип выражения `J: JSONata`).

  • JSONata-выражение `&` конкатенирует (склеивает) строки, динамически создавая нужный нам топик на основе префикса из переменной окружения и уровня, определенного на предыдущем шаге. Нажмите Done.
  • Шаг 5: Финальная сборка и публикация

    Соедините выход узла `Function` с входом узла `Change`, а выход узла `Change` — с выходом `Output` нашего Subflow. Дайте Subflow осмысленное имя (например, "HI-Logger-Universal") и добавьте описание его работы и контракта во вкладку "Appearance".

    Нажмите кнопку `Deploy` в верхнем меню или вернитесь на основную вкладку рабочих процессов (Workspace) — теперь ваш новый Subflow появится в палитре слева в разделе `subflows`, и вы сможете использовать его как обычный узел. Теперь наш универсальный Subflow полностью готов к использованию.

    ---

    Пример: Интеграция логгера в реальный сценарий

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

    > 🔗 Ключевой инструмент: Для автоматического перехвата ошибок мы будем использовать специальный узел `Catch`. Он позволяет отлавливать сбои со всех узлов на текущей вкладке и направлять их в нашу систему логирования.

    Логирование штатной операции

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

  • Размещение Subflow: Перетащите ваш новый Subflow "HI-Logger-Universal" из палитры на рабочую область. В его настройках укажите значения переменных окружения, например:
  • * `LOG_MQTT_TOPIC_PREFIX`: `hi/sites/cottage-1/logs`

    * `LOG_DEFAULT_LEVEL`: `info`

  • Подготовка сообщения: После узла, который отправляет команду на реле, установите узел `Change`. Назовите его "Подготовить лог INFO".
  • Настройте в нем следующие правила для `msg`:

    * Set `msg.level` to `info` (string)

    * Set `msg.source_id` to `SCN-LIGHT-LIVINGROOM-001` (string)

    * Set `msg.payload` to `"Свет в гостиной включен"` (string)

  • Соединение и запуск: Соедините выход узла "Подготовить лог INFO" с входом Subflow "HI-Logger-Universal". Выход Subflow, в свою очередь, подключите к узлу `mqtt out`, который настроен на отправку сообщений на ваш MQTT-брокер. Выполните `Deploy` проекта.
  • Теперь при каждом включении света (или при симуляции через узел `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.

  • Добавление `Catch`: На той же вкладке, где находится ваш сценарий, разместите узел `Catch`. Настройте его на перехват ошибок со всех узлов на текущей вкладке (`Scope: all nodes`).
  • Прямое соединение: Соедините выход узла `Catch` напрямую со входом Subflow "HI-Logger-Universal". Выполните `Deploy`. Больше ничего делать не нужно!
  • Теперь, если в любом узле этого потока произойдет ошибка (например, узел `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-компонент, который позволяет централизованно управлять форматом и отправкой всех записей журнала в проекте.

    Ключевые результаты:

    Лучшие практики

    Что дальше?

    Созданный нами логгер — это мощная основа. Его можно и нужно развивать. Например, можно добавить второй выход, на который будут отправляться только сообщения с уровнем `critical`. Этот выход можно подключить к узлу отправки Telegram-сообщений или email, чтобы обеспечить мгновенное оповещение о самых серьезных сбоях в системе.

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