1
+ import { randomUUID } from 'node:crypto'
1
2
import { setTimeout } from 'node:timers/promises'
2
- import { InternalError , stringValueSerializer } from '@lokalise/node-core'
3
+ import {
4
+ InternalError ,
5
+ type TransactionObservabilityManager ,
6
+ stringValueSerializer ,
7
+ } from '@lokalise/node-core'
8
+ import type { QueueConsumerDependencies } from '@message-queue-toolkit/core'
3
9
import {
4
10
type ConsumeOptions ,
5
11
Consumer ,
@@ -10,17 +16,20 @@ import {
10
16
stringDeserializer ,
11
17
} from '@platformatic/kafka'
12
18
import { AbstractKafkaService , type BaseKafkaOptions } from './AbstractKafkaService.ts'
13
- import { KafkaHandlerContainer } from './handler-container/KafkaHandlerContainer.js '
14
- import type { KafkaHandlerRouting } from './handler-container/KafkaHandlerRoutingBuilder.js '
15
- import type { KafkaHandler } from './handler-container/index.js '
19
+ import { KafkaHandlerContainer } from './handler-container/KafkaHandlerContainer.ts '
20
+ import type { KafkaHandlerRouting } from './handler-container/KafkaHandlerRoutingBuilder.ts '
21
+ import type { KafkaHandler , RequestContext } from './handler-container/index.ts '
16
22
import type { KafkaDependencies , TopicConfig } from './types.ts'
17
23
24
+ export type KafkaConsumerDependencies = KafkaDependencies &
25
+ Pick < QueueConsumerDependencies , 'transactionObservabilityManager' >
26
+
18
27
export type KafkaConsumerOptions < TopicsConfig extends TopicConfig [ ] > = BaseKafkaOptions &
19
28
Omit <
20
- ConsumerOptions < string , object , string , object > ,
29
+ ConsumerOptions < string , object , string , string > ,
21
30
'deserializers' | 'autocommit' | 'bootstrapBrokers'
22
31
> &
23
- Omit < ConsumeOptions < string , object , string , object > , 'topics' > & {
32
+ Omit < ConsumeOptions < string , object , string , string > , 'topics' > & {
24
33
handlers : KafkaHandlerRouting < TopicsConfig >
25
34
}
26
35
@@ -33,14 +42,19 @@ const MAX_IN_MEMORY_RETRIES = 3
33
42
export abstract class AbstractKafkaConsumer <
34
43
TopicsConfig extends TopicConfig [ ] ,
35
44
> extends AbstractKafkaService < TopicsConfig , KafkaConsumerOptions < TopicsConfig > > {
36
- private readonly consumer : Consumer < string , object , string , object >
37
- private consumerStream ?: MessagesStream < string , object , string , object >
45
+ private readonly consumer : Consumer < string , object , string , string >
46
+ private consumerStream ?: MessagesStream < string , object , string , string >
38
47
48
+ private readonly transactionObservabilityManager : TransactionObservabilityManager
39
49
private readonly handlerContainer : KafkaHandlerContainer < TopicsConfig >
40
50
41
- constructor ( dependencies : KafkaDependencies , options : KafkaConsumerOptions < TopicsConfig > ) {
51
+ constructor (
52
+ dependencies : KafkaConsumerDependencies ,
53
+ options : KafkaConsumerOptions < TopicsConfig > ,
54
+ ) {
42
55
super ( dependencies , options )
43
56
57
+ this . transactionObservabilityManager = dependencies . transactionObservabilityManager
44
58
this . handlerContainer = new KafkaHandlerContainer < TopicsConfig > (
45
59
options . handlers ,
46
60
options . messageTypeField ,
@@ -54,7 +68,7 @@ export abstract class AbstractKafkaConsumer<
54
68
key : stringDeserializer ,
55
69
value : jsonDeserializer ,
56
70
headerKey : stringDeserializer ,
57
- headerValue : jsonDeserializer ,
71
+ headerValue : stringDeserializer ,
58
72
} ,
59
73
} )
60
74
}
@@ -87,18 +101,15 @@ export abstract class AbstractKafkaConsumer<
87
101
await this . consumer . close ( )
88
102
}
89
103
90
- /*
91
- TODO: https://lokalise.atlassian.net/browse/EDEXP-493
92
- - Improve logging with logger child on constructor + add request context?
93
- - Message logging
94
- - Observability
95
- */
96
-
97
- private async consume ( message : Message < string , object , string , object > ) : Promise < void > {
104
+ private async consume ( message : Message < string , object , string , string > ) : Promise < void > {
98
105
const handler = this . handlerContainer . resolveHandler ( message . topic , message . value )
99
106
// if there is no handler for the message, we ignore it (simulating subscription)
100
107
if ( ! handler ) return message . commit ( )
101
108
109
+ /* v8 ignore next */
110
+ const transactionId = this . resolveMessageId ( message . value ) ?? randomUUID ( )
111
+ this . transactionObservabilityManager ?. start ( this . buildTransactionName ( message ) , transactionId )
112
+
102
113
const parseResult = handler . schema . safeParse ( message . value )
103
114
if ( ! parseResult . success ) {
104
115
this . handlerError ( parseResult . error , {
@@ -116,13 +127,19 @@ export abstract class AbstractKafkaConsumer<
116
127
117
128
const validatedMessage = parseResult . data
118
129
130
+ const requestContext = this . getRequestContext ( message )
131
+
119
132
let retries = 0
120
133
let consumed = false
121
134
do {
122
135
// exponential backoff -> 2^(retry-1)
123
136
if ( retries > 0 ) await setTimeout ( Math . pow ( 2 , retries - 1 ) )
124
137
125
- consumed = await this . tryToConsume ( { ...message , value : validatedMessage } , handler . handler )
138
+ consumed = await this . tryToConsume (
139
+ { ...message , value : validatedMessage } ,
140
+ handler . handler ,
141
+ requestContext ,
142
+ )
126
143
if ( consumed ) break
127
144
128
145
retries ++
@@ -142,15 +159,18 @@ export abstract class AbstractKafkaConsumer<
142
159
} )
143
160
}
144
161
162
+ this . transactionObservabilityManager ?. stop ( transactionId )
163
+
145
164
return message . commit ( )
146
165
}
147
166
148
167
private async tryToConsume < MessageValue extends object > (
149
- message : Message < string , MessageValue , string , object > ,
168
+ message : Message < string , MessageValue , string , string > ,
150
169
handler : KafkaHandler < MessageValue > ,
170
+ requestContext : RequestContext ,
151
171
) : Promise < boolean > {
152
172
try {
153
- await handler ( message )
173
+ await handler ( message , requestContext )
154
174
return true
155
175
} catch ( error ) {
156
176
this . handlerError ( error , {
@@ -161,4 +181,27 @@ export abstract class AbstractKafkaConsumer<
161
181
162
182
return false
163
183
}
184
+
185
+ private buildTransactionName ( message : Message < string , object , string , string > ) {
186
+ const messageType = this . resolveMessageType ( message . value )
187
+
188
+ let name = `kafka:${ message . topic } `
189
+ if ( messageType ?. trim ( ) . length ) name += `:${ messageType . trim ( ) } `
190
+
191
+ return name
192
+ }
193
+
194
+ private getRequestContext ( message : Message < string , object , string , string > ) : RequestContext {
195
+ let reqId = message . headers . get ( this . resolveHeaderRequestIdField ( ) )
196
+ if ( ! reqId || reqId . trim ( ) . length === 0 ) reqId = randomUUID ( )
197
+
198
+ return {
199
+ reqId,
200
+ logger : this . logger . child ( {
201
+ 'x-request-id' : reqId ,
202
+ topic : message . topic ,
203
+ messageKey : message . key ,
204
+ } ) ,
205
+ }
206
+ }
164
207
}
0 commit comments