mqtt-devices-parser 1.0.19 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Changelog.md +4 -0
- package/Readme.md +80 -0
- package/config/index.js +16 -1
- package/example/kafka-example.js +181 -0
- package/index.js +12 -2
- package/models/mqtt.models.js +41 -0
- package/models/mqttTemplate.models.js +29 -0
- package/package.json +2 -1
- package/src/device/device.js +4 -0
- package/src/kafka/consumer.js +176 -0
- package/src/unitTest/kafka-integration.test.js +118 -0
- package/src/unitTest/kafka.test.js +72 -0
package/Changelog.md
CHANGED
package/Readme.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
## NPM Module
|
|
5
5
|
- MQTT parser
|
|
6
6
|
- Parses topics coming from devices and updates data on dB
|
|
7
|
+
- **NEW**: Kafka consumer support for multi-process message parsing
|
|
7
8
|
|
|
8
9
|
## Deploy
|
|
9
10
|
```
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
|
|
26
27
|
- mysql8.0
|
|
27
28
|
- MQTT
|
|
29
|
+
- **NEW**: Apache Kafka (optional)
|
|
28
30
|
|
|
29
31
|
## Front end
|
|
30
32
|
- Node service running the following [project](https://github.com/zimbora/mgmt-iot-web)
|
|
@@ -37,3 +39,81 @@ In your program set the same struct and pass it as argument on the module init c
|
|
|
37
39
|
An mqtt account will be created using the following settings
|
|
38
40
|
mqtt.user - mqtt user login
|
|
39
41
|
mqtt.pwd - mqtt pwd login
|
|
42
|
+
|
|
43
|
+
### Kafka Consumer Configuration (NEW)
|
|
44
|
+
|
|
45
|
+
The module now supports consuming messages from Kafka topics as an alternative or complement to MQTT. This enables multi-process message parsing using Kafka's shared subscription mechanism.
|
|
46
|
+
|
|
47
|
+
#### Environment Variables
|
|
48
|
+
|
|
49
|
+
**Kafka Settings:**
|
|
50
|
+
- `KAFKA_ENABLED` - Set to `'true'` to enable Kafka consumer (default: `false`)
|
|
51
|
+
- `KAFKA_BROKERS` - Comma-separated list of Kafka brokers (default: `'localhost:9092'`)
|
|
52
|
+
- `KAFKA_GROUP_ID` - Consumer group ID for shared subscriptions (default: `'mqtt-devices-parser-group'`)
|
|
53
|
+
- `KAFKA_CLIENT_ID` - Kafka client identifier (default: `'mqtt-devices-parser'`)
|
|
54
|
+
|
|
55
|
+
**Authentication (SASL):**
|
|
56
|
+
- `KAFKA_SASL_MECHANISM` - SASL mechanism: `'plain'`, `'scram-sha-256'`, `'scram-sha-512'` (default: `'plain'`)
|
|
57
|
+
- `KAFKA_SASL_USERNAME` - SASL username (default: empty)
|
|
58
|
+
- `KAFKA_SASL_PASSWORD` - SASL password (default: empty)
|
|
59
|
+
|
|
60
|
+
**Security:**
|
|
61
|
+
- `KAFKA_SSL` - Set to `'true'` to enable SSL (default: `false`)
|
|
62
|
+
|
|
63
|
+
**Connection Settings:**
|
|
64
|
+
- `KAFKA_CONNECTION_TIMEOUT` - Connection timeout in ms (default: `3000`)
|
|
65
|
+
- `KAFKA_REQUEST_TIMEOUT` - Request timeout in ms (default: `30000`)
|
|
66
|
+
|
|
67
|
+
**MQTT Parsing Control:**
|
|
68
|
+
- `MQTT_PARSE_MESSAGES` - Set to `'false'` to disable direct MQTT message parsing (default: `true`)
|
|
69
|
+
|
|
70
|
+
#### Usage Examples
|
|
71
|
+
|
|
72
|
+
**Example 1: Enable Kafka with basic authentication**
|
|
73
|
+
```bash
|
|
74
|
+
export KAFKA_ENABLED=true
|
|
75
|
+
export KAFKA_BROKERS=kafka1:9092,kafka2:9092
|
|
76
|
+
export KAFKA_SASL_USERNAME=myuser
|
|
77
|
+
export KAFKA_SASL_PASSWORD=mypassword
|
|
78
|
+
export KAFKA_SASL_MECHANISM=scram-sha-256
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Example 2: Use Kafka only (disable MQTT parsing)**
|
|
82
|
+
```bash
|
|
83
|
+
export KAFKA_ENABLED=true
|
|
84
|
+
export MQTT_PARSE_MESSAGES=false
|
|
85
|
+
export KAFKA_BROKERS=kafka-cluster:9092
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Example 3: Multi-process deployment**
|
|
89
|
+
```bash
|
|
90
|
+
# Start multiple instances with the same group ID for load balancing
|
|
91
|
+
export KAFKA_ENABLED=true
|
|
92
|
+
export KAFKA_GROUP_ID=my-parser-group
|
|
93
|
+
export KAFKA_BROKERS=kafka1:9092,kafka2:9092
|
|
94
|
+
# Each process will consume different partitions automatically
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Kafka Topic Structure
|
|
98
|
+
|
|
99
|
+
Kafka topics should correspond to project names configured in your `projects` array. Messages are expected to have:
|
|
100
|
+
|
|
101
|
+
- **Topic**: The project name (e.g., `demo`, `freeRTOS2`)
|
|
102
|
+
- **Key**: Device path part (e.g., `uid:123/status`)
|
|
103
|
+
- **Value**: Message payload (same format as MQTT payloads)
|
|
104
|
+
|
|
105
|
+
The consumer will automatically format messages as `{project}/{key}` to match the expected MQTT topic structure.
|
|
106
|
+
|
|
107
|
+
#### Shared Subscriptions
|
|
108
|
+
|
|
109
|
+
When multiple instances use the same `KAFKA_GROUP_ID`, Kafka automatically distributes messages across consumers, enabling:
|
|
110
|
+
|
|
111
|
+
- **Load balancing**: Messages are distributed across multiple parser instances
|
|
112
|
+
- **High availability**: If one instance fails, others continue processing
|
|
113
|
+
- **Scalability**: Add more instances to handle increased load
|
|
114
|
+
|
|
115
|
+
#### Message Flow
|
|
116
|
+
|
|
117
|
+
1. **MQTT Mode** (traditional): MQTT messages → `$.device.parseMessage`
|
|
118
|
+
2. **Kafka Mode**: Kafka messages → Topic formatting → `$.device.parseMessage`
|
|
119
|
+
3. **Hybrid Mode**: Both MQTT and Kafka messages → `$.device.parseMessage`
|
package/config/index.js
CHANGED
|
@@ -13,7 +13,22 @@ module.exports = {
|
|
|
13
13
|
user:process.env.MQTT_USER || 'admin',
|
|
14
14
|
pwd:process.env.MQTT_PWD || 'admin',
|
|
15
15
|
client:process.env.MQTT_CLIENT || 'mqtt-devices-parser',
|
|
16
|
-
logs_path: process.env.MQTT_LOGS || 'uServices'
|
|
16
|
+
logs_path: process.env.MQTT_LOGS || 'uServices',
|
|
17
|
+
parseMessages: process.env.MQTT_PARSE_MESSAGES !== 'false' // Default to true for backward compatibility
|
|
18
|
+
},
|
|
19
|
+
kafka: {
|
|
20
|
+
enabled: process.env.KAFKA_ENABLED === 'true' ? true : true,
|
|
21
|
+
brokers: (process.env.KAFKA_BROKERS || 'localhost:9092').split(','),
|
|
22
|
+
groupId: process.env.KAFKA_GROUP_ID || 'mqtt-devices-parser-group',
|
|
23
|
+
clientId: process.env.KAFKA_CLIENT_ID || 'mqtt-devices-parser',
|
|
24
|
+
sasl: {
|
|
25
|
+
mechanism: process.env.KAFKA_SASL_MECHANISM || 'plain', // plain, scram-sha-256, scram-sha-512
|
|
26
|
+
username: process.env.KAFKA_SASL_USERNAME || '',
|
|
27
|
+
password: process.env.KAFKA_SASL_PASSWORD || ''
|
|
28
|
+
},
|
|
29
|
+
ssl: process.env.KAFKA_SSL === 'true' ? true : false,
|
|
30
|
+
connectionTimeout: parseInt(process.env.KAFKA_CONNECTION_TIMEOUT) || 3000,
|
|
31
|
+
requestTimeout: parseInt(process.env.KAFKA_REQUEST_TIMEOUT) || 30000
|
|
17
32
|
},
|
|
18
33
|
mysqldb: {
|
|
19
34
|
conn_limit: process.env.DB_CONN_LIMIT || 15,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example showing how to use mqtt-devices-parser with Kafka consumer support
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* 1. Traditional MQTT-only mode
|
|
6
|
+
* 2. Kafka-only mode
|
|
7
|
+
* 3. Hybrid mode (both MQTT and Kafka)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const parser = require('../index.js');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
global.BASE_DIR = process.cwd();
|
|
14
|
+
|
|
15
|
+
// Example configuration with Kafka support
|
|
16
|
+
const configWithKafka = {
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
dev: true,
|
|
19
|
+
web: {
|
|
20
|
+
protocol: "http://",
|
|
21
|
+
domain: 'localhost',
|
|
22
|
+
port: 8002,
|
|
23
|
+
},
|
|
24
|
+
mqtt: {
|
|
25
|
+
protocol: 'MQTT',
|
|
26
|
+
host: 'localhost',
|
|
27
|
+
port: '1883',
|
|
28
|
+
user: 'admin',
|
|
29
|
+
pwd: 'admin',
|
|
30
|
+
client: 'mqtt-devices-parser-example',
|
|
31
|
+
logs_path: 'uServices',
|
|
32
|
+
parseMessages: true // Set to false to disable MQTT message parsing
|
|
33
|
+
},
|
|
34
|
+
kafka: {
|
|
35
|
+
enabled: true, // Enable Kafka consumer
|
|
36
|
+
brokers: ['localhost:9092'], // Your Kafka brokers
|
|
37
|
+
groupId: 'mqtt-devices-parser-group',
|
|
38
|
+
clientId: 'mqtt-devices-parser-client',
|
|
39
|
+
sasl: {
|
|
40
|
+
mechanism: 'plain', // or 'scram-sha-256', 'scram-sha-512'
|
|
41
|
+
username: '', // Your Kafka username
|
|
42
|
+
password: '' // Your Kafka password
|
|
43
|
+
},
|
|
44
|
+
ssl: false, // Set to true for SSL/TLS
|
|
45
|
+
connectionTimeout: 3000,
|
|
46
|
+
requestTimeout: 30000
|
|
47
|
+
},
|
|
48
|
+
mysqldb: {
|
|
49
|
+
conn_limit: 15,
|
|
50
|
+
host: 'localhost',
|
|
51
|
+
port: 3306,
|
|
52
|
+
user: 'user',
|
|
53
|
+
pwd: 'user_pwd',
|
|
54
|
+
name: 'mqtt-aedes',
|
|
55
|
+
},
|
|
56
|
+
sync_main_tables: true,
|
|
57
|
+
projects: {
|
|
58
|
+
demo: true,
|
|
59
|
+
// Add your project names here
|
|
60
|
+
// These will become Kafka topic names when Kafka is enabled
|
|
61
|
+
},
|
|
62
|
+
demo: {
|
|
63
|
+
uidPrefix: 'uid:',
|
|
64
|
+
uidLength: 16
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Example: MQTT-only configuration (traditional mode)
|
|
69
|
+
const configMQTTOnly = {
|
|
70
|
+
...configWithKafka,
|
|
71
|
+
kafka: {
|
|
72
|
+
...configWithKafka.kafka,
|
|
73
|
+
enabled: false // Disable Kafka
|
|
74
|
+
},
|
|
75
|
+
mqtt: {
|
|
76
|
+
...configWithKafka.mqtt,
|
|
77
|
+
parseMessages: true // Keep MQTT parsing enabled
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Example: Kafka-only configuration
|
|
82
|
+
const configKafkaOnly = {
|
|
83
|
+
...configWithKafka,
|
|
84
|
+
kafka: {
|
|
85
|
+
...configWithKafka.kafka,
|
|
86
|
+
enabled: true // Enable Kafka
|
|
87
|
+
},
|
|
88
|
+
mqtt: {
|
|
89
|
+
...configWithKafka.mqtt,
|
|
90
|
+
parseMessages: false // Disable MQTT parsing
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Load projects from directories
|
|
95
|
+
var projectsPath = '../projects/';
|
|
96
|
+
var projects = [];
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
fs.readdirSync(projectsPath)
|
|
100
|
+
.filter((file) => {
|
|
101
|
+
return (file.indexOf('.') == -1);
|
|
102
|
+
})
|
|
103
|
+
.forEach((project) => {
|
|
104
|
+
if (configWithKafka.projects[project]) {
|
|
105
|
+
projects.push(project);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.log('Projects directory not found, using default projects');
|
|
110
|
+
projects = ['demo'];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runExample() {
|
|
114
|
+
try {
|
|
115
|
+
console.log('=== MQTT Devices Parser with Kafka Support Example ===\n');
|
|
116
|
+
|
|
117
|
+
// Choose which configuration to use
|
|
118
|
+
const selectedConfig = configWithKafka; // Change this to test different modes
|
|
119
|
+
|
|
120
|
+
console.log('Configuration mode:');
|
|
121
|
+
console.log('- MQTT parsing enabled:', selectedConfig.mqtt.parseMessages);
|
|
122
|
+
console.log('- Kafka consumer enabled:', selectedConfig.kafka.enabled);
|
|
123
|
+
console.log('- Projects:', projects.join(', '));
|
|
124
|
+
console.log();
|
|
125
|
+
|
|
126
|
+
if (selectedConfig.kafka.enabled) {
|
|
127
|
+
console.log('Kafka configuration:');
|
|
128
|
+
console.log('- Brokers:', selectedConfig.kafka.brokers.join(', '));
|
|
129
|
+
console.log('- Group ID:', selectedConfig.kafka.groupId);
|
|
130
|
+
console.log('- SASL enabled:', !!(selectedConfig.kafka.sasl.username && selectedConfig.kafka.sasl.password));
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Initialize the parser
|
|
135
|
+
console.log('Initializing parser...');
|
|
136
|
+
await parser.init(selectedConfig, projects);
|
|
137
|
+
|
|
138
|
+
console.log('Parser initialized successfully!');
|
|
139
|
+
console.log();
|
|
140
|
+
|
|
141
|
+
if (selectedConfig.kafka.enabled) {
|
|
142
|
+
console.log('Kafka consumer is running and listening for messages on topics:', projects.join(', '));
|
|
143
|
+
console.log('Send messages to these Kafka topics with format:');
|
|
144
|
+
console.log('- Topic: <project_name> (e.g., "demo")');
|
|
145
|
+
console.log('- Key: <device_path> (e.g., "uid:123/status")');
|
|
146
|
+
console.log('- Value: <message_payload> (e.g., "online")');
|
|
147
|
+
console.log();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (selectedConfig.mqtt.parseMessages) {
|
|
151
|
+
console.log('MQTT client is connected and parsing messages');
|
|
152
|
+
console.log('Subscribed to MQTT topics:', projects.map(p => p + '/#').join(', '));
|
|
153
|
+
console.log();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log('Press Ctrl+C to stop...');
|
|
157
|
+
|
|
158
|
+
// Keep the process running
|
|
159
|
+
await new Promise(resolve => {
|
|
160
|
+
process.on('SIGINT', () => {
|
|
161
|
+
console.log('\nShutting down gracefully...');
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Error running example:', error);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle unhandled rejections
|
|
173
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
174
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
175
|
+
if (configWithKafka.dev) {
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Run the example
|
|
181
|
+
runExample();
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ $.md5 = require('md5');
|
|
|
7
7
|
$.config = require('./config');
|
|
8
8
|
$.models = require('./models/models.js');
|
|
9
9
|
$.device = require('./src/device/device.js');
|
|
10
|
+
$.kafka_consumer = require('./src/kafka/consumer.js');
|
|
10
11
|
$.db = require('./src/db/db');
|
|
11
12
|
$.db_data = require('./src/db/data');
|
|
12
13
|
$.db_project = require('./src/db/project');
|
|
@@ -40,6 +41,12 @@ var self = module.exports = {
|
|
|
40
41
|
//auth.init();
|
|
41
42
|
await $.device.init($.config,projects);
|
|
42
43
|
|
|
44
|
+
// Initialize Kafka consumer if enabled
|
|
45
|
+
if ($.config.kafka.enabled) {
|
|
46
|
+
await $.kafka_consumer.init($.config, projects);
|
|
47
|
+
await $.kafka_consumer.start();
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
console.log("mqtt connecting..")
|
|
44
51
|
mqtt_connect();
|
|
45
52
|
|
|
@@ -181,8 +188,11 @@ function mqtt_connect(){
|
|
|
181
188
|
});
|
|
182
189
|
|
|
183
190
|
$.mqtt_client.on("message", (topic, payload, packet) => {
|
|
184
|
-
//
|
|
185
|
-
|
|
191
|
+
// Only parse MQTT messages if enabled in configuration
|
|
192
|
+
if ($.config.mqtt.parseMessages) {
|
|
193
|
+
// payload is Buffer
|
|
194
|
+
$.device.parseMessage($.mqtt_client,topic.toString(),payload.toString(),packet.retain);
|
|
195
|
+
}
|
|
186
196
|
});
|
|
187
197
|
|
|
188
198
|
$.mqtt_client.on("reconnect",()=>{
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
module.exports = (sequelize,DataTypes)=>{
|
|
3
|
+
return sequelize.define("mqtt", {
|
|
4
|
+
device_id: {
|
|
5
|
+
type: DataTypes.INTEGER,
|
|
6
|
+
allowNull: true
|
|
7
|
+
},
|
|
8
|
+
topic: {
|
|
9
|
+
type: DataTypes.STRING,
|
|
10
|
+
allowNull: false
|
|
11
|
+
},
|
|
12
|
+
description: {
|
|
13
|
+
type: DataTypes.JSON,
|
|
14
|
+
allowNull: false
|
|
15
|
+
},
|
|
16
|
+
defaultData: {
|
|
17
|
+
type: DataTypes.JSON,
|
|
18
|
+
allowNull: true
|
|
19
|
+
},
|
|
20
|
+
remoteData: {
|
|
21
|
+
type: DataTypes.JSON,
|
|
22
|
+
allowNull: true
|
|
23
|
+
},
|
|
24
|
+
localData: {
|
|
25
|
+
type: DataTypes.JSON,
|
|
26
|
+
allowNull: true
|
|
27
|
+
},
|
|
28
|
+
readInterval: {
|
|
29
|
+
type: DataTypes.INTEGER,
|
|
30
|
+
allowNull: true
|
|
31
|
+
},
|
|
32
|
+
template_id: {
|
|
33
|
+
type: DataTypes.INTEGER,
|
|
34
|
+
allowNull: true,
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
tableName: 'mqtt',
|
|
39
|
+
freezeTableName: true
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
module.exports = (sequelize,DataTypes)=>{
|
|
3
|
+
return sequelize.define("mqttTemplate", {
|
|
4
|
+
topic: {
|
|
5
|
+
type: DataTypes.STRING,
|
|
6
|
+
allowNull: false
|
|
7
|
+
},
|
|
8
|
+
description: {
|
|
9
|
+
type: DataTypes.JSON,
|
|
10
|
+
allowNull: false
|
|
11
|
+
},
|
|
12
|
+
defaultData: {
|
|
13
|
+
type: DataTypes.JSON,
|
|
14
|
+
allowNull: true
|
|
15
|
+
},
|
|
16
|
+
readInterval: {
|
|
17
|
+
type: DataTypes.INTEGER,
|
|
18
|
+
allowNull: true
|
|
19
|
+
},
|
|
20
|
+
template_id: {
|
|
21
|
+
type: DataTypes.INTEGER,
|
|
22
|
+
allowNull: false
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
tableName: 'mqttTemplate',
|
|
27
|
+
freezeTableName: true
|
|
28
|
+
})
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mqtt-devices-parser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"async": "^3.2.4",
|
|
14
|
+
"kafkajs": "^2.2.4",
|
|
14
15
|
"lodash": "^4.17.21",
|
|
15
16
|
"md5": "^2.3.0",
|
|
16
17
|
"moment": "^2.29.4",
|
package/src/device/device.js
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const { Kafka, logLevel } = require('kafkajs');
|
|
2
|
+
|
|
3
|
+
var kafka = null;
|
|
4
|
+
var consumer = null;
|
|
5
|
+
var running = false;
|
|
6
|
+
|
|
7
|
+
var self = module.exports = {
|
|
8
|
+
|
|
9
|
+
init: async (config, projects) => {
|
|
10
|
+
|
|
11
|
+
return new Promise(async (resolve, reject) => {
|
|
12
|
+
try {
|
|
13
|
+
if (!config.kafka.enabled) {
|
|
14
|
+
console.log("Kafka consumer is disabled");
|
|
15
|
+
return resolve();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log("Initializing Kafka consumer...");
|
|
19
|
+
|
|
20
|
+
// Build Kafka configuration
|
|
21
|
+
const kafkaConfig = {
|
|
22
|
+
clientId: config.kafka.clientId,
|
|
23
|
+
brokers: config.kafka.brokers,
|
|
24
|
+
connectionTimeout: config.kafka.connectionTimeout,
|
|
25
|
+
requestTimeout: config.kafka.requestTimeout,
|
|
26
|
+
logLevel: logLevel.INFO
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Add SASL authentication if credentials provided
|
|
30
|
+
if (config.kafka.sasl.username && config.kafka.sasl.password) {
|
|
31
|
+
kafkaConfig.sasl = {
|
|
32
|
+
mechanism: config.kafka.sasl.mechanism,
|
|
33
|
+
username: config.kafka.sasl.username,
|
|
34
|
+
password: config.kafka.sasl.password
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add SSL configuration if enabled
|
|
39
|
+
if (config.kafka.ssl) {
|
|
40
|
+
kafkaConfig.ssl = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
kafka = new Kafka(kafkaConfig);
|
|
44
|
+
|
|
45
|
+
// Create consumer with shared subscription support
|
|
46
|
+
consumer = kafka.consumer({
|
|
47
|
+
groupId: config.kafka.groupId,
|
|
48
|
+
sessionTimeout: 30000,
|
|
49
|
+
rebalanceTimeout: 60000,
|
|
50
|
+
heartbeatInterval: 3000,
|
|
51
|
+
maxBytesPerPartition: 1048576,
|
|
52
|
+
minBytes: 1,
|
|
53
|
+
maxBytes: 10485760,
|
|
54
|
+
maxWaitTimeInMs: 5000,
|
|
55
|
+
retry: {
|
|
56
|
+
initialRetryTime: 100,
|
|
57
|
+
retries: 8
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Subscribe to project topics
|
|
62
|
+
const topics = projects.map(project => project.toString());
|
|
63
|
+
|
|
64
|
+
if (topics.length === 0) {
|
|
65
|
+
console.log("No Kafka topics to subscribe to");
|
|
66
|
+
return resolve();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await consumer.subscribe({
|
|
70
|
+
topics: topics,
|
|
71
|
+
fromBeginning: false
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(`Kafka consumer subscribed to topics: ${topics.join(', ')}`);
|
|
75
|
+
|
|
76
|
+
return resolve();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("Failed to initialize Kafka consumer:", error);
|
|
79
|
+
return reject(error);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
start: async () => {
|
|
85
|
+
if (!consumer || running) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
console.log("Starting Kafka consumer...");
|
|
91
|
+
running = true;
|
|
92
|
+
|
|
93
|
+
await consumer.run({
|
|
94
|
+
eachMessage: async ({ topic, partition, message }) => {
|
|
95
|
+
try {
|
|
96
|
+
let payload = message.value ? message.value.toString() : '';
|
|
97
|
+
const kafkaTopic = topic.toString();
|
|
98
|
+
|
|
99
|
+
// Format message to look like MQTT format for compatibility
|
|
100
|
+
// Kafka topic becomes the project name, message key becomes the device path
|
|
101
|
+
const messageKey = message.key ? message.key.toString() : '';
|
|
102
|
+
|
|
103
|
+
let formattedTopic;
|
|
104
|
+
if (messageKey) {
|
|
105
|
+
// If message has a key, use it as the device path: project/device_path
|
|
106
|
+
formattedTopic = `${kafkaTopic}/${messageKey}`;
|
|
107
|
+
} else {
|
|
108
|
+
// If no key, try to extract from headers or use a default format
|
|
109
|
+
const headers = message.headers || {};
|
|
110
|
+
const devicePath = headers.device_path ? headers.device_path.toString() : 'unknown';
|
|
111
|
+
formattedTopic = `${kafkaTopic}/${devicePath}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try{
|
|
115
|
+
payload = JSON.parse(payload);
|
|
116
|
+
}catch(error){}
|
|
117
|
+
|
|
118
|
+
// Call the device parser with formatted topic
|
|
119
|
+
await $.device.parseMessage(payload.client.id, formattedTopic, payload.mqtt.payload, payload.mqtt.retain);
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('[Kafka] Error processing message:', {
|
|
123
|
+
topic: topic.toString(),
|
|
124
|
+
partition,
|
|
125
|
+
offset: message.offset ? message.offset.toString() : 'unknown',
|
|
126
|
+
error: error.message
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log("Kafka consumer started successfully");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("Failed to start Kafka consumer:", error);
|
|
135
|
+
running = false;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
stop: async () => {
|
|
141
|
+
if (!consumer || !running) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
console.log("Stopping Kafka consumer...");
|
|
147
|
+
running = false;
|
|
148
|
+
await consumer.stop();
|
|
149
|
+
await consumer.disconnect();
|
|
150
|
+
console.log("Kafka consumer stopped successfully");
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error("Error stopping Kafka consumer:", error);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
isRunning: () => {
|
|
157
|
+
return running;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Graceful shutdown handling
|
|
162
|
+
process.on('SIGINT', async () => {
|
|
163
|
+
if (running) {
|
|
164
|
+
console.log('Received SIGINT, gracefully shutting down Kafka consumer...');
|
|
165
|
+
await self.stop();
|
|
166
|
+
}
|
|
167
|
+
process.exit(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
process.on('SIGTERM', async () => {
|
|
171
|
+
if (running) {
|
|
172
|
+
console.log('Received SIGTERM, gracefully shutting down Kafka consumer...');
|
|
173
|
+
await self.stop();
|
|
174
|
+
}
|
|
175
|
+
process.exit(0);
|
|
176
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
$ = {};
|
|
4
|
+
var path = "/../..";
|
|
5
|
+
$.config = require(__dirname + path + '/config');
|
|
6
|
+
|
|
7
|
+
// Mock device module to prevent actual parsing
|
|
8
|
+
$.device = {
|
|
9
|
+
parseMessage: async (client, topic, payload, retain) => {
|
|
10
|
+
console.log('Mock parseMessage called:', { topic, payload, retain });
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function testConfigurationValues() {
|
|
16
|
+
console.log('\n=== Testing Configuration Values ===');
|
|
17
|
+
|
|
18
|
+
// Test default values
|
|
19
|
+
console.log('Default MQTT parseMessages:', $.config.mqtt.parseMessages);
|
|
20
|
+
console.log('Default Kafka enabled:', $.config.kafka.enabled);
|
|
21
|
+
console.log('Default Kafka brokers:', $.config.kafka.brokers);
|
|
22
|
+
console.log('Default Kafka groupId:', $.config.kafka.groupId);
|
|
23
|
+
console.log('Default Kafka SASL mechanism:', $.config.kafka.sasl.mechanism);
|
|
24
|
+
|
|
25
|
+
// Test with environment variables
|
|
26
|
+
process.env.MQTT_PARSE_MESSAGES = 'false';
|
|
27
|
+
process.env.KAFKA_ENABLED = 'true';
|
|
28
|
+
process.env.KAFKA_BROKERS = 'broker1:9092,broker2:9092';
|
|
29
|
+
process.env.KAFKA_GROUP_ID = 'test-group';
|
|
30
|
+
process.env.KAFKA_SASL_MECHANISM = 'scram-sha-256';
|
|
31
|
+
process.env.KAFKA_SASL_USERNAME = 'testuser';
|
|
32
|
+
process.env.KAFKA_SASL_PASSWORD = 'testpass';
|
|
33
|
+
|
|
34
|
+
// Reload config to pick up env variables
|
|
35
|
+
delete require.cache[require.resolve(__dirname + path + '/config')];
|
|
36
|
+
const newConfig = require(__dirname + path + '/config');
|
|
37
|
+
|
|
38
|
+
console.log('\nWith environment variables:');
|
|
39
|
+
console.log('MQTT parseMessages:', newConfig.mqtt.parseMessages);
|
|
40
|
+
console.log('Kafka enabled:', newConfig.kafka.enabled);
|
|
41
|
+
console.log('Kafka brokers:', newConfig.kafka.brokers);
|
|
42
|
+
console.log('Kafka groupId:', newConfig.kafka.groupId);
|
|
43
|
+
console.log('Kafka SASL mechanism:', newConfig.kafka.sasl.mechanism);
|
|
44
|
+
console.log('Kafka SASL username:', newConfig.kafka.sasl.username);
|
|
45
|
+
console.log('Kafka SASL password:', newConfig.kafka.sasl.password ? '[SET]' : '[NOT SET]');
|
|
46
|
+
|
|
47
|
+
console.log('✓ Configuration values test passed!');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function testKafkaConsumerModule() {
|
|
51
|
+
console.log('\n=== Testing Kafka Consumer Module Structure ===');
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const kafkaConsumer = require(__dirname + path + '/src/kafka/consumer.js');
|
|
55
|
+
|
|
56
|
+
// Test module exports
|
|
57
|
+
console.log('Module has init method:', typeof kafkaConsumer.init === 'function');
|
|
58
|
+
console.log('Module has start method:', typeof kafkaConsumer.start === 'function');
|
|
59
|
+
console.log('Module has stop method:', typeof kafkaConsumer.stop === 'function');
|
|
60
|
+
console.log('Module has isRunning method:', typeof kafkaConsumer.isRunning === 'function');
|
|
61
|
+
|
|
62
|
+
// Test initial state
|
|
63
|
+
console.log('Initial running state:', kafkaConsumer.isRunning());
|
|
64
|
+
|
|
65
|
+
// Test with disabled Kafka
|
|
66
|
+
const configDisabled = { kafka: { enabled: false } };
|
|
67
|
+
await kafkaConsumer.init(configDisabled, ['test']);
|
|
68
|
+
console.log('Init with disabled config succeeded');
|
|
69
|
+
|
|
70
|
+
console.log('✓ Kafka consumer module structure test passed!');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('✗ Kafka consumer module test failed:', error.message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function testMQTTMessageParsing() {
|
|
77
|
+
console.log('\n=== Testing MQTT Message Parsing Logic ===');
|
|
78
|
+
|
|
79
|
+
// Test the logic that would be used in index.js
|
|
80
|
+
const testConfigs = [
|
|
81
|
+
{ mqtt: { parseMessages: true } },
|
|
82
|
+
{ mqtt: { parseMessages: false } }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const config of testConfigs) {
|
|
86
|
+
console.log(`\nTesting with parseMessages: ${config.mqtt.parseMessages}`);
|
|
87
|
+
|
|
88
|
+
// Simulate the MQTT message handler logic
|
|
89
|
+
const mockTopic = "test/uid:123/status";
|
|
90
|
+
const mockPayload = "online";
|
|
91
|
+
const mockPacket = { retain: false };
|
|
92
|
+
|
|
93
|
+
if (config.mqtt.parseMessages) {
|
|
94
|
+
console.log('Would call device.parseMessage');
|
|
95
|
+
await $.device.parseMessage(null, mockTopic, mockPayload, mockPacket.retain);
|
|
96
|
+
} else {
|
|
97
|
+
console.log('Would skip device.parseMessage');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('✓ MQTT message parsing logic test passed!');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function runTests() {
|
|
105
|
+
console.log('Starting Kafka Integration Tests...');
|
|
106
|
+
|
|
107
|
+
await testConfigurationValues();
|
|
108
|
+
await testKafkaConsumerModule();
|
|
109
|
+
await testMQTTMessageParsing();
|
|
110
|
+
|
|
111
|
+
console.log('\n=== All Tests Completed Successfully ===');
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
runTests().catch(error => {
|
|
116
|
+
console.error('Test suite failed:', error);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
$ = {};
|
|
4
|
+
var path = "/../..";
|
|
5
|
+
$.config = require(__dirname + path + '/config');
|
|
6
|
+
$.kafka_consumer = require(__dirname + path + '/src/kafka/consumer.js');
|
|
7
|
+
$.device = {
|
|
8
|
+
parseMessage: async (client, topic, payload, retain) => {
|
|
9
|
+
console.log('Mock parseMessage called:', { topic, payload, retain });
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function testKafkaConsumerInit() {
|
|
14
|
+
console.log('\n=== Testing Kafka Consumer Initialization ===');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Test with Kafka disabled
|
|
18
|
+
console.log('1. Testing with Kafka disabled...');
|
|
19
|
+
const configDisabled = { ...$.config, kafka: { ...$.config.kafka, enabled: false } };
|
|
20
|
+
await $.kafka_consumer.init(configDisabled, ['test-project']);
|
|
21
|
+
console.log('✓ Kafka consumer init with disabled config passed');
|
|
22
|
+
|
|
23
|
+
// Test with Kafka enabled but no authentication
|
|
24
|
+
console.log('2. Testing with Kafka enabled (no auth)...');
|
|
25
|
+
const configEnabled = {
|
|
26
|
+
...$.config,
|
|
27
|
+
kafka: {
|
|
28
|
+
...$.config.kafka,
|
|
29
|
+
enabled: true,
|
|
30
|
+
sasl: { username: '', password: '' }
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
await $.kafka_consumer.init(configEnabled, ['test-project']);
|
|
34
|
+
console.log('✓ Kafka consumer init with enabled config passed');
|
|
35
|
+
|
|
36
|
+
// Test isRunning method
|
|
37
|
+
console.log('3. Testing isRunning method...');
|
|
38
|
+
console.log('Is running:', $.kafka_consumer.isRunning());
|
|
39
|
+
console.log('✓ isRunning method works');
|
|
40
|
+
|
|
41
|
+
console.log('\n✓ All Kafka consumer tests passed!');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('✗ Kafka consumer test failed:', error.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function testConfiguration() {
|
|
48
|
+
console.log('\n=== Testing Configuration ===');
|
|
49
|
+
|
|
50
|
+
console.log('MQTT parseMessages:', $.config.mqtt.parseMessages);
|
|
51
|
+
console.log('Kafka enabled:', $.config.kafka.enabled);
|
|
52
|
+
console.log('Kafka brokers:', $.config.kafka.brokers);
|
|
53
|
+
console.log('Kafka groupId:', $.config.kafka.groupId);
|
|
54
|
+
console.log('Kafka SASL mechanism:', $.config.kafka.sasl.mechanism);
|
|
55
|
+
|
|
56
|
+
console.log('✓ Configuration test passed!');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function runTests() {
|
|
60
|
+
console.log('Starting Kafka Consumer Tests...');
|
|
61
|
+
|
|
62
|
+
await testConfiguration();
|
|
63
|
+
await testKafkaConsumerInit();
|
|
64
|
+
|
|
65
|
+
console.log('\n=== All Tests Completed ===');
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
runTests().catch(error => {
|
|
70
|
+
console.error('Test suite failed:', error);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|