@mhmdhammoud/meritt-utils 1.5.9 → 1.6.1

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.
@@ -33,51 +33,141 @@ function getIndexName(index, time) {
33
33
  }
34
34
  return index.replace('%{DATE}', time.substring(0, 10));
35
35
  }
36
- function initializeBulkHandler(opts, client, splitter) {
36
+ function createBulkSender(opts, client, splitter) {
37
37
  var _a, _b, _c, _d, _e, _f, _g;
38
38
  const esVersion = Number((_b = (_a = opts.esVersion) !== null && _a !== void 0 ? _a : opts['es-version']) !== null && _b !== void 0 ? _b : 7);
39
39
  const index = (_c = opts.index) !== null && _c !== void 0 ? _c : 'pino';
40
40
  const buildIndexName = typeof index === 'function' ? index : null;
41
41
  const opType = esVersion >= 7 ? undefined : undefined;
42
- // CRITICAL FIX (issue #140): When bulk helper destroys stream after retries exhausted,
43
- // we must BOTH resurrect the pool AND reinitialize the bulk handler so logging continues.
44
- // connectionPool.resurrect exists at runtime (elastic-transport) but may not be in types
45
- const pool = client.connectionPool;
46
- const splitterWithDestroy = splitter;
47
- splitterWithDestroy.destroy = function () {
48
- if (typeof pool.resurrect === 'function') {
49
- pool.resurrect({ name: 'elasticsearch-js' });
42
+ const flushBytes = (_e = (_d = opts.flushBytes) !== null && _d !== void 0 ? _d : opts['flush-bytes']) !== null && _e !== void 0 ? _e : 1000;
43
+ const flushInterval = (_g = (_f = opts.flushInterval) !== null && _f !== void 0 ? _f : opts['flush-interval']) !== null && _g !== void 0 ? _g : 3000;
44
+ let buffer = [];
45
+ let bufferedBytes = 0;
46
+ let timer;
47
+ let isFlushing = false;
48
+ let flushAgain = false;
49
+ const indexName = (time = new Date().toISOString()) => buildIndexName ? buildIndexName(time) : getIndexName(index, time);
50
+ const clearFlushTimer = () => {
51
+ if (timer) {
52
+ clearTimeout(timer);
53
+ timer = undefined;
50
54
  }
51
- // Reinitialize bulk handler - without this, logging stops permanently until restart
52
- initializeBulkHandler(opts, client, splitter);
53
55
  };
54
- const indexName = (time = new Date().toISOString()) => buildIndexName ? buildIndexName(time) : getIndexName(index, time);
55
- const bulkInsert = client.helpers.bulk({
56
- datasource: splitter,
57
- flushBytes: (_e = (_d = opts.flushBytes) !== null && _d !== void 0 ? _d : opts['flush-bytes']) !== null && _e !== void 0 ? _e : 1000,
58
- flushInterval: (_g = (_f = opts.flushInterval) !== null && _f !== void 0 ? _f : opts['flush-interval']) !== null && _g !== void 0 ? _g : 3000,
59
- refreshOnCompletion: false,
60
- onDocument(doc) {
61
- var _a, _b;
56
+ const scheduleFlush = () => {
57
+ var _a;
58
+ if (timer || buffer.length === 0) {
59
+ return;
60
+ }
61
+ timer = setTimeout(() => {
62
+ timer = undefined;
63
+ void flush();
64
+ }, flushInterval);
65
+ (_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
66
+ };
67
+ const buildOperation = (doc) => {
68
+ var _a, _b;
69
+ try {
62
70
  const d = doc;
63
71
  const date = (_b = (_a = d.time) !== null && _a !== void 0 ? _a : d['@timestamp']) !== null && _b !== void 0 ? _b : new Date().toISOString();
64
72
  if (opType === 'create') {
65
73
  d['@timestamp'] = date;
66
74
  }
67
- return {
68
- index: {
69
- _index: indexName(date),
70
- op_type: opType,
75
+ return [
76
+ {
77
+ index: {
78
+ _index: indexName(date),
79
+ op_type: opType,
80
+ },
71
81
  },
72
- };
82
+ doc,
83
+ ];
84
+ }
85
+ catch (_c) {
86
+ return [
87
+ {
88
+ index: {
89
+ _index: indexName(),
90
+ op_type: opType,
91
+ },
92
+ },
93
+ doc,
94
+ ];
95
+ }
96
+ };
97
+ const emitDroppedDocument = (doc, cause) => {
98
+ const error = new Error('Dropped document');
99
+ error.document = doc;
100
+ error.cause = cause;
101
+ splitter.emit('insertError', error);
102
+ };
103
+ const flush = async () => {
104
+ var _a, _b;
105
+ if (isFlushing) {
106
+ flushAgain = true;
107
+ return;
108
+ }
109
+ clearFlushTimer();
110
+ if (buffer.length === 0) {
111
+ return;
112
+ }
113
+ isFlushing = true;
114
+ const batch = buffer;
115
+ buffer = [];
116
+ bufferedBytes = 0;
117
+ try {
118
+ const operations = batch.flatMap(buildOperation);
119
+ const response = await client.bulk({
120
+ operations,
121
+ refresh: false,
122
+ timeout: opts.requestTimeout ? `${opts.requestTimeout}ms` : undefined,
123
+ });
124
+ const body = response;
125
+ if (body.errors && Array.isArray(body.items)) {
126
+ body.items.forEach((item, index) => {
127
+ const result = Object.values(item)[0];
128
+ if (result === null || result === void 0 ? void 0 : result.error) {
129
+ emitDroppedDocument(batch[index], result.error);
130
+ }
131
+ });
132
+ }
133
+ splitter.emit('insert', {
134
+ successful: batch.length,
135
+ failed: body.errors ? ((_b = (_a = body.items) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) : 0,
136
+ });
137
+ }
138
+ catch (err) {
139
+ splitter.emit('error', err);
140
+ // Drop the failed batch instead of wedging the stream. The next log line
141
+ // creates a fresh bulk request and can recover without a process restart.
142
+ batch.forEach((doc) => emitDroppedDocument(doc, err));
143
+ }
144
+ finally {
145
+ isFlushing = false;
146
+ if (flushAgain || buffer.length > 0) {
147
+ flushAgain = false;
148
+ scheduleFlush();
149
+ if (bufferedBytes >= flushBytes) {
150
+ void flush();
151
+ }
152
+ }
153
+ }
154
+ };
155
+ return {
156
+ add(doc) {
157
+ buffer.push(doc);
158
+ bufferedBytes += Buffer.byteLength(JSON.stringify(doc));
159
+ if (bufferedBytes >= flushBytes) {
160
+ void flush();
161
+ return;
162
+ }
163
+ scheduleFlush();
73
164
  },
74
- onDrop(doc) {
75
- const error = new Error('Dropped document');
76
- error.document = doc;
77
- splitter.emit('insertError', error);
165
+ flush,
166
+ async close() {
167
+ clearFlushTimer();
168
+ await flush();
78
169
  },
79
- });
80
- bulkInsert.then((stats) => splitter.emit('insert', stats), (err) => splitter.emit('error', err));
170
+ };
81
171
  }
82
172
  const createElasticTransport = (opts = {}) => {
83
173
  const splitter = split(function (line) {
@@ -128,10 +218,19 @@ const createElasticTransport = (opts = {}) => {
128
218
  clientOpts.ConnectionPool = opts.ConnectionPool;
129
219
  }
130
220
  const client = new elasticsearch_1.Client(clientOpts);
131
- client.diagnostic.on('resurrect', () => {
132
- initializeBulkHandler(opts, client, splitter);
221
+ const bulkSender = createBulkSender(opts, client, splitter);
222
+ splitter.on('data', (doc) => {
223
+ bulkSender.add(doc);
133
224
  });
134
- initializeBulkHandler(opts, client, splitter);
225
+ splitter.on('finish', () => {
226
+ void bulkSender.close();
227
+ });
228
+ const splitterWithDestroy = splitter;
229
+ const originalDestroy = splitterWithDestroy.destroy.bind(splitterWithDestroy);
230
+ splitterWithDestroy.destroy = function (err) {
231
+ void bulkSender.close();
232
+ originalDestroy(err);
233
+ };
135
234
  return splitter;
136
235
  };
137
236
  exports.createElasticTransport = createElasticTransport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhmdhammoud/meritt-utils",
3
- "version": "1.5.9",
3
+ "version": "1.6.1",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
@@ -17,7 +17,6 @@ const split = require('split2') as (
17
17
  fn: (line: string) => unknown,
18
18
  opts?: { autoDestroy?: boolean }
19
19
  ) => NodeJS.ReadWriteStream
20
- import { Readable } from 'stream'
21
20
  import { Client } from '@elastic/elasticsearch'
22
21
  import type { ClientOptions } from '@elastic/elasticsearch'
23
22
 
@@ -73,67 +72,161 @@ function getIndexName(
73
72
  return index.replace('%{DATE}', time.substring(0, 10))
74
73
  }
75
74
 
76
- function initializeBulkHandler(
75
+ function createBulkSender(
77
76
  opts: ElasticTransportOptions,
78
77
  client: Client,
79
78
  splitter: NodeJS.ReadWriteStream
80
- ): void {
79
+ ): {
80
+ add: (doc: unknown) => void
81
+ flush: () => Promise<void>
82
+ close: () => Promise<void>
83
+ } {
81
84
  const esVersion = Number(opts.esVersion ?? opts['es-version'] ?? 7)
82
85
  const index = opts.index ?? 'pino'
83
86
  const buildIndexName = typeof index === 'function' ? index : null
84
87
  const opType = esVersion >= 7 ? undefined : undefined
88
+ const flushBytes = opts.flushBytes ?? opts['flush-bytes'] ?? 1000
89
+ const flushInterval = opts.flushInterval ?? opts['flush-interval'] ?? 3000
85
90
 
86
- // CRITICAL FIX (issue #140): When bulk helper destroys stream after retries exhausted,
87
- // we must BOTH resurrect the pool AND reinitialize the bulk handler so logging continues.
88
- // connectionPool.resurrect exists at runtime (elastic-transport) but may not be in types
89
- const pool = client.connectionPool as {
90
- resurrect?: (opts: { name: string }) => void
91
- }
92
- const splitterWithDestroy = splitter as NodeJS.ReadWriteStream & {
93
- destroy: (err?: Error) => void
94
- }
95
- splitterWithDestroy.destroy = function () {
96
- if (typeof pool.resurrect === 'function') {
97
- pool.resurrect({ name: 'elasticsearch-js' })
98
- }
99
- // Reinitialize bulk handler - without this, logging stops permanently until restart
100
- initializeBulkHandler(opts, client, splitter)
101
- }
91
+ let buffer: unknown[] = []
92
+ let bufferedBytes = 0
93
+ let timer: NodeJS.Timeout | undefined
94
+ let isFlushing = false
95
+ let flushAgain = false
102
96
 
103
97
  const indexName = (time = new Date().toISOString()) =>
104
98
  buildIndexName ? buildIndexName(time) : getIndexName(index as string, time)
105
99
 
106
- const bulkInsert = client.helpers.bulk({
107
- datasource: splitter as unknown as Readable,
108
- flushBytes: opts.flushBytes ?? opts['flush-bytes'] ?? 1000,
109
- flushInterval: opts.flushInterval ?? opts['flush-interval'] ?? 3000,
110
- refreshOnCompletion: false,
111
- onDocument(doc: unknown) {
100
+ const clearFlushTimer = () => {
101
+ if (timer) {
102
+ clearTimeout(timer)
103
+ timer = undefined
104
+ }
105
+ }
106
+
107
+ const scheduleFlush = () => {
108
+ if (timer || buffer.length === 0) {
109
+ return
110
+ }
111
+ timer = setTimeout(() => {
112
+ timer = undefined
113
+ void flush()
114
+ }, flushInterval)
115
+ timer.unref?.()
116
+ }
117
+
118
+ const buildOperation = (doc: unknown): [Record<string, unknown>, unknown] => {
119
+ try {
112
120
  const d = doc as LogDocument
113
121
  const date = d.time ?? d['@timestamp'] ?? new Date().toISOString()
114
122
  if (opType === 'create') {
115
123
  d['@timestamp'] = date
116
124
  }
117
- return {
118
- index: {
119
- _index: indexName(date),
120
- op_type: opType,
125
+ return [
126
+ {
127
+ index: {
128
+ _index: indexName(date),
129
+ op_type: opType,
130
+ },
131
+ },
132
+ doc,
133
+ ]
134
+ } catch {
135
+ return [
136
+ {
137
+ index: {
138
+ _index: indexName(),
139
+ op_type: opType,
140
+ },
121
141
  },
142
+ doc,
143
+ ]
144
+ }
145
+ }
146
+
147
+ const emitDroppedDocument = (doc: unknown, cause?: unknown) => {
148
+ const error = new Error('Dropped document') as Error & {
149
+ document: unknown
150
+ cause?: unknown
151
+ }
152
+ error.document = doc
153
+ error.cause = cause
154
+ splitter.emit('insertError', error)
155
+ }
156
+
157
+ const flush = async (): Promise<void> => {
158
+ if (isFlushing) {
159
+ flushAgain = true
160
+ return
161
+ }
162
+ clearFlushTimer()
163
+ if (buffer.length === 0) {
164
+ return
165
+ }
166
+
167
+ isFlushing = true
168
+ const batch = buffer
169
+ buffer = []
170
+ bufferedBytes = 0
171
+
172
+ try {
173
+ const operations = batch.flatMap(buildOperation)
174
+ const response = await client.bulk({
175
+ operations,
176
+ refresh: false,
177
+ timeout: opts.requestTimeout ? `${opts.requestTimeout}ms` : undefined,
178
+ })
179
+
180
+ const body = response as {
181
+ errors?: boolean
182
+ items?: Array<Record<string, { error?: unknown }>>
122
183
  }
123
- },
124
- onDrop(doc: unknown) {
125
- const error = new Error('Dropped document') as Error & {
126
- document: unknown
184
+ if (body.errors && Array.isArray(body.items)) {
185
+ body.items.forEach((item, index) => {
186
+ const result = Object.values(item)[0]
187
+ if (result?.error) {
188
+ emitDroppedDocument(batch[index], result.error)
189
+ }
190
+ })
127
191
  }
128
- error.document = doc
129
- splitter.emit('insertError', error)
130
- },
131
- })
132
192
 
133
- bulkInsert.then(
134
- (stats) => splitter.emit('insert', stats),
135
- (err) => splitter.emit('error', err)
136
- )
193
+ splitter.emit('insert', {
194
+ successful: batch.length,
195
+ failed: body.errors ? (body.items?.length ?? 0) : 0,
196
+ })
197
+ } catch (err) {
198
+ splitter.emit('error', err)
199
+ // Drop the failed batch instead of wedging the stream. The next log line
200
+ // creates a fresh bulk request and can recover without a process restart.
201
+ batch.forEach((doc) => emitDroppedDocument(doc, err))
202
+ } finally {
203
+ isFlushing = false
204
+ if (flushAgain || buffer.length > 0) {
205
+ flushAgain = false
206
+ scheduleFlush()
207
+ if (bufferedBytes >= flushBytes) {
208
+ void flush()
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ return {
215
+ add(doc: unknown) {
216
+ buffer.push(doc)
217
+ bufferedBytes += Buffer.byteLength(JSON.stringify(doc))
218
+ if (bufferedBytes >= flushBytes) {
219
+ void flush()
220
+ return
221
+ }
222
+ scheduleFlush()
223
+ },
224
+ flush,
225
+ async close() {
226
+ clearFlushTimer()
227
+ await flush()
228
+ },
229
+ }
137
230
  }
138
231
 
139
232
  export const createElasticTransport = (
@@ -192,12 +285,23 @@ export const createElasticTransport = (
192
285
  }
193
286
 
194
287
  const client = new Client(clientOpts)
288
+ const bulkSender = createBulkSender(opts, client, splitter)
195
289
 
196
- client.diagnostic.on('resurrect', () => {
197
- initializeBulkHandler(opts, client, splitter)
290
+ splitter.on('data', (doc) => {
291
+ bulkSender.add(doc)
292
+ })
293
+ splitter.on('finish', () => {
294
+ void bulkSender.close()
198
295
  })
199
296
 
200
- initializeBulkHandler(opts, client, splitter)
297
+ const splitterWithDestroy = splitter as NodeJS.ReadWriteStream & {
298
+ destroy: (err?: Error) => void
299
+ }
300
+ const originalDestroy = splitterWithDestroy.destroy.bind(splitterWithDestroy)
301
+ splitterWithDestroy.destroy = function (err?: Error) {
302
+ void bulkSender.close()
303
+ originalDestroy(err)
304
+ }
201
305
 
202
306
  return splitter
203
307
  }