azify-logger 1.0.25 → 1.0.28

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.
@@ -1,348 +1,177 @@
1
- /**
2
- * Creates a Restify middleware for automatic request/response logging with azify-logger
3
- * @param {Object} options - Configuration options
4
- * @param {string} [options.serviceName] - Name of the service (defaults to APP_NAME env var or 'azipay')
5
- * @param {string} [options.loggerUrl] - URL of the azify-logger service (defaults to AZIFY_LOGGER_URL env var or 'http://localhost:3001')
6
- * @param {string} [options.environment] - Environment name (defaults to NODE_ENV env var or 'development')
7
- * @returns {Function} Restify middleware function
8
- * @example
9
- * const azifyMiddleware = require('azify-logger/middleware-restify');
10
- * server.use(azifyMiddleware({ serviceName: 'my-app' }));
11
- */
12
- const axios = require('axios')
13
- const { als, runWithRequestContext, startRequestContext, getRequestContext } = require('./store')
14
-
15
- function createRestifyLoggingMiddleware(options = {}) {
16
- const config = {
17
- serviceName: options.serviceName || process.env.APP_NAME || 'azipay',
18
- loggerUrl: (options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log').replace(/\/log$/, '') + '/log',
19
- environment: options.environment || process.env.NODE_ENV || 'development'
20
- }
21
-
22
- const pendingLogs = new Set()
23
-
24
- let exitHandlerInstalled = false
25
- if (!exitHandlerInstalled) {
26
- exitHandlerInstalled = true
27
-
28
- process.on('uncaughtException', (err) => {
29
- console.error('[AZIFY] Uncaught exception:', err.message)
30
-
31
- if (pendingLogs.size > 0) {
32
- setTimeout(() => {
33
- process.exit(1)
34
- }, 500)
35
- } else {
36
- process.exit(1)
37
- }
38
- })
1
+ const { runWithRequestContext, startRequestContext, getRequestContext } = require('./store')
2
+ const { createHttpLoggerTransport } = require('./streams/httpQueue')
3
+ const { randomUUID } = require('crypto')
4
+ const os = require('os')
5
+
6
+ const HEADER_WHITELIST = new Set([
7
+ 'content-type',
8
+ 'content-length',
9
+ 'accept',
10
+ 'accept-encoding',
11
+ 'user-agent',
12
+ 'host',
13
+ 'x-request-id',
14
+ 'x-trace-id',
15
+ 'x-span-id',
16
+ 'x-parent-span-id'
17
+ ])
18
+
19
+ function pickHeaders (source) {
20
+ if (!source || typeof source !== 'object') {
21
+ return {}
39
22
  }
40
-
41
- function sendLog(level, message, meta = {}) {
42
- const logData = {
43
- level,
44
- message,
45
- meta: {
46
- ...meta,
47
- service: {
48
- name: config.serviceName,
49
- version: '1.0.0'
50
- },
51
- environment: config.environment,
52
- timestamp: new Date().toISOString(),
53
- hostname: require('os').hostname()
54
- }
23
+ const result = {}
24
+ for (const [key, value] of Object.entries(source)) {
25
+ const lower = key.toLowerCase()
26
+ if (HEADER_WHITELIST.has(lower)) {
27
+ result[key] = Array.isArray(value) ? value.map(String) : (value != null ? String(value) : value)
55
28
  }
56
-
57
- const logId = Math.random().toString(36).substring(7)
58
- pendingLogs.add(logId)
59
-
60
- axios.post(`${config.loggerUrl}`, logData, {
61
- timeout: 2000
62
- }).then(() => {
63
- pendingLogs.delete(logId)
64
- }).catch(() => {
65
- pendingLogs.delete(logId)
66
- })
67
29
  }
30
+ return result
31
+ }
68
32
 
69
- function sanitizeHeaders(headers) {
70
- const sanitized = { ...headers }
71
- const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token', 'x-access-token']
72
-
73
- for (const key of Object.keys(sanitized)) {
74
- if (sensitiveHeaders.includes(key.toLowerCase())) {
75
- sanitized[key] = '***'
76
- }
77
- }
78
-
79
- return sanitized
33
+ function createRestifyLoggingMiddleware (options = {}) {
34
+ const config = {
35
+ serviceName: options.serviceName || process.env.APP_NAME,
36
+ loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
37
+ environment: options.environment || process.env.NODE_ENV
80
38
  }
81
39
 
82
- function sanitizeBody(body) {
83
- if (!body || typeof body !== 'object') return body
84
-
85
- const sanitized = Array.isArray(body) ? [...body] : { ...body }
86
- const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'api_key', 'accessToken', 'access_token', 'refreshToken', 'refresh_token', 'clientSecret', 'client_secret']
87
-
88
- for (const key of Object.keys(sanitized)) {
89
- if (sensitiveFields.includes(key) || key.toLowerCase().includes('password') || key.toLowerCase().includes('secret')) {
90
- sanitized[key] = '***'
91
- }
92
- }
93
-
94
- return sanitized
40
+ const transport = createHttpLoggerTransport(config.loggerUrl)
41
+
42
+ function sendLog (level, message, meta = {}) {
43
+ try {
44
+ transport.enqueue({
45
+ level,
46
+ message,
47
+ meta: {
48
+ ...meta,
49
+ service: {
50
+ name: config.serviceName,
51
+ version: '1.0.0'
52
+ },
53
+ environment: config.environment,
54
+ timestamp: new Date().toISOString(),
55
+ hostname: os.hostname()
56
+ }
57
+ }, { 'content-type': 'application/json' })
58
+ } catch (err) { }
95
59
  }
96
60
 
97
- return function azifyLoggingMiddleware(req, res, next) {
61
+ return function azifyLoggingMiddleware (req, res, next) {
98
62
  const startTime = Date.now()
99
- const requestId = req.requestId || require('uuid').v4()
100
-
63
+ const requestId = req.requestId || randomUUID()
64
+
101
65
  if (res._azifySetup) {
102
66
  return next()
103
67
  }
104
68
  res._azifySetup = true
105
-
106
- const reqCtx = startRequestContext({ requestId })
107
- const requestTraceId = reqCtx.traceId
108
- const requestSpanId = reqCtx.spanId
109
- const requestParentSpanId = reqCtx.parentSpanId
110
-
111
- let baseUrl = req.url
112
- if (baseUrl.includes('?')) {
113
- baseUrl = baseUrl.substring(0, baseUrl.indexOf('?'))
69
+
70
+ const currentCtx = getRequestContext()
71
+ const traceId = currentCtx && currentCtx.traceId ? currentCtx.traceId : (req.headers['x-trace-id'] || randomUUID())
72
+ const parentSpanId = currentCtx && currentCtx.spanId ? currentCtx.spanId : (req.headers['x-parent-span-id'] || null)
73
+ const spanId = randomUUID().replace(/-/g, '').slice(0, 16)
74
+
75
+ const reqCtx = startRequestContext({ requestId, traceId, spanId, parentSpanId })
76
+
77
+ let normalizedPath = req.url
78
+ if (normalizedPath.includes('?')) {
79
+ normalizedPath = normalizedPath.slice(0, normalizedPath.indexOf('?'))
114
80
  }
115
- baseUrl = baseUrl.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/{id}')
116
- baseUrl = baseUrl.replace(/\/[0-9]+/g, '/{id}')
81
+ normalizedPath = normalizedPath.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/{id}')
82
+ normalizedPath = normalizedPath.replace(/\/[0-9]+/g, '/{id}')
117
83
 
118
- req._azifyRequestData = {
119
- requestId,
84
+ const requestSnapshot = {
85
+ id: requestId,
120
86
  method: req.method,
121
87
  url: req.url,
122
- baseUrl: baseUrl,
123
- path: req.url,
124
- headers: sanitizeHeaders(req.headers),
125
- query: req.query,
126
- userAgent: req.headers['user-agent'],
127
- ip: req.connection.remoteAddress || req.socket.remoteAddress,
128
- traceId: requestTraceId,
129
- spanId: requestSpanId,
130
- parentSpanId: requestParentSpanId
131
- }
132
-
133
- if (req.method === 'GET') {
134
- sendLog('info', `[REQUEST] ${req.method} ${req.url}`, req._azifyRequestData)
135
- } else {
136
- if (req.body !== undefined) {
137
- req._azifyRequestData.requestBody = sanitizeBody(req.body)
138
- }
139
- sendLog('info', `[REQUEST] ${req.method} ${req.url}`, req._azifyRequestData)
88
+ baseUrl: normalizedPath,
89
+ headers: pickHeaders(req.headers || {}),
90
+ query: req.query || {},
91
+ ip: req.connection.remoteAddress || req.socket.remoteAddress
140
92
  }
141
93
 
142
- res._azifyResponseLogged = false
143
- let sentBody
144
-
145
- const originalSend = res.send && res.send.bind(res)
146
- if (originalSend) {
147
- res.send = function patchedSend() {
148
- try {
149
- if (arguments.length === 1) {
150
- sentBody = arguments[0]
151
- } else if (arguments.length >= 2) {
152
- sentBody = typeof arguments[0] === 'number' ? arguments[1] : (arguments[1] || arguments[0])
153
- }
154
- } catch (_) {}
155
-
156
- if (!res._azifyResponseLogged) {
157
- res._azifyResponseLogged = true
158
- logResponse()
94
+ let responseStatus
95
+ const originalStatus = res.status
96
+ if (typeof originalStatus === 'function') {
97
+ res.status = function patchedStatus (code) {
98
+ if (typeof code === 'number') {
99
+ responseStatus = code
159
100
  }
160
-
161
- return originalSend.apply(this, arguments)
101
+ return originalStatus.call(this, code)
162
102
  }
163
103
  }
164
104
 
165
-
166
- const originalStatus = res.status
167
- res.status = function(code) {
168
- res._actualStatusCode = code
169
- return originalStatus.call(this, code)
170
- }
171
-
172
105
  const originalWriteHead = res.writeHead
173
- res.writeHead = function(statusCode, statusMessage, headers) {
174
- res._actualStatusCode = statusCode
175
- if (typeof statusMessage === 'object') {
176
- headers = statusMessage
177
- statusMessage = undefined
178
- }
179
- return originalWriteHead.call(this, statusCode, statusMessage, headers)
180
- }
181
-
182
- const originalJsonMethod = res.json
183
- res.json = function(code, body) {
184
- try {
185
- if (arguments.length === 1) {
186
- sentBody = arguments[0]
187
- } else if (arguments.length >= 2) {
188
- sentBody = typeof arguments[0] === 'number' ? arguments[1] : (arguments[1] || arguments[0])
106
+ if (typeof originalWriteHead === 'function') {
107
+ res.writeHead = function patchedWriteHead (statusCode, statusMessage, headers) {
108
+ if (typeof statusCode === 'number') {
109
+ responseStatus = statusCode
189
110
  }
190
- } catch (_) {}
191
-
192
- if (typeof code === 'number') {
193
- res._actualStatusCode = code
194
- } else {
195
- const errorObj = arguments.length === 1 ? arguments[0] : (typeof arguments[0] === 'number' ? arguments[1] : arguments[0])
196
-
197
- if (errorObj && errorObj.constructor && errorObj.constructor.name === 'ErrCtor') {
198
- const errorName = errorObj.toString()
199
- if (errorName.includes('InternalServerError') || errorName.includes('InternalError')) {
200
- res._actualStatusCode = 500
201
- res._actualStatusCode = 500
202
- } else if (errorName.includes('BadRequest') || errorName.includes('BadDigest')) {
203
- res._actualStatusCode = 400
204
- } else if (errorName.includes('NotFound')) {
205
- res._actualStatusCode = 404
206
- } else if (errorName.includes('Unauthorized')) {
207
- res._actualStatusCode = 401
208
- } else if (errorName.includes('Forbidden')) {
209
- res._actualStatusCode = 403
210
- } else {
211
- res._actualStatusCode = 500
212
- }
213
- } else {
214
- res._actualStatusCode = res.statusCode || 200
111
+ if (typeof statusMessage === 'object') {
112
+ headers = statusMessage
215
113
  }
114
+ return originalWriteHead.call(this, statusCode, statusMessage, headers)
216
115
  }
217
- return originalJsonMethod.apply(this, arguments)
218
116
  }
219
117
 
220
- const originalEnd = res.end
221
- res.end = function(chunk, encoding) {
118
+ function emitLog (level, message, extraMeta = {}) {
222
119
  const duration = Date.now() - startTime
223
-
224
- let responseBody = sentBody
225
- try {
226
- if (responseBody == null && chunk) {
227
- if (Buffer.isBuffer(chunk)) {
228
- responseBody = chunk.toString('utf8')
229
- } else if (typeof chunk === 'string') {
230
- responseBody = chunk
231
- } else {
232
- responseBody = JSON.stringify(chunk)
233
- }
234
- }
235
- } catch (_) {}
236
-
237
- if (!res._azifyResponseLogged) {
238
- logResponse()
239
- res._azifyResponseLogged = true
120
+ const statusCode = responseStatus || res.statusCode || 200
121
+ const responseHeaders = typeof res.getHeaders === 'function' ? res.getHeaders() : {}
122
+
123
+ const meta = {
124
+ traceId: reqCtx.traceId,
125
+ spanId: reqCtx.spanId,
126
+ parentSpanId: reqCtx.parentSpanId,
127
+ requestId,
128
+ request: requestSnapshot,
129
+ response: {
130
+ statusCode,
131
+ headers: pickHeaders(responseHeaders),
132
+ durationMs: duration
133
+ },
134
+ ...extraMeta
240
135
  }
241
136
 
242
- originalEnd.call(this, chunk, encoding)
243
- }
244
-
245
- function logResponse() {
246
- const duration = Date.now() - startTime
247
-
248
- let responseBody = sentBody
249
-
250
- let serializedResponseBody
251
- try {
252
- if (typeof responseBody === 'string') {
253
- serializedResponseBody = responseBody
254
- } else if (Array.isArray(responseBody)) {
255
- serializedResponseBody = JSON.stringify(responseBody)
256
- } else if (responseBody && typeof responseBody === 'object') {
257
- if (responseBody.toJSON && typeof responseBody.toJSON === 'function') {
258
- serializedResponseBody = JSON.stringify(responseBody.toJSON())
259
- } else if (responseBody.toString && typeof responseBody.toString === 'function' && responseBody.toString() !== '[object Object]') {
260
- serializedResponseBody = responseBody.toString()
261
- } else {
262
- serializedResponseBody = JSON.stringify(responseBody, (key, value) => {
263
- if (typeof value === 'function') {
264
- return '[Function]'
265
- }
266
- if (value instanceof Error) {
267
- return { name: value.name, message: value.message, stack: value.stack }
268
- }
269
- return value
270
- }, null, 0)
271
- }
272
- } else {
273
- serializedResponseBody = responseBody != null ? String(responseBody) : ''
274
- }
275
- } catch (error) {
276
- try {
277
- serializedResponseBody = JSON.stringify(responseBody, null, 2)
278
- } catch (secondError) {
279
- serializedResponseBody = '[Complex object - serialization failed]'
280
- }
281
- }
282
-
283
- const statusCode = res._actualStatusCode || res._statusCode || res.statusCode || 200
284
-
285
- const responseMessage = serializedResponseBody && serializedResponseBody.length > 0
286
- ? `[RESPONSE] ${serializedResponseBody}`
287
- : `[RESPONSE] ${req.method} ${req.url} ${statusCode} ${duration}ms`
288
-
289
- const responseData = {
290
- ...req._azifyRequestData,
291
- requestBody: req.body,
292
- statusCode: statusCode,
293
- responseTime: duration,
294
- responseHeaders: res.getHeaders ? res.getHeaders() : {},
295
- responseBody: serializedResponseBody
296
- }
297
-
298
- try { res._azifyResponseLogged = true } catch (_) {}
299
- sendLog('info', responseMessage, responseData)
137
+ sendLog(level, message, meta)
300
138
  }
301
139
 
302
- req._azifyContext = reqCtx
303
-
304
- const originalNext = next
305
- const wrappedNext = (err) => {
306
- if (err && !res._azifyResponseLogged) {
307
- const errorData = {
308
- ...req._azifyRequestData,
309
- statusCode: res.statusCode || 500,
310
- responseTime: Date.now() - startTime,
311
- error: {
312
- message: err.message || String(err),
313
- stack: err.stack,
314
- name: err.name,
315
- code: err.code
316
- }
317
- }
318
- sendLog('error', `[ERROR] ${req.method} ${req.url}: ${err.message || String(err)}`, errorData)
319
- res._azifyResponseLogged = true
320
- }
321
- return originalNext(err)
140
+ function finalize (level = 'info', errorMeta) {
141
+ const message = `${req.method} ${req.url}`
142
+ const meta = errorMeta ? { error: errorMeta } : {}
143
+ emitLog(level, message, meta)
322
144
  }
323
-
324
- res.on('error', (error) => {
325
- if (!res._azifyResponseLogged) {
326
- const errorData = {
327
- ...req._azifyRequestData,
328
- statusCode: res.statusCode || 500,
329
- responseTime: Date.now() - startTime,
330
- error: {
331
- message: error.message,
332
- stack: error.stack,
333
- name: error.name
334
- }
335
- }
336
- sendLog('error', `[ERROR] ${req.method} ${req.url}: ${error.message}`, errorData)
337
- res._azifyResponseLogged = true
338
- }
145
+
146
+ res.on('finish', () => {
147
+ finalize('info')
148
+ })
149
+
150
+ res.on('close', () => {
151
+ finalize('warn', { message: 'response closed before finish' })
152
+ })
153
+
154
+ res.on('error', (err) => {
155
+ finalize('error', {
156
+ message: err && err.message ? err.message : String(err),
157
+ name: err && err.name ? err.name : 'Error',
158
+ stack: err && err.stack ? err.stack : undefined
159
+ })
339
160
  })
340
-
161
+
341
162
  runWithRequestContext(reqCtx, () => {
342
- wrappedNext()
163
+ try {
164
+ next()
165
+ } catch (err) {
166
+ finalize('error', {
167
+ message: err && err.message ? err.message : String(err),
168
+ name: err && err.name ? err.name : 'Error',
169
+ stack: err && err.stack ? err.stack : undefined
170
+ })
171
+ throw err
172
+ }
343
173
  })
344
174
  }
345
175
  }
346
176
 
347
177
  module.exports = createRestifyLoggingMiddleware
348
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.25",
3
+ "version": "1.0.28",
4
4
  "description": "Azify Logger Client - Centralized logging for OpenSearch",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -16,7 +16,8 @@
16
16
  "pm2:logs": "pm2 logs azify-logger",
17
17
  "docker:up": "docker-compose up -d",
18
18
  "docker:down": "docker-compose down",
19
- "docker:logs": "docker-compose logs -f"
19
+ "docker:logs": "docker-compose logs -f",
20
+ "worker": "node scripts/redis-worker.js"
20
21
  },
21
22
  "keywords": [
22
23
  "logging",
@@ -28,6 +29,7 @@
28
29
  "license": "MIT",
29
30
  "dependencies": {
30
31
  "axios": "^1.6.0",
32
+ "ioredis": "^5.4.1",
31
33
  "cors": "^2.8.5",
32
34
  "express": "^4.18.2",
33
35
  "express-session": "^1.17.3",
@@ -36,26 +38,33 @@
36
38
  "js-yaml": "^4.1.0",
37
39
  "require-in-the-middle": "^7.4.0",
38
40
  "uuid": "^9.0.1",
39
- "@opentelemetry/api": "1.1.0",
40
- "@opentelemetry/core": "1.1.0",
41
- "@opentelemetry/resources": "1.1.0",
42
- "@opentelemetry/semantic-conventions": "1.1.0",
43
- "@opentelemetry/sdk-node": "0.39.1",
44
- "@opentelemetry/exporter-trace-otlp-http": "0.39.1",
45
- "@opentelemetry/auto-instrumentations-node": "0.39.1"
41
+ "@opentelemetry/instrumentation-http": "0.27.0",
42
+ "@opentelemetry/instrumentation-express": "0.27.0",
43
+ "@opentelemetry/instrumentation-restify": "0.27.0",
44
+
45
+ "@opentelemetry/api": "1.0.4",
46
+ "@opentelemetry/core": "1.0.1",
47
+ "@opentelemetry/resources": "1.0.1",
48
+ "@opentelemetry/semantic-conventions": "1.0.1",
49
+ "@opentelemetry/sdk-node": "0.27.0",
50
+ "@opentelemetry/exporter-trace-otlp-http": "0.27.0",
51
+ "@opentelemetry/auto-instrumentations-node": "0.27.0"
46
52
  },
47
53
  "overrides": {
48
- "@opentelemetry/api": "1.1.0",
49
- "@opentelemetry/core": "1.1.0",
50
- "@opentelemetry/resources": "1.1.0",
51
- "@opentelemetry/semantic-conventions": "1.1.0",
52
- "@opentelemetry/sdk-node": "0.39.1",
53
- "@opentelemetry/exporter-trace-otlp-http": "0.39.1",
54
- "@opentelemetry/auto-instrumentations-node": "0.39.1"
54
+ "@opentelemetry/api": "1.0.4",
55
+ "@opentelemetry/core": "1.0.1",
56
+ "@opentelemetry/resources": "1.0.1",
57
+ "@opentelemetry/semantic-conventions": "1.0.1",
58
+ "@opentelemetry/sdk-node": "0.27.0",
59
+ "@opentelemetry/exporter-trace-otlp-http": "0.27.0",
60
+ "@opentelemetry/auto-instrumentations-node": "0.27.0"
55
61
  },
56
62
  "optionalDependencies": {},
57
63
  "engines": {
58
- "node": ">=12.0.0"
64
+ "node": ">=12 <=22"
65
+ },
66
+ "bin": {
67
+ "azify-logger-worker": "scripts/redis-worker.js"
59
68
  },
60
69
  "files": [
61
70
  "index.js",
@@ -69,6 +78,9 @@
69
78
  "middleware-express.js",
70
79
  "middleware-express.d.ts",
71
80
  "server.js",
81
+ "sampling.js",
82
+ "queue/",
83
+ "scripts/redis-worker.js",
72
84
  "streams/",
73
85
  "package.json",
74
86
  "README.md"
@@ -0,0 +1,100 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ function createFileSpool(options = {}) {
5
+ const directory = options.directory || path.join(process.cwd(), '.azify-logger-spool')
6
+ const flushInterval = options.flushInterval || 5000
7
+ const batchSize = options.batchSize || 100
8
+ const pushFn = typeof options.pushFn === 'function' ? options.pushFn : null
9
+
10
+ if (!pushFn) {
11
+ return null
12
+ }
13
+
14
+ fs.mkdirSync(directory, { recursive: true })
15
+
16
+ let scheduled = false
17
+ let flushing = false
18
+
19
+ const scheduleFlush = () => {
20
+ if (scheduled) {
21
+ return
22
+ }
23
+ scheduled = true
24
+ const timer = setTimeout(async () => {
25
+ scheduled = false
26
+ await flush().catch(() => {})
27
+ }, flushInterval)
28
+ if (typeof timer.unref === 'function') {
29
+ timer.unref()
30
+ }
31
+ }
32
+
33
+ async function append(entry) {
34
+ const filePath = path.join(directory, `spool-${process.pid}.ndjson`)
35
+ await fs.promises.appendFile(filePath, JSON.stringify(entry) + '\n')
36
+ scheduleFlush()
37
+ }
38
+
39
+ async function flush() {
40
+ if (flushing) {
41
+ return
42
+ }
43
+ flushing = true
44
+ try {
45
+ const files = (await fs.promises.readdir(directory)).filter((file) => file.endsWith('.ndjson'))
46
+ for (const file of files) {
47
+ const fullPath = path.join(directory, file)
48
+ const drainingPath = `${fullPath}.draining`
49
+ try {
50
+ await fs.promises.rename(fullPath, drainingPath)
51
+ } catch (err) {
52
+ if (err.code === 'ENOENT') {
53
+ continue
54
+ }
55
+ throw err
56
+ }
57
+
58
+ const content = await fs.promises.readFile(drainingPath, 'utf8').catch(() => '')
59
+ if (!content) {
60
+ await fs.promises.unlink(drainingPath).catch(() => {})
61
+ continue
62
+ }
63
+
64
+ const lines = content.split('\n').filter(Boolean)
65
+ while (lines.length) {
66
+ const slice = lines.splice(0, batchSize)
67
+ const entries = slice.map((line) => {
68
+ try {
69
+ return JSON.parse(line)
70
+ } catch (_) {
71
+ return null
72
+ }
73
+ }).filter(Boolean)
74
+ if (entries.length > 0) {
75
+ await pushFn(entries).catch(async () => {
76
+ const remaining = entries.map((entry) => JSON.stringify(entry)).join('\n') + '\n'
77
+ await fs.promises.appendFile(fullPath, remaining)
78
+ throw new Error('Failed to flush file spool entries back to Redis')
79
+ })
80
+ }
81
+ }
82
+
83
+ await fs.promises.unlink(drainingPath).catch(() => {})
84
+ }
85
+ } finally {
86
+ flushing = false
87
+ }
88
+ }
89
+
90
+ return {
91
+ append,
92
+ flush
93
+ }
94
+ }
95
+
96
+ module.exports = {
97
+ createFileSpool
98
+ }
99
+
100
+