@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.
- package/dist/lib/elastic-transport.js +132 -33
- package/package.json +1 -1
- package/src/lib/elastic-transport.ts +148 -44
|
@@ -33,51 +33,141 @@ function getIndexName(index, time) {
|
|
|
33
33
|
}
|
|
34
34
|
return index.replace('%{DATE}', time.substring(0, 10));
|
|
35
35
|
}
|
|
36
|
-
function
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
132
|
-
|
|
221
|
+
const bulkSender = createBulkSender(opts, client, splitter);
|
|
222
|
+
splitter.on('data', (doc) => {
|
|
223
|
+
bulkSender.add(doc);
|
|
133
224
|
});
|
|
134
|
-
|
|
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
|
@@ -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
|
|
75
|
+
function createBulkSender(
|
|
77
76
|
opts: ElasticTransportOptions,
|
|
78
77
|
client: Client,
|
|
79
78
|
splitter: NodeJS.ReadWriteStream
|
|
80
|
-
):
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
197
|
-
|
|
290
|
+
splitter.on('data', (doc) => {
|
|
291
|
+
bulkSender.add(doc)
|
|
292
|
+
})
|
|
293
|
+
splitter.on('finish', () => {
|
|
294
|
+
void bulkSender.close()
|
|
198
295
|
})
|
|
199
296
|
|
|
200
|
-
|
|
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
|
}
|