@lucaapp/service-utils 5.10.0 → 5.11.0
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/dist/lib/kafka/defaultPoisonHandler.d.ts +13 -0
- package/dist/lib/kafka/defaultPoisonHandler.js +37 -0
- package/dist/lib/kafka/index.d.ts +1 -1
- package/dist/lib/kafka/kafkaClient.d.ts +3 -2
- package/dist/lib/kafka/kafkaClient.js +34 -12
- package/dist/lib/kafka/types.d.ts +11 -2
- package/package.json +5 -4
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CustomTopicPoisonHandler, GenericKafkaEvent } from './types';
|
|
2
|
+
type PoisonHandlerDeps = {
|
|
3
|
+
logger: {
|
|
4
|
+
error: (obj: Record<string, unknown>, msg: string) => void;
|
|
5
|
+
};
|
|
6
|
+
produceCustom: (topicName: string, key: string, value: GenericKafkaEvent) => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Creates a poison handler that logs the error and produces the raw
|
|
10
|
+
* payload to a DLQ topic named `${topicName}-dlq`.
|
|
11
|
+
*/
|
|
12
|
+
declare const createDefaultPoisonHandler: (topicName: string, deps: PoisonHandlerDeps) => CustomTopicPoisonHandler;
|
|
13
|
+
export { createDefaultPoisonHandler };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDefaultPoisonHandler = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a poison handler that logs the error and produces the raw
|
|
6
|
+
* payload to a DLQ topic named `${topicName}-dlq`.
|
|
7
|
+
*/
|
|
8
|
+
const createDefaultPoisonHandler = (topicName, deps) => {
|
|
9
|
+
const dlqTopicName = `${topicName}-dlq`;
|
|
10
|
+
return async (ctx) => {
|
|
11
|
+
const errorMessage = ctx.error instanceof Error ? ctx.error.message : String(ctx.error);
|
|
12
|
+
deps.logger.error({
|
|
13
|
+
topic: topicName,
|
|
14
|
+
dlqTopic: dlqTopicName,
|
|
15
|
+
partition: ctx.partition,
|
|
16
|
+
error: errorMessage,
|
|
17
|
+
key: ctx.message.key?.toString(),
|
|
18
|
+
}, 'Poison message detected, forwarding to DLQ');
|
|
19
|
+
try {
|
|
20
|
+
await deps.produceCustom(dlqTopicName, ctx.message.key?.toString() ?? 'unknown', {
|
|
21
|
+
id: 'dlq',
|
|
22
|
+
type: 'create',
|
|
23
|
+
entity: {
|
|
24
|
+
rawPayload: ctx.rawPayloadUtf8,
|
|
25
|
+
error: errorMessage,
|
|
26
|
+
originalTopic: ctx.primaryTopicFullName,
|
|
27
|
+
partition: ctx.partition,
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (dlqError) {
|
|
33
|
+
deps.logger.error({ error: dlqError, topic: dlqTopicName }, 'Failed to produce to DLQ topic');
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
exports.createDefaultPoisonHandler = createDefaultPoisonHandler;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { KafkaClient } from './kafkaClient';
|
|
2
2
|
export { KafkaTopic } from './events';
|
|
3
|
-
export type { KafkaEvent, GenericKafkaEvent, EventPayloadHandler, GenericEventPayloadHandler, KafkaConfiguration, CustomTopicConfig, } from './types';
|
|
3
|
+
export type { KafkaEvent, GenericKafkaEvent, EventPayloadHandler, GenericEventPayloadHandler, KafkaConfiguration, CustomTopicConfig, CustomTopicPoisonContext, CustomTopicPoisonHandler, } from './types';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Consumer } from 'kafkajs';
|
|
2
2
|
import { Logger } from 'pino';
|
|
3
3
|
import { ServiceIdentity } from '../serviceIdentity';
|
|
4
|
-
import type { EventPayloadHandler, GenericEventPayloadHandler, KafkaConfiguration, KafkaEvent, GenericKafkaEvent, CustomTopicConfig } from './types';
|
|
4
|
+
import type { EventPayloadHandler, GenericEventPayloadHandler, KafkaConfiguration, KafkaEvent, GenericKafkaEvent, CustomTopicConfig, CustomTopicPoisonHandler } from './types';
|
|
5
5
|
import { KafkaTopic } from './events';
|
|
6
6
|
declare class KafkaClient {
|
|
7
7
|
private readonly environment;
|
|
@@ -42,7 +42,8 @@ declare class KafkaClient {
|
|
|
42
42
|
/**
|
|
43
43
|
* Consume messages from custom topic
|
|
44
44
|
*/
|
|
45
|
-
consumeCustom: <T = unknown>(topicName: string, handler: GenericEventPayloadHandler<T>, fromBeginning?: boolean) => Promise<Consumer>;
|
|
45
|
+
consumeCustom: <T = unknown>(topicName: string, handler: GenericEventPayloadHandler<T>, fromBeginning?: boolean, onPoison?: boolean | CustomTopicPoisonHandler) => Promise<Consumer>;
|
|
46
|
+
private defaultPoisonHandler;
|
|
46
47
|
private encryptCustomValue;
|
|
47
48
|
private decryptCustomValue;
|
|
48
49
|
private parseCustomValue;
|
|
@@ -38,6 +38,7 @@ const kafkajs_1 = require("kafkajs");
|
|
|
38
38
|
const jose = __importStar(require("jose"));
|
|
39
39
|
const util_1 = require("util");
|
|
40
40
|
const serviceIdentity_1 = require("../serviceIdentity");
|
|
41
|
+
const defaultPoisonHandler_1 = require("./defaultPoisonHandler");
|
|
41
42
|
const events_1 = require("./events");
|
|
42
43
|
const utils_1 = require("../../utils/utils");
|
|
43
44
|
const metrics_1 = require("../metrics");
|
|
@@ -331,7 +332,10 @@ class KafkaClient {
|
|
|
331
332
|
/**
|
|
332
333
|
* Consume messages from custom topic
|
|
333
334
|
*/
|
|
334
|
-
this.consumeCustom = async (topicName, handler, fromBeginning = false) => {
|
|
335
|
+
this.consumeCustom = async (topicName, handler, fromBeginning = false, onPoison) => {
|
|
336
|
+
const poisonHandler = onPoison === true
|
|
337
|
+
? this.defaultPoisonHandler(topicName)
|
|
338
|
+
: onPoison || undefined;
|
|
335
339
|
const topic = await this.getCustomTopic(topicName);
|
|
336
340
|
const groupId = `${this.environment.valueOf()}_${topicName}_${this.serviceIdentity.identityName}`;
|
|
337
341
|
try {
|
|
@@ -345,7 +349,7 @@ class KafkaClient {
|
|
|
345
349
|
await consumer.subscribe({ topic, fromBeginning });
|
|
346
350
|
await consumer.run({
|
|
347
351
|
autoCommit: true,
|
|
348
|
-
eachMessage: async ({ message }) => {
|
|
352
|
+
eachMessage: async ({ message, partition }) => {
|
|
349
353
|
try {
|
|
350
354
|
messageConsumedCounter.labels({ topic: topicName, groupId }).inc();
|
|
351
355
|
// Decrypt if secret was provided
|
|
@@ -359,21 +363,35 @@ class KafkaClient {
|
|
|
359
363
|
value,
|
|
360
364
|
timestamp: message.timestamp,
|
|
361
365
|
}, 'Custom topic record received');
|
|
362
|
-
|
|
363
|
-
await handler({ ...message, value });
|
|
364
|
-
}
|
|
365
|
-
catch (error) {
|
|
366
|
-
throw (0, utils_1.logAndGetError)(this.logger, `Could not consume message for custom topic=${topicName}`, error);
|
|
367
|
-
}
|
|
366
|
+
await handler({ ...message, value });
|
|
368
367
|
messageAcknowledgedCounter
|
|
369
368
|
.labels({ topic: topicName, groupId })
|
|
370
369
|
.inc();
|
|
371
370
|
}
|
|
372
371
|
catch (error) {
|
|
373
|
-
|
|
374
|
-
.
|
|
375
|
-
|
|
376
|
-
|
|
372
|
+
if (poisonHandler) {
|
|
373
|
+
this.logger.warn(error, 'Poison message detected, forwarding to DLQ');
|
|
374
|
+
await poisonHandler({
|
|
375
|
+
error,
|
|
376
|
+
decryptFailed: false,
|
|
377
|
+
message,
|
|
378
|
+
partition,
|
|
379
|
+
primaryTopicFullName: topic,
|
|
380
|
+
rawPayloadUtf8: message.value
|
|
381
|
+
? Buffer.from(message.value).toString('utf8')
|
|
382
|
+
: '',
|
|
383
|
+
});
|
|
384
|
+
messageAcknowledgedCounter
|
|
385
|
+
.labels({ topic: topicName, groupId })
|
|
386
|
+
.inc();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
messageConsumedErrorCounter
|
|
391
|
+
.labels({ topic: topicName, groupId })
|
|
392
|
+
.inc();
|
|
393
|
+
throw (0, utils_1.logAndGetError)(this.logger, `Could not consume message for custom topic=${topicName}`, error);
|
|
394
|
+
}
|
|
377
395
|
}
|
|
378
396
|
},
|
|
379
397
|
});
|
|
@@ -383,6 +401,10 @@ class KafkaClient {
|
|
|
383
401
|
throw (0, utils_1.logAndGetError)(this.logger, `Could not create consumer for custom topic=${topicName}`, error);
|
|
384
402
|
}
|
|
385
403
|
};
|
|
404
|
+
this.defaultPoisonHandler = (topicName) => (0, defaultPoisonHandler_1.createDefaultPoisonHandler)(topicName, {
|
|
405
|
+
logger: this.logger,
|
|
406
|
+
produceCustom: (t, k, v) => this.produceCustom(t, k, v),
|
|
407
|
+
});
|
|
386
408
|
this.encryptCustomValue = async (secret, value) => {
|
|
387
409
|
const jwe = await new jose.CompactEncrypt(new util_1.TextEncoder().encode(value));
|
|
388
410
|
jwe.setProtectedHeader({ alg: 'A256GCMKW', enc: 'A256GCM' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KafkaTopic, MessageFormats } from './events';
|
|
2
2
|
import { Environment, Service } from '../serviceIdentity';
|
|
3
|
-
import { KafkaMessage } from 'kafkajs';
|
|
3
|
+
import type { KafkaMessage } from 'kafkajs';
|
|
4
4
|
type KafkaEvent<T extends KafkaTopic> = {
|
|
5
5
|
id: string;
|
|
6
6
|
type: 'create' | 'update' | 'soft-destroy' | 'destroy';
|
|
@@ -31,4 +31,13 @@ type CustomTopicConfig = {
|
|
|
31
31
|
issuer: Service;
|
|
32
32
|
secret?: string;
|
|
33
33
|
};
|
|
34
|
-
|
|
34
|
+
type CustomTopicPoisonContext = {
|
|
35
|
+
error: unknown;
|
|
36
|
+
decryptFailed: boolean;
|
|
37
|
+
message: KafkaMessage;
|
|
38
|
+
partition: number;
|
|
39
|
+
primaryTopicFullName: string;
|
|
40
|
+
rawPayloadUtf8: string;
|
|
41
|
+
};
|
|
42
|
+
type CustomTopicPoisonHandler = (ctx: CustomTopicPoisonContext) => Promise<void>;
|
|
43
|
+
export type { KafkaEvent, GenericKafkaEvent, KafkaConfiguration, EventPayloadHandler, GenericEventPayloadHandler, CustomTopicConfig, CustomTopicPoisonContext, CustomTopicPoisonHandler, };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucaapp/service-utils",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.11.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@types/response-time": "^2.3.9",
|
|
32
32
|
"@types/swagger-ui-express": "4.1.8",
|
|
33
33
|
"@types/validator": "13.7.1",
|
|
34
|
-
"axios": "
|
|
34
|
+
"axios": "1.16.0",
|
|
35
35
|
"axios-rate-limit": "^1.4.0",
|
|
36
36
|
"axios-retry": "^4.5.0",
|
|
37
37
|
"body-parser": "1.20.4",
|
|
@@ -97,7 +97,8 @@
|
|
|
97
97
|
"node-forge": "1.4.0",
|
|
98
98
|
"uuid": "^14.0.0",
|
|
99
99
|
"follow-redirects": "^1.16.0",
|
|
100
|
-
"axios": "
|
|
101
|
-
"path-to-regexp": "0.1.13"
|
|
100
|
+
"axios": "1.16.0",
|
|
101
|
+
"path-to-regexp": "0.1.13",
|
|
102
|
+
"ip-address": "10.1.1"
|
|
102
103
|
}
|
|
103
104
|
}
|