README, комментарии в коде, чистка requirements.txt

This commit is contained in:
2026-07-22 10:25:51 +04:00
parent 03fefc07fd
commit d8fde896b9
4 changed files with 288 additions and 39 deletions
+67
View File
@@ -0,0 +1,67 @@
# IoT Consumer — обработчик событий
Читает сообщения из очереди RabbitMQ и сохраняет их в Redis (оперативный кэш) и MongoDB (постоянный архив).
## Пайплайн
```
Producer → RabbitMQ → Consumer (этот сервис) → Redis + MongoDB → Dashboard
```
## Хранилища
### Redis — оперативный кэш
| Ключ | Тип | Содержимое |
|---|---|---|
| `iot:latest` | Hash | Последнее значение каждого датчика (`sensor_id → JSON`) |
| `iot:counters` | Hash | Счётчики событий по типам (`temperature: N`, ...) |
| `iot:recent` | ZSET | Лента последних 1000 событий (сортировка по времени) |
### MongoDB — постоянный архив
- Коллекция: `iot_events`
- TTL-индекс: автоматическое удаление через **7 дней**
## Переменные окружения
| Переменная | По умолчанию | Описание |
|---|---|---|
| `RMQ_HOST` | `localhost` | Хост RabbitMQ |
| `RMQ_PORT` | `5672` | Порт AMQP |
| `RMQ_USER` | `guest` | Логин RabbitMQ |
| `RMQ_PASS` | `guest` | Пароль RabbitMQ |
| `RMQ_QUEUE` | `iot-events` | Имя очереди |
| `REDIS_HOST` | `localhost` | Хост Redis |
| `REDIS_PORT` | `6379` | Порт Redis |
| `REDIS_PASS` | — | Пароль Redis (необязательно) |
| `MONGO_URI` | `mongodb://localhost:27017/iot` | Строка подключения MongoDB |
| `MONGO_DB` | `iot` | Имя базы MongoDB |
| `PORT` | `3000` | Порт HTTP |
## Эндпоинты
| Путь | Ответ |
|---|---|
| `GET /` | `{"service":"iot-consumer","consumed":N,"errors":N}` |
| `GET /health` | `{"status":"ok"}` |
## Локальный запуск
```bash
npm install
# Нужны RabbitMQ, Redis и MongoDB (локально или в Docker)
RMQ_HOST=localhost REDIS_HOST=localhost MONGO_URI=mongodb://localhost:27017/iot node server.js
```
## Деплой на Nubes Cloud
1. Запушьте этот репо в свой Gitea
2. В Terraform-конфиге укажите `git_path` на ваш репо и `git_revision`
3. Nubes автоматически запустит `npm start`
## Отказоустойчивость
- Каждое хранилище (Redis, MongoDB) обёрнуто в отдельный `try/catch`
- Отказ одного хранилища **не ломает** запись в другое
- Ошибки логируются, consumer продолжает работу
+9 -2
View File
@@ -2,6 +2,13 @@
"name": "iot-consumer", "name": "iot-consumer",
"version": "1.0.0", "version": "1.0.0",
"main": "server.js", "main": "server.js",
"scripts": { "start": "node server.js" }, "scripts": {
"dependencies": { "amqplib": "^0.10.0", "express": "^4.21.0", "redis": "^4.7.0", "mongodb": "^6.0.0" } "start": "node server.js"
},
"dependencies": {
"amqplib": "^0.10.0",
"express": "^4.21.0",
"redis": "^4.7.0",
"mongodb": "^6.0.0"
}
} }
-4
View File
@@ -1,4 +0,0 @@
Flask>=3.0
gunicorn>=21.2
redis>=5.0
pymongo>=4.0
+221 -42
View File
@@ -1,27 +1,92 @@
const amqp = require("amqplib"); // =============================================================================
const express = require("express"); // IoT Consumer — Обработчик событий из RabbitMQ
const { createClient } = require("redis"); // =============================================================================
const { MongoClient } = require("mongodb"); // Читает сообщения из очереди RabbitMQ (iot-events), помещённые туда
// Producer'ом, и сохраняет их в два хранилища:
// 1. Redis — оперативный кэш для Dashboard:
// - iot:latest (hash) — последнее значение каждого датчика
// - iot:counters (hash) — счётчики событий по типам устройств
// - iot:recent (zset) — лента последних 1000 событий
// 2. MongoDB — постоянный архив всех событий:
// - коллекция iot_events с TTL-индексом (автоудаление через 7 дней)
//
// Каждое хранилище обёрнуто в try/catch — отказ одного не ломает другое.
//
// Переменные окружения (задаются Terraform'ом через json_env):
// RMQ_HOST — хост RabbitMQ
// RMQ_PORT — порт AMQP (5672)
// RMQ_USER — логин RabbitMQ
// RMQ_PASS — пароль RabbitMQ
// RMQ_QUEUE — имя очереди (iot-events)
// REDIS_HOST — хост Redis
// REDIS_PORT — порт Redis (6379)
// REDIS_PASS — пароль Redis
// MONGO_URI — строка подключения MongoDB (mongodb://user:pass@host:port/db)
// MONGO_DB — имя базы MongoDB (iot)
// PORT — порт HTTP-сервера (3000)
//
// Эндпоинты:
// GET / — статистика (consumed, errors)
// GET /health — health-check для Kubernetes
// =============================================================================
const RMQ_HOST = process.env.RMQ_HOST || "localhost"; // ---------------------------------------------------------------------------
const RMQ_PORT = parseInt(process.env.RMQ_PORT || "5672"); // Зависимости
const RMQ_USER = process.env.RMQ_USER || "guest"; // ---------------------------------------------------------------------------
const RMQ_PASS = process.env.RMQ_PASS || "guest"; const amqp = require("amqplib"); // AMQP-клиент для RabbitMQ
const RMQ_QUEUE = process.env.RMQ_QUEUE || "iot-events"; const express = require("express"); // HTTP-фреймворк для /health и /stats
const { createClient } = require("redis"); // Redis-клиент (v4+, с Promise API)
const { MongoClient } = require("mongodb");// MongoDB-клиент (v6+, нативный драйвер)
const REDIS_HOST = process.env.REDIS_HOST || "localhost"; // ---------------------------------------------------------------------------
const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379"); // Конфигурация RabbitMQ из переменных окружения
const REDIS_PASS = process.env.REDIS_PASS || undefined; // ---------------------------------------------------------------------------
const RMQ_HOST = process.env.RMQ_HOST || "localhost"; // K8s-хост
const RMQ_PORT = parseInt(process.env.RMQ_PORT || "5672"); // AMQP-порт
const RMQ_USER = process.env.RMQ_USER || "guest"; // логин
const RMQ_PASS = process.env.RMQ_PASS || "guest"; // пароль
const RMQ_QUEUE = process.env.RMQ_QUEUE || "iot-events"; // имя очереди
// ---------------------------------------------------------------------------
// Конфигурация Redis из переменных окружения
// ---------------------------------------------------------------------------
const REDIS_HOST = process.env.REDIS_HOST || "localhost"; // K8s-хост
const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379"); // порт Redis
const REDIS_PASS = process.env.REDIS_PASS || undefined; // пароль (undefined = без пароля)
// ---------------------------------------------------------------------------
// Конфигурация MongoDB из переменных окружения
// ---------------------------------------------------------------------------
// MONGO_URI формируется Terraform'ом как:
// mongodb://admin:@хост:27017/iot?authSource=admin
// (без пароля для демо-стенда — platform limitation)
// ---------------------------------------------------------------------------
const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017/iot"; const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017/iot";
const MONGO_DB = process.env.MONGO_DB || "iot"; const MONGO_DB = process.env.MONGO_DB || "iot";
let consumed = 0, errors = 0; // ---------------------------------------------------------------------------
let redis, mongo, mongoColl; // Глобальное состояние
// ---------------------------------------------------------------------------
let consumed = 0, errors = 0; // счётчики для /
let redis, // Redis-клиент (ленивая инициализация)
mongo, // MongoDB-клиент (ленивая инициализация)
mongoColl; // MongoDB-коллекция iot_events
// ---------------------------------------------------------------------------
// getRedis() — ленивое подключение к Redis
// ---------------------------------------------------------------------------
// Создаёт клиент Redis один раз, при первом вызове.
// Подавляем ошибки Redis (redis.on("error", () => {})) — не хотим крашить
// consumer при временной недоступности Redis.
// ---------------------------------------------------------------------------
async function getRedis() { async function getRedis() {
if (!redis) { if (!redis) {
redis = createClient({ socket: { host: REDIS_HOST, port: REDIS_PORT }, password: REDIS_PASS }); // Создаём клиент с настройками из переменных окружения
redis = createClient({
socket: { host: REDIS_HOST, port: REDIS_PORT }, // TCP-сокет
password: REDIS_PASS, // пароль (undefined если нет)
});
// Подавляем ошибки Redis — не крашим consumer из-за временных проблем
redis.on("error", () => {}); redis.on("error", () => {});
await redis.connect(); await redis.connect();
console.log("Redis connected"); console.log("Redis connected");
@@ -29,61 +94,175 @@ async function getRedis() {
return redis; return redis;
} }
// ---------------------------------------------------------------------------
// getMongo() — ленивое подключение к MongoDB
// ---------------------------------------------------------------------------
// Создаёт клиент MongoDB один раз, при первом вызове.
// При подключении создаёт TTL-индекс на поле timestamp:
// expireAfterSeconds: 604800 = 7 дней
// Это значит MongoDB автоматически удалит документы старше 7 дней —
// не нужно ручной очистки.
// ---------------------------------------------------------------------------
async function getMongo() { async function getMongo() {
if (!mongo) { if (!mongo) {
mongo = new MongoClient(MONGO_URI); mongo = new MongoClient(MONGO_URI); // строка подключения из env
await mongo.connect(); await mongo.connect();
// Получаем коллекцию iot_events в базе MONGO_DB
mongoColl = mongo.db(MONGO_DB).collection("iot_events"); mongoColl = mongo.db(MONGO_DB).collection("iot_events");
// TTL-индекс: документы старше 7 дней удаляются автоматически
await mongoColl.createIndex({ timestamp: 1 }, { expireAfterSeconds: 604800 }); await mongoColl.createIndex({ timestamp: 1 }, { expireAfterSeconds: 604800 });
console.log("MongoDB connected"); console.log("MongoDB connected");
} }
return mongoColl; return mongoColl;
} }
// ---------------------------------------------------------------------------
// store(event) — сохраняет событие в Redis и MongoDB
// ---------------------------------------------------------------------------
// Каждое хранилище — в своём try/catch:
// - Если Redis упал — событие всё равно попадёт в MongoDB
// - Если MongoDB упала — событие всё равно попадёт в Redis
// Это гарантирует, что отказ одного хранилища не блокирует другое.
//
// Redis-структуры:
// iot:latest — HSET sensor_id → JSON{value, unit, location, timestamp}
// (каждый новый запрос перезаписывает предыдущий)
// iot:counters — HINCRBY device_type → +1
// (счётчик событий по типам: temperature, humidity, power_meter)
// iot:recent — ZADD score=Date.now() → JSON-событие
// ZREMRANGEBYRANK 0..-1001 — держим только 1000 последних
// ---------------------------------------------------------------------------
async function store(event) { async function store(event) {
// === Запись в Redis (оперативный кэш) ===
try { try {
const r = await getRedis(); const r = await getRedis();
await r.hSet("iot:latest", event.sensor_id, JSON.stringify({
value: event.value, unit: event.unit, location: event.location, timestamp: event.timestamp,
}));
await r.hIncrBy("iot:counters", event.device_type, 1);
await r.zAdd("iot:recent", [{ score: Date.now(), value: JSON.stringify(event) }]);
await r.zRemRangeByRank("iot:recent", 0, -1001);
} catch (e) { console.error("Redis error:", e.message); }
try { // HSET — последнее значение датчика (ключ = sensor_id)
const coll = await getMongo(); // Перезаписывает предыдущее значение — всегда актуальное
await coll.insertOne({ await r.hSet("iot:latest", event.sensor_id, JSON.stringify({
event_id: event.event_id, sensor_id: event.sensor_id, value: event.value,
device_type: event.device_type, location: event.location, unit: event.unit,
value: event.value, unit: event.unit, timestamp: new Date(event.timestamp), location: event.location,
}); timestamp: event.timestamp,
} catch (e) { console.error("Mongo error:", e.message); } }));
// HINCRBY — атомарный инкремент счётчика по типу устройства
await r.hIncrBy("iot:counters", event.device_type, 1);
// ZADD — добавляем в сортированное множество (счёт = timestamp)
// score = Date.now() для сортировки по времени
await r.zAdd("iot:recent", [{
score: Date.now(),
value: JSON.stringify(event),
}]);
// ZREMRANGEBYRANK — удаляем всё что выходит за пределы последних 1000
// 0..-1001 означает: удалить с 0-го элемента до элемента с индексом
// (len - 1001), оставляя 1000 последних
await r.zRemRangeByRank("iot:recent", 0, -1001);
} catch (e) {
// Redis недоступен — логируем, но НЕ крашим consumer
console.error("Redis error:", e.message);
} }
async function start() { // === Запись в MongoDB (постоянный архив) ===
const conn = await amqp.connect(`amqp://${RMQ_USER}:${RMQ_PASS}@${RMQ_HOST}:${RMQ_PORT}`);
const ch = await conn.createChannel();
await ch.assertQueue(RMQ_QUEUE, { durable: true });
ch.prefetch(10);
await ch.consume(RMQ_QUEUE, async (msg) => {
if (!msg) return;
try { try {
const coll = await getMongo();
// insertOne — вставка одного документа
// timestamp преобразуется из ISO-строки в Date для TTL-индекса
await coll.insertOne({
event_id: event.event_id,
sensor_id: event.sensor_id,
device_type: event.device_type,
location: event.location,
value: event.value,
unit: event.unit,
timestamp: new Date(event.timestamp), // Date-объект для TTL
});
} catch (e) {
// MongoDB недоступна — логируем, но НЕ крашим consumer
console.error("Mongo error:", e.message);
}
}
// ---------------------------------------------------------------------------
// start() — подключение к RabbitMQ и запуск консьюмера
// ---------------------------------------------------------------------------
// 1. Подключается к RabbitMQ по AMQP
// 2. Создаёт канал и гарантирует существование очереди
// 3. Устанавливает prefetch(10) — не более 10 неподтверждённых сообщений
// одновременно (backpressure: не загружаем consumer слишком сильно)
// 4. consume() — подписывается на очередь, обрабатывает каждое сообщение:
// a. Парсит JSON из тела сообщения
// b. Вызывает store() для записи в Redis + MongoDB
// c. ack() — подтверждает обработку (сообщение удаляется из очереди)
// d. При ошибке: nack(msg, false, true) — возвращает в очередь (requeue)
// ---------------------------------------------------------------------------
async function start() {
// Подключение к RabbitMQ
const conn = await amqp.connect(
`amqp://${RMQ_USER}:${RMQ_PASS}@${RMQ_HOST}:${RMQ_PORT}`
);
const ch = await conn.createChannel();
// Гарантируем существование очереди (идемпотентно)
await ch.assertQueue(RMQ_QUEUE, { durable: true });
// prefetch(10): не отправлять больше 10 сообщений одновременно,
// пока предыдущие не подтверждены (ack)
// Это защита от перегрузки consumer'а
ch.prefetch(10);
// Подписка на очередь
await ch.consume(RMQ_QUEUE, async (msg) => {
// msg === null означает отмену подписки (например, очередь удалена)
if (!msg) return;
try {
// Парсим JSON из тела сообщения (Buffer → string → object)
const event = JSON.parse(msg.content.toString()); const event = JSON.parse(msg.content.toString());
// Сохраняем в Redis + MongoDB
await store(event); await store(event);
consumed++;
ch.ack(msg); consumed++; // инкрементируем счётчик успешных
ch.ack(msg); // подтверждаем — сообщение удаляется из очереди
// Каждые 10 сообщений — логируем прогресс
if (consumed % 10 === 0) console.log(`Consumed ${consumed} events`); if (consumed % 10 === 0) console.log(`Consumed ${consumed} events`);
} catch (e) { } catch (e) {
errors++; errors++;
// nack с requeue=true — возвращаем сообщение в очередь
// для повторной попытки (если ошибка временная)
// ВНИМАНИЕ: при систематической ошибке может вызвать infinite loop!
ch.nack(msg, false, true); ch.nack(msg, false, true);
console.error("Error:", e.message); console.error("Error:", e.message);
} }
}); });
console.log(`Consumer listening on queue: ${RMQ_QUEUE}`); console.log(`Consumer listening on queue: ${RMQ_QUEUE}`);
} }
// ---------------------------------------------------------------------------
// HTTP-сервер (Express)
// ---------------------------------------------------------------------------
const app = express(); const app = express();
app.get("/", (req, res) => res.json({ service: "iot-consumer", status: "running", queue: RMQ_QUEUE, consumed, errors }));
// GET / — статистика consumer'а
app.get("/", (req, res) => res.json({
service: "iot-consumer",
status: "running",
queue: RMQ_QUEUE,
consumed,
errors,
}));
// GET /health — для Kubernetes liveness/readiness probe
app.get("/health", (req, res) => res.json({ status: "ok" })); app.get("/health", (req, res) => res.json({ status: "ok" }));
app.listen(process.env.PORT || 3000, () => { console.log("Consumer started"); start(); });
// Запуск: слушаем порт, затем подключаемся к RabbitMQ
app.listen(process.env.PORT || 3000, () => {
console.log("Consumer started");
start(); // подключаемся к RabbitMQ и начинаем обрабатывать сообщения
});