@itentialopensource/adapter-kafkav2 0.3.12
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/.eslintignore +6 -0
- package/.eslintrc.js +18 -0
- package/.gitlab/.gitkeep +0 -0
- package/.gitlab/issue_templates/.gitkeep +0 -0
- package/.gitlab/issue_templates/Default.md +17 -0
- package/.gitlab/issue_templates/bugReportTemplate.md +76 -0
- package/.gitlab/issue_templates/featureRequestTemplate.md +14 -0
- package/.jshintrc +0 -0
- package/CHANGELOG.md +152 -0
- package/CODE_OF_CONDUCT.md +48 -0
- package/CONTRIBUTING.md +158 -0
- package/LICENSE +201 -0
- package/README.md +380 -0
- package/Release_Notes.md +0 -0
- package/adapter.js +814 -0
- package/error.json +178 -0
- package/package.json +71 -0
- package/pronghorn.json +44 -0
- package/propertiesSchema.json +198 -0
- package/refs?service=git-upload-pack +0 -0
- package/sampleProperties.json +41 -0
- package/test/integration/adapterTestIntegration.js +445 -0
- package/test/unit/adapterTestUnit.js +412 -0
- package/utils/artifactize.js +146 -0
- package/utils/packModificationScript.js +35 -0
- package/utils/pre-commit.sh +27 -0
- package/utils/setup.js +33 -0
- package/utils/testRunner.js +298 -0
- package/workflows/kafka_test.json +1 -0
package/adapter.js
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
/* @copyright Itential, LLC 2019 (pre-modifications) */
|
|
2
|
+
|
|
3
|
+
// Set globals
|
|
4
|
+
/* global eventSystem log */
|
|
5
|
+
/* eslint no-underscore-dangle: warn */
|
|
6
|
+
/* eslint no-loop-func: warn */
|
|
7
|
+
/* eslint no-cond-assign: warn */
|
|
8
|
+
/* eslint no-unused-vars: warn */
|
|
9
|
+
/* eslint consistent-return: warn */
|
|
10
|
+
/* eslint import/no-dynamic-require: warn */
|
|
11
|
+
/* eslint global-require: warn */
|
|
12
|
+
/* eslint no-await-in-loop: warn */
|
|
13
|
+
|
|
14
|
+
/* Required libraries. */
|
|
15
|
+
const fs = require('fs-extra');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const util = require('util');
|
|
18
|
+
/* Fetch in the other needed components for the this Adaptor */
|
|
19
|
+
const EventEmitterCl = require('events').EventEmitter;
|
|
20
|
+
const request = require('request');
|
|
21
|
+
|
|
22
|
+
function consoleLoggerProvider(name) {
|
|
23
|
+
return {
|
|
24
|
+
debug: log.debug,
|
|
25
|
+
info: log.info,
|
|
26
|
+
warn: log.warn,
|
|
27
|
+
error: log.error
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { Kafka, logLevel } = require('kafkajs');
|
|
32
|
+
|
|
33
|
+
const pjson = require(path.resolve(__dirname, 'pronghorn.json'));
|
|
34
|
+
|
|
35
|
+
let myid = null;
|
|
36
|
+
let errors = [];
|
|
37
|
+
let firstRun = false;
|
|
38
|
+
let firstDone = false;
|
|
39
|
+
const mytopics = Object.keys(pjson.topics);
|
|
40
|
+
const pronghornFile = path.join(__dirname, '/pronghorn.json');
|
|
41
|
+
|
|
42
|
+
async function fetchSchema(registryUrl, topic, version) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
log.debug(`${registryUrl}/subjects/${topic}/versions/${version}`);
|
|
45
|
+
try {
|
|
46
|
+
request(
|
|
47
|
+
`${registryUrl}/subjects/${topic}/versions/${version}`,
|
|
48
|
+
(err, res, body) => {
|
|
49
|
+
if (err) {
|
|
50
|
+
log.debug('schema fetch - evaluating error');
|
|
51
|
+
try {
|
|
52
|
+
const error = JSON.parse(err);
|
|
53
|
+
return reject(
|
|
54
|
+
new Error(
|
|
55
|
+
`Schema registry error: ${error.error_code} - ${error.message}`
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
} catch (ex) {
|
|
59
|
+
return reject(
|
|
60
|
+
new Error(
|
|
61
|
+
`Schema registry error: ${err}`
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!res || !res.statusCode || res.statusCode !== 200) {
|
|
67
|
+
log.debug('schema fetch - Evaluating error response');
|
|
68
|
+
return reject(
|
|
69
|
+
new Error(
|
|
70
|
+
'Schema registry error: Did not receive a valid status code'
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
// this is the schema response
|
|
75
|
+
log.debug('returning schema response');
|
|
76
|
+
return resolve(JSON.parse(body));
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
log.error(`error sending request: ${e} \n${e.stack}`);
|
|
81
|
+
return reject(e);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @summary Build a standard error object from the data provided
|
|
88
|
+
*
|
|
89
|
+
* @function formatErrorObject
|
|
90
|
+
* @param {String} origin - the originator of the error (optional).
|
|
91
|
+
* @param {String} type - the internal error type (optional).
|
|
92
|
+
* @param {String} variables - the variables to put into the error message (optional).
|
|
93
|
+
* @param {Integer} sysCode - the error code from the other system (optional).
|
|
94
|
+
* @param {Object} sysRes - the raw response from the other system (optional).
|
|
95
|
+
* @param {Exception} stack - any available stack trace from the issue (optional).
|
|
96
|
+
*
|
|
97
|
+
* @return {Object} - the error object, null if missing pertinent information
|
|
98
|
+
*/
|
|
99
|
+
function formatErrorObject(origin, type, variables, sysCode, sysRes, stack) {
|
|
100
|
+
log.trace(`${myid}-adapter-formatErrorObject`);
|
|
101
|
+
|
|
102
|
+
// add the required fields
|
|
103
|
+
const errorObject = {
|
|
104
|
+
icode: 'AD.999',
|
|
105
|
+
IAPerror: {
|
|
106
|
+
origin: `${myid}-unidentified`,
|
|
107
|
+
displayString: 'error not provided',
|
|
108
|
+
recommendation: 'report this issue to the adapter team!'
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (origin) {
|
|
113
|
+
errorObject.IAPerror.origin = origin;
|
|
114
|
+
}
|
|
115
|
+
if (type) {
|
|
116
|
+
errorObject.IAPerror.displayString = type;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// add the messages from the error.json
|
|
120
|
+
for (let e = 0; e < errors.length; e += 1) {
|
|
121
|
+
if (errors[e].key === type) {
|
|
122
|
+
errorObject.icode = errors[e].icode;
|
|
123
|
+
errorObject.IAPerror.displayString = errors[e].displayString;
|
|
124
|
+
errorObject.IAPerror.recommendation = errors[e].recommendation;
|
|
125
|
+
} else if (errors[e].icode === type) {
|
|
126
|
+
errorObject.icode = errors[e].icode;
|
|
127
|
+
errorObject.IAPerror.displayString = errors[e].displayString;
|
|
128
|
+
errorObject.IAPerror.recommendation = errors[e].recommendation;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// replace the variables
|
|
133
|
+
let varCnt = 0;
|
|
134
|
+
while (errorObject.IAPerror.displayString.indexOf('$VARIABLE$') >= 0) {
|
|
135
|
+
let curVar = '';
|
|
136
|
+
|
|
137
|
+
// get the current variable
|
|
138
|
+
if (variables && Array.isArray(variables) && variables.length >= varCnt + 1) {
|
|
139
|
+
curVar = variables[varCnt];
|
|
140
|
+
}
|
|
141
|
+
varCnt += 1;
|
|
142
|
+
errorObject.IAPerror.displayString = errorObject.IAPerror.displayString.replace('$VARIABLE$', curVar);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// add all of the optional fields
|
|
146
|
+
if (sysCode) {
|
|
147
|
+
errorObject.IAPerror.code = sysCode;
|
|
148
|
+
}
|
|
149
|
+
if (sysRes) {
|
|
150
|
+
errorObject.IAPerror.raw_response = sysRes;
|
|
151
|
+
}
|
|
152
|
+
if (stack) {
|
|
153
|
+
errorObject.IAPerror.stack = stack;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// return the object
|
|
157
|
+
return errorObject;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* This is the adapter/interface into Kafka
|
|
162
|
+
*/
|
|
163
|
+
class Kafkav2 extends EventEmitterCl {
|
|
164
|
+
/**
|
|
165
|
+
* Kafka Adapter
|
|
166
|
+
* @constructor
|
|
167
|
+
*/
|
|
168
|
+
constructor(prongid, properties) {
|
|
169
|
+
super();
|
|
170
|
+
|
|
171
|
+
this.props = properties;
|
|
172
|
+
this.alive = false;
|
|
173
|
+
this.healthy = false;
|
|
174
|
+
this.id = prongid;
|
|
175
|
+
myid = prongid;
|
|
176
|
+
|
|
177
|
+
// put topics from file into memory
|
|
178
|
+
this.topicsEvents = [];
|
|
179
|
+
const topicsFile = path.join(__dirname, '/.topics.json');
|
|
180
|
+
|
|
181
|
+
// if the file does not exist - error
|
|
182
|
+
if (fs.existsSync(topicsFile)) {
|
|
183
|
+
this.topicsEvents = JSON.parse(fs.readFileSync(topicsFile, 'utf-8'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// get the topics into the right format --- fix older .topics.json files so they have the right format
|
|
187
|
+
if (!Array.isArray(this.topicsEvents)) {
|
|
188
|
+
// original format
|
|
189
|
+
const keys = Object.keys(this.topicsEvents);
|
|
190
|
+
const tempTops = [];
|
|
191
|
+
keys.forEach((item) => {
|
|
192
|
+
tempTops.push({
|
|
193
|
+
topic: item,
|
|
194
|
+
partition: this.topicsEvents[item].partitions || this.topicsEvents[item].partition || 0,
|
|
195
|
+
offset: this.topicsEvents[item].offset || 0,
|
|
196
|
+
processed: this.topicsEvents[item].offset || 0,
|
|
197
|
+
subscribers: this.topicsEvents[item].subscribers || 0,
|
|
198
|
+
avro: this.topicsEvents[item].avro || 'NO',
|
|
199
|
+
subInfo: [
|
|
200
|
+
{
|
|
201
|
+
subname: 'default',
|
|
202
|
+
filters: [],
|
|
203
|
+
rabbit: 'kafka',
|
|
204
|
+
throttle: {}
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
this.topicsEvents = tempTops;
|
|
210
|
+
} else {
|
|
211
|
+
for (let t = 0; t < this.topicsEvents.length; t += 1) {
|
|
212
|
+
if (!Object.hasOwnProperty.call(this.topicsEvents[t], 'processed')) {
|
|
213
|
+
this.topicsEvents[t].processed = this.topicsEvents[t].offset || 0;
|
|
214
|
+
}
|
|
215
|
+
if (!Object.hasOwnProperty.call(this.topicsEvents[t], 'subInfo')) {
|
|
216
|
+
this.topicsEvents[t].subInfo = [
|
|
217
|
+
{
|
|
218
|
+
subname: 'default',
|
|
219
|
+
filters: [],
|
|
220
|
+
rabbit: 'kafka',
|
|
221
|
+
throttle: {}
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// If we have a rabbit queue - need to add that to topics.json
|
|
229
|
+
// if there are topics in the properties
|
|
230
|
+
if (this.props && this.props.topics) {
|
|
231
|
+
let needRestart = false;
|
|
232
|
+
for (let t = 0; t < this.props.topics.length; t += 1) {
|
|
233
|
+
if (typeof this.props.topics[t] === 'string') {
|
|
234
|
+
const topName = this.props.topics[t];
|
|
235
|
+
if (!mytopics.includes(topName)) {
|
|
236
|
+
pjson.topics[this.props.topics[t]] = {};
|
|
237
|
+
needRestart = true;
|
|
238
|
+
}
|
|
239
|
+
for (let te = 0; te < this.topicsEvents.length; te += 1) {
|
|
240
|
+
// only set the rabbit in the topic if it is the default (kafka)
|
|
241
|
+
if (this.topicsEvents[te].topic === topName && this.topicsEvents[te].partition === 0
|
|
242
|
+
&& this.topicsEvents[te].subInfo.rabbit === 'kafka') {
|
|
243
|
+
this.topicsEvents[te].subInfo.rabbit = topName;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else if (typeof this.props.topics[t] === 'object') {
|
|
248
|
+
const topName = this.props.topics[t].name;
|
|
249
|
+
// if the topic is not in the pronghorn.json
|
|
250
|
+
if (!mytopics.includes(topName)) {
|
|
251
|
+
pjson.topics[topName] = {};
|
|
252
|
+
needRestart = true;
|
|
253
|
+
}
|
|
254
|
+
const topPart = this.props.topics[t].partition || 0;
|
|
255
|
+
let useAvro = 'NO';
|
|
256
|
+
if (this.props.topics[t].avro !== undefined && this.props.topics[t].avro !== null && this.props.topics[t].avro === true) {
|
|
257
|
+
useAvro = 'YES';
|
|
258
|
+
}
|
|
259
|
+
let subInfo = [
|
|
260
|
+
{
|
|
261
|
+
subname: 'default',
|
|
262
|
+
filters: [],
|
|
263
|
+
rabbit: 'kafka',
|
|
264
|
+
throttle: {}
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
if (this.props.topics[t].subscriberInfo) {
|
|
268
|
+
subInfo = this.props.topics[t].subscriberInfo;
|
|
269
|
+
for (let r = 0; r < subInfo.length; r += 1) {
|
|
270
|
+
// if the rabbit topic is not in the pronghorn.json
|
|
271
|
+
if (!mytopics.includes(subInfo[r].rabbit)) {
|
|
272
|
+
pjson.topics[subInfo[r].rabbit] = {};
|
|
273
|
+
needRestart = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
let found = false;
|
|
278
|
+
for (let te = 0; te < this.topicsEvents.length; te += 1) {
|
|
279
|
+
// see if we find the topic and partition
|
|
280
|
+
if (this.topicsEvents[te].topic === topName && this.topicsEvents[te].partition === topPart) {
|
|
281
|
+
found = true;
|
|
282
|
+
// only set the rabbit in the topic if it is the default (kafka)
|
|
283
|
+
if (this.topicsEvents[te].subInfo.rabbit === 'kafka') {
|
|
284
|
+
this.topicsEvents[te].subInfo.rabbit = topName;
|
|
285
|
+
}
|
|
286
|
+
// set the subscribers if always
|
|
287
|
+
if (this.props.topics[t].always) {
|
|
288
|
+
this.topicsEvents[te].subscribers = 99999;
|
|
289
|
+
}
|
|
290
|
+
// set the avro accordingly
|
|
291
|
+
this.topicsEvents[te].avro = useAvro;
|
|
292
|
+
|
|
293
|
+
// check to see if we need to add a subscriber
|
|
294
|
+
for (let sub = 0; sub < subInfo.length; sub += 1) {
|
|
295
|
+
let subFound = false;
|
|
296
|
+
for (let s = 0; s <= this.topicsEvents[te].subInfo.length; s += 1) {
|
|
297
|
+
// if the subscriber is found, no need to add anything
|
|
298
|
+
if (subInfo[sub].subname === this.topicsEvents[te].subInfo[s].subname) {
|
|
299
|
+
subFound = true;
|
|
300
|
+
// set the filters
|
|
301
|
+
if (subInfo[sub].filters) {
|
|
302
|
+
this.topicsEvents[te].subInfo[s].filters = subInfo[sub].filters;
|
|
303
|
+
}
|
|
304
|
+
// set the rabbit
|
|
305
|
+
if (subInfo[sub].rabbit) {
|
|
306
|
+
this.topicsEvents[te].subInfo[s].rabbit = subInfo[sub].rabbit;
|
|
307
|
+
}
|
|
308
|
+
// set the throttle
|
|
309
|
+
if (subInfo[sub].throttle) {
|
|
310
|
+
this.topicsEvents[te].subInfo[s].throttle = subInfo[sub].throttle;
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!subFound) {
|
|
316
|
+
// add the subscriber if it was not found
|
|
317
|
+
this.topicsEvents[te].subInfo.push(subInfo[sub]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// if the topic was not found but should always be subscribed to, add it
|
|
325
|
+
if (!found && this.props.topics[t].always) {
|
|
326
|
+
this.topicsEvents.push({
|
|
327
|
+
topic: topName,
|
|
328
|
+
partition: topPart,
|
|
329
|
+
offset: 0,
|
|
330
|
+
processed: 0,
|
|
331
|
+
subscribers: 99999,
|
|
332
|
+
avro: useAvro,
|
|
333
|
+
subInfo
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// if we need to restart the adapter due to changes to pronghorn.json
|
|
340
|
+
if (needRestart) {
|
|
341
|
+
fs.writeFileSync(pronghornFile, JSON.stringify(pjson, null, 2));
|
|
342
|
+
log.error('NEED TO RESTART ADAPTER - EXITING');
|
|
343
|
+
const errorObj = {
|
|
344
|
+
origin: `${this.id}-adapter-constructor`,
|
|
345
|
+
type: 'Restarting to add new topics',
|
|
346
|
+
vars: []
|
|
347
|
+
};
|
|
348
|
+
// log and throw the error
|
|
349
|
+
log.error(`${errorObj.origin}: ${errorObj.displayString}`);
|
|
350
|
+
setTimeout(() => {
|
|
351
|
+
throw new Error(JSON.stringify(errorObj));
|
|
352
|
+
}, 1000);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this.KafkaClient = null;
|
|
357
|
+
this.producer = null;
|
|
358
|
+
this.consumer = null;
|
|
359
|
+
this.registryUrl = null;
|
|
360
|
+
this.registry = 'abc';
|
|
361
|
+
if (this.props && this.props.registry_url) {
|
|
362
|
+
this.registryUrl = this.props.registry_url;
|
|
363
|
+
this.registry = require('avro-schema-registry')(this.props.registry_url);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// get the path for the specific error file
|
|
367
|
+
const errorFile = path.join(__dirname, '/error.json');
|
|
368
|
+
|
|
369
|
+
// if the file does not exist - error
|
|
370
|
+
if (!fs.existsSync(errorFile)) {
|
|
371
|
+
const origin = `${this.id}-adapter-constructor`;
|
|
372
|
+
log.warn(`${origin}: Could not locate ${errorFile} - errors will be missing details`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Read the action from the file system
|
|
376
|
+
const errorData = JSON.parse(fs.readFileSync(errorFile, 'utf-8'));
|
|
377
|
+
({ errors } = errorData);
|
|
378
|
+
|
|
379
|
+
// rewrite the topics file for persistence
|
|
380
|
+
if (this.props && this.props.stub === false) {
|
|
381
|
+
const intTime = this.props.interval_time || 30000;
|
|
382
|
+
setInterval(() => {
|
|
383
|
+
try {
|
|
384
|
+
log.debug('Update .topics.json file');
|
|
385
|
+
fs.writeFileSync(topicsFile, JSON.stringify(this.topicsEvents, null, 2));
|
|
386
|
+
} catch (err) {
|
|
387
|
+
log.error(err);
|
|
388
|
+
}
|
|
389
|
+
}, intTime);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @summary Connect function is used during Pronghorn startup to provide instantiation feedback.
|
|
395
|
+
*
|
|
396
|
+
* @function connect
|
|
397
|
+
*/
|
|
398
|
+
connect() {
|
|
399
|
+
const meth = 'adapter-connect';
|
|
400
|
+
const origin = `${this.id}-${meth}`;
|
|
401
|
+
log.trace(origin);
|
|
402
|
+
|
|
403
|
+
// initially set as off
|
|
404
|
+
this.emit('OFFLINE', { id: this.id });
|
|
405
|
+
this.alive = true;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
// Create a kafka client
|
|
409
|
+
const combinedProps = this.props.client || {};
|
|
410
|
+
if (this.props.client.ssl.ca) {
|
|
411
|
+
combinedProps.ssl.ca = [fs.readFileSync(this.props.client.ssl.ca, 'utf-8')];
|
|
412
|
+
}
|
|
413
|
+
if (this.props.client.ssl.cert) {
|
|
414
|
+
combinedProps.ssl.cert = fs.readFileSync(this.props.client.ssl.cert, 'utf-8');
|
|
415
|
+
}
|
|
416
|
+
if (this.props.client.ssl.key) {
|
|
417
|
+
combinedProps.ssl.key = fs.readFileSync(this.props.client.ssl.key, 'utf-8');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
this.KafkaClient = new Kafka(combinedProps);
|
|
421
|
+
|
|
422
|
+
this.producer = this.KafkaClient.producer(this.props.producer || {});
|
|
423
|
+
|
|
424
|
+
this.consumer = this.KafkaClient.consumer(this.props.consumer || {});
|
|
425
|
+
|
|
426
|
+
const isPassingFiltering = (topic, message) => {
|
|
427
|
+
const topicConfig = this.topicsEvents.find((e) => e.topic === topic);
|
|
428
|
+
|
|
429
|
+
if (!topicConfig) return true;
|
|
430
|
+
|
|
431
|
+
if (!topicConfig.subInfo || !Array.isArray(topicConfig.subInfo)) return true;
|
|
432
|
+
|
|
433
|
+
const allFilters = topicConfig.subInfo.filter((e) => Array.isArray(e.filters) && e.filters.length > 0);
|
|
434
|
+
if (allFilters.length === 0) return true;
|
|
435
|
+
const matched = allFilters.find((s) => {
|
|
436
|
+
for (let f = 0; f < s.filters.length; f += 1) {
|
|
437
|
+
const re = new RegExp(s.filters[f]);
|
|
438
|
+
if (message.value.toString().match(re)) {
|
|
439
|
+
log.debug('Matched filter: ', re);
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
});
|
|
445
|
+
return matched;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const run = async () => {
|
|
449
|
+
this.emit('ONLINE', {
|
|
450
|
+
id: this.id
|
|
451
|
+
});
|
|
452
|
+
log.info('EMITTED ONLINE');
|
|
453
|
+
// Start consuming
|
|
454
|
+
// Iterate through all topics in topicsEvents
|
|
455
|
+
const topics = [];
|
|
456
|
+
for (let t = 0; t < this.topicsEvents.length; t += 1) {
|
|
457
|
+
topics.push(this.topicsEvents[t].topic);
|
|
458
|
+
}
|
|
459
|
+
await this.consumer.connect();
|
|
460
|
+
await this.consumer.subscribe({ topics, fromBeginning: true });
|
|
461
|
+
await this.consumer.run({
|
|
462
|
+
eachMessage: async ({ topic, partition, message }) => {
|
|
463
|
+
log.info(`PROCESSING NEW MESSAGE ON TOPIC: ${topic} PARTITION: ${partition} WITH OFFSET: ${message.offset}`);
|
|
464
|
+
const newMsg = message;
|
|
465
|
+
let avroUsed = false;
|
|
466
|
+
let desiredTopic = 'kafka';
|
|
467
|
+
|
|
468
|
+
// find the topic here to change the offset
|
|
469
|
+
for (let t = 0; t < this.topicsEvents.length; t += 1) {
|
|
470
|
+
if (this.topicsEvents[t].topic === topic && this.topicsEvents[t].partition === partition) {
|
|
471
|
+
if (this.topicsEvents[t].offset < message.offset) {
|
|
472
|
+
this.topicsEvents[t].offset = message.offset;
|
|
473
|
+
}
|
|
474
|
+
// determine if we are using AVRO and set flag
|
|
475
|
+
if (this.topicsEvents[t].avro && this.topicsEvents[t].avro.toUpperCase() === 'YES') {
|
|
476
|
+
avroUsed = true;
|
|
477
|
+
}
|
|
478
|
+
// set desired topic
|
|
479
|
+
if (this.topicsEvents[t].subInfo && this.topicsEvents[t].subInfo.rabbit) {
|
|
480
|
+
desiredTopic = this.topicsEvents[t].subInfo.rabbit;
|
|
481
|
+
} else {
|
|
482
|
+
desiredTopic = this.topicsEvents[t].topic;
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// if using avro
|
|
489
|
+
if (this.registry && avroUsed) {
|
|
490
|
+
// will give us an interval between 5001 and 60000 milliseconds (5 seconds to 1 minute)
|
|
491
|
+
const fastInt = Math.floor(Math.random() * 55000) + 5001;
|
|
492
|
+
|
|
493
|
+
// This interval is to help prevent the adapter from bombarding the registry by sending requests at random intervals
|
|
494
|
+
const intervalObject = setInterval(async () => {
|
|
495
|
+
// Prevents running the request while the first one is running
|
|
496
|
+
if (!firstRun) {
|
|
497
|
+
if (!firstDone) {
|
|
498
|
+
firstRun = true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
log.debug('GET AVRO KEY');
|
|
503
|
+
newMsg.key = await this.registry.decode(newMsg.key);
|
|
504
|
+
log.debug(`AVRO KEY: ${newMsg.key}`);
|
|
505
|
+
newMsg.value = await this.registry.decode(newMsg.value);
|
|
506
|
+
log.debug(`AVRO MSG: ${newMsg.value}`);
|
|
507
|
+
firstDone = true;
|
|
508
|
+
firstRun = false;
|
|
509
|
+
clearInterval(intervalObject);
|
|
510
|
+
} catch (ex) {
|
|
511
|
+
log.warn(`Had issue getting registry will try again in ${fastInt} milliseconds`);
|
|
512
|
+
firstRun = false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}, fastInt);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const goodMsg = isPassingFiltering(topic, newMsg);
|
|
519
|
+
if (goodMsg) {
|
|
520
|
+
log.info(`Message: '${message.value.toString()}' being CONSUMED as it does meet the filter condition`);
|
|
521
|
+
if (mytopics.includes(desiredTopic)) {
|
|
522
|
+
log.info(`Emitting: ${message.value.toString()} TO ${desiredTopic}`);
|
|
523
|
+
} else {
|
|
524
|
+
log.info(`The desired topic ${desiredTopic} does not exist. If it should, make sure it is in pronghorn.json`);
|
|
525
|
+
log.info(`Emitting: ${newMsg.value.toString()} TO kafka`);
|
|
526
|
+
desiredTopic = 'kafka';
|
|
527
|
+
}
|
|
528
|
+
if (this.props.parseMessage) {
|
|
529
|
+
let messageObj;
|
|
530
|
+
const wrapper = this.props.wrapMessage || 'payload';
|
|
531
|
+
try {
|
|
532
|
+
messageObj = { [wrapper]: JSON.parse(newMsg.value) };
|
|
533
|
+
} catch (ex) {
|
|
534
|
+
messageObj = { [wrapper]: newMsg.value.toString() };
|
|
535
|
+
}
|
|
536
|
+
eventSystem.publish(desiredTopic, messageObj);
|
|
537
|
+
} else {
|
|
538
|
+
eventSystem.publish(desiredTopic, newMsg.value.toString());
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
log.info(`Message: '${newMsg.value.toString()}' being DROPPED as it fails to meet any filter condition`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
run().catch((err) => console.error(`[consumer] ${err.message}`, err));
|
|
547
|
+
|
|
548
|
+
const errorTypes = ['unhandledRejection', 'uncaughtException'];
|
|
549
|
+
const signalTraps = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
|
|
550
|
+
|
|
551
|
+
errorTypes.forEach((type) => {
|
|
552
|
+
process.on(type, async (e) => {
|
|
553
|
+
try {
|
|
554
|
+
console.log(`process.on ${type}`);
|
|
555
|
+
console.error(e);
|
|
556
|
+
await this.consumer.disconnect();
|
|
557
|
+
process.exit(0);
|
|
558
|
+
} catch (_) {
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
signalTraps.forEach((type) => {
|
|
565
|
+
process.once(type, async () => {
|
|
566
|
+
try {
|
|
567
|
+
await this.consumer.disconnect();
|
|
568
|
+
} finally {
|
|
569
|
+
process.kill(process.pid, type);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
} catch (ex) {
|
|
574
|
+
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
|
|
575
|
+
log.error(JSON.stringify(ex));
|
|
576
|
+
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
|
|
577
|
+
return (null, errorObj);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @summary HealthCheck function is used to provide Pronghorn the status of this adapter.
|
|
583
|
+
*
|
|
584
|
+
* @function healthCheck
|
|
585
|
+
* @param callback - a callback function to return the result id and status
|
|
586
|
+
*/
|
|
587
|
+
healthCheck(callback) {
|
|
588
|
+
// need to work on this later
|
|
589
|
+
const retstatus = { id: this.id, status: 'success' };
|
|
590
|
+
return callback(retstatus);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* getAllFunctions is used to get all of the exposed function in the adapter
|
|
595
|
+
*
|
|
596
|
+
* @function getAllFunctions
|
|
597
|
+
*/
|
|
598
|
+
getAllFunctions() {
|
|
599
|
+
let myfunctions = [];
|
|
600
|
+
let obj = this;
|
|
601
|
+
|
|
602
|
+
// find the functions in this class
|
|
603
|
+
do {
|
|
604
|
+
const l = Object.getOwnPropertyNames(obj)
|
|
605
|
+
.concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString()))
|
|
606
|
+
.sort()
|
|
607
|
+
.filter((p, i, arr) => typeof obj[p] === 'function' && p !== 'constructor' && (i === 0 || p !== arr[i - 1]) && myfunctions.indexOf(p) === -1);
|
|
608
|
+
myfunctions = myfunctions.concat(l);
|
|
609
|
+
}
|
|
610
|
+
while (
|
|
611
|
+
(obj = Object.getPrototypeOf(obj)) && Object.getPrototypeOf(obj)
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
return myfunctions;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* getWorkflowFunctions is used to get all of the workflow function in the adapter
|
|
619
|
+
*
|
|
620
|
+
* @function getWorkflowFunctions
|
|
621
|
+
*/
|
|
622
|
+
getWorkflowFunctions() {
|
|
623
|
+
const myIgnore = [
|
|
624
|
+
'subscribeAvroWithSubscriber',
|
|
625
|
+
'subscribeWithSubscriber'
|
|
626
|
+
];
|
|
627
|
+
|
|
628
|
+
const myfunctions = this.getAllFunctions();
|
|
629
|
+
const wffunctions = [];
|
|
630
|
+
|
|
631
|
+
// remove the functions that should not be in a Workflow
|
|
632
|
+
for (let m = 0; m < myfunctions.length; m += 1) {
|
|
633
|
+
if (myfunctions[m] === 'addListener') {
|
|
634
|
+
// got to the second tier (adapterBase)
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
if (myfunctions[m] !== 'connect' && myfunctions[m] !== 'healthCheck'
|
|
638
|
+
&& myfunctions[m] !== 'getAllFunctions' && myfunctions[m] !== 'getWorkflowFunctions') {
|
|
639
|
+
let found = false;
|
|
640
|
+
if (myIgnore && Array.isArray(myIgnore)) {
|
|
641
|
+
for (let i = 0; i < myIgnore.length; i += 1) {
|
|
642
|
+
if (myfunctions[m].toUpperCase() === myIgnore[i].toUpperCase()) {
|
|
643
|
+
found = true;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (!found) {
|
|
648
|
+
wffunctions.push(myfunctions[m]);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return wffunctions;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Call to send message.
|
|
658
|
+
* @function send
|
|
659
|
+
* @param payloads - array of ProduceRequest, ProduceRequest is a JSON object (required)
|
|
660
|
+
* @param callback - a callback function to return a result
|
|
661
|
+
*/
|
|
662
|
+
sendMessage(payloads, callback) {
|
|
663
|
+
const meth = 'adapter-sendMessage';
|
|
664
|
+
const origin = `${this.id}-${meth}`;
|
|
665
|
+
log.trace(origin);
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
if (!payloads) {
|
|
669
|
+
const errorObj = formatErrorObject(origin, 'Missing Data', ['payloads'], null, null, null);
|
|
670
|
+
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
|
|
671
|
+
return callback(null, errorObj);
|
|
672
|
+
}
|
|
673
|
+
if (!Array.isArray(payloads)) {
|
|
674
|
+
const errorObj = formatErrorObject(origin, 'Invalid payloads format - payloads must be an array', null, null, null, null);
|
|
675
|
+
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
|
|
676
|
+
return callback(null, errorObj);
|
|
677
|
+
}
|
|
678
|
+
if (!this.producer) {
|
|
679
|
+
const errorObj = formatErrorObject(origin, 'Producer not created', null, null, null, null);
|
|
680
|
+
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
|
|
681
|
+
return callback(null, errorObj);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const run = async () => {
|
|
685
|
+
try {
|
|
686
|
+
const topicMessages = payloads;
|
|
687
|
+
// need to go through the payloads that were provided
|
|
688
|
+
for (let p = 0; p < payloads.length; p += 1) {
|
|
689
|
+
if (!payloads[p].topic) {
|
|
690
|
+
const errorObj = formatErrorObject(origin, 'Missing Data', ['payloads[p].topic'], null, null, null);
|
|
691
|
+
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
|
|
692
|
+
return callback(null, errorObj);
|
|
693
|
+
}
|
|
694
|
+
if (!payloads[p].messages) {
|
|
695
|
+
const errorObj = formatErrorObject(origin, 'Missing Data', ['payloads[p].messages'], null, null, null);
|
|
696
|
+
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
|
|
697
|
+
return callback(null, errorObj);
|
|
698
|
+
}
|
|
699
|
+
topicMessages[p].messages = payloads[p].messages.map((message) => {
|
|
700
|
+
if (typeof message.value === 'object') {
|
|
701
|
+
return { ...message, value: JSON.stringify(message.value) };
|
|
702
|
+
}
|
|
703
|
+
return message;
|
|
704
|
+
});
|
|
705
|
+
if (this.registry && topicMessages[p].simple && topicMessages[p].simple.toUpperCase() === 'NO') {
|
|
706
|
+
log.debug(`Handling AVRO Message: ${JSON.stringify(topicMessages[p])}`);
|
|
707
|
+
let keyVer = 1;
|
|
708
|
+
let valVer = 1;
|
|
709
|
+
if (topicMessages[p].key_version) {
|
|
710
|
+
keyVer = topicMessages[p].key_version;
|
|
711
|
+
}
|
|
712
|
+
if (topicMessages[p].value_version) {
|
|
713
|
+
valVer = topicMessages[p].value_version;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// will give us an interval between 2501 and 10000 milliseconds (2.5 to 10 seconds)
|
|
717
|
+
const fastInt = Math.floor(Math.random() * 7500) + 2501;
|
|
718
|
+
let currValue = '';
|
|
719
|
+
let encodeKey = '';
|
|
720
|
+
const encodeKeys = [];
|
|
721
|
+
|
|
722
|
+
// This interval is to help prevent the adapter from bombarding the registry by sending requests at random intervals
|
|
723
|
+
const intervalObject = setInterval(async () => {
|
|
724
|
+
try {
|
|
725
|
+
// get the key and value for the topic of this message
|
|
726
|
+
log.spam('fetching schema values');
|
|
727
|
+
currValue = await fetchSchema(this.registryUrl, `${topicMessages[p].topic}-value`, valVer);
|
|
728
|
+
for (let m = 0; m < topicMessages[p].messages.length; m += 1) {
|
|
729
|
+
if (topicMessages[p].messages[m].key) {
|
|
730
|
+
let schKey = '';
|
|
731
|
+
log.spam('fetching schema keys');
|
|
732
|
+
schKey = await fetchSchema(this.registryUrl, `${topicMessages[p].topic}-key`, keyVer);
|
|
733
|
+
// encode the key
|
|
734
|
+
encodeKey = await this.registry.encodeKey(topicMessages[p].topic, schKey.schema, topicMessages[p].messages[m].key);
|
|
735
|
+
encodeKeys.push(encodeKey);
|
|
736
|
+
} else {
|
|
737
|
+
encodeKeys.push(null);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
clearInterval(intervalObject);
|
|
742
|
+
} catch (ex) {
|
|
743
|
+
log.warn(`Had issue getting registry will try again in ${fastInt} milliseconds`);
|
|
744
|
+
firstRun = false;
|
|
745
|
+
}
|
|
746
|
+
}, fastInt);
|
|
747
|
+
|
|
748
|
+
log.debug('Encoding message array');
|
|
749
|
+
for (let m = 0; m < topicMessages[p].messages.length; m += 1) {
|
|
750
|
+
let msg = '';
|
|
751
|
+
log.debug(`Attemping to encode message ${m}`);
|
|
752
|
+
try {
|
|
753
|
+
msg = await this.registry.encodeMessage(topicMessages[p].topic, currValue.schema, topicMessages[p].messages[m].value);
|
|
754
|
+
} catch (encodeErr) {
|
|
755
|
+
log.error(encodeErr.stack);
|
|
756
|
+
return callback(null, encodeErr);
|
|
757
|
+
}
|
|
758
|
+
if (topicMessages[p].messages[m].key) {
|
|
759
|
+
log.debug('Replacing key with encoded key');
|
|
760
|
+
topicMessages[p].messages[m].key = encodeKeys[m];
|
|
761
|
+
}
|
|
762
|
+
log.debug('Replacing message with encoded message');
|
|
763
|
+
topicMessages[p].messages[m].value = msg;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
await this.producer.connect();
|
|
768
|
+
await this.producer.sendBatch({ topicMessages });
|
|
769
|
+
} catch (err) {
|
|
770
|
+
const errorObj = formatErrorObject(origin, 'Producer sending message failed', null, null, null, err);
|
|
771
|
+
log.error('error: '.concat(err));
|
|
772
|
+
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
|
|
773
|
+
return callback(null, errorObj);
|
|
774
|
+
}
|
|
775
|
+
return callback({
|
|
776
|
+
status: 'success',
|
|
777
|
+
code: 200
|
|
778
|
+
});
|
|
779
|
+
};
|
|
780
|
+
run();
|
|
781
|
+
|
|
782
|
+
const errorTypes = ['unhandledRejection', 'uncaughtException'];
|
|
783
|
+
const signalTraps = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
|
|
784
|
+
|
|
785
|
+
errorTypes.forEach((type) => {
|
|
786
|
+
process.on(type, async () => {
|
|
787
|
+
try {
|
|
788
|
+
console.log(`process.on ${type}`);
|
|
789
|
+
await this.producer.disconnect();
|
|
790
|
+
process.exit(0);
|
|
791
|
+
} catch (_) {
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
signalTraps.forEach((type) => {
|
|
798
|
+
process.once(type, async () => {
|
|
799
|
+
try {
|
|
800
|
+
await this.producer.disconnect();
|
|
801
|
+
} finally {
|
|
802
|
+
process.kill(process.pid, type);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
} catch (ex) {
|
|
807
|
+
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
|
|
808
|
+
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
|
|
809
|
+
return callback(null, errorObj);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
module.exports = Kafkav2;
|