azify-logger 1.0.26 → 1.0.29

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,192 @@
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
- }
1
+ const { startRequestContext } = require('./store')
2
+ const { createHttpLoggerTransport } = require('./streams/httpQueue')
3
+ const os = require('os')
4
+
5
+ function fastUUID() {
6
+ const timestamp = Date.now().toString(36)
7
+ const randomPart = Math.random().toString(36).substring(2, 15)
8
+ const randomPart2 = Math.random().toString(36).substring(2, 15)
9
+ return `${timestamp}-${randomPart}-${randomPart2}`.substring(0, 36)
10
+ }
21
11
 
22
- const pendingLogs = new Set()
12
+ function sanitizeTraceHex(value) {
13
+ if (!value || typeof value !== 'string') return null
14
+ const hex = value.replace(/[^0-9a-fA-F]/g, '').toLowerCase()
15
+ if (!hex) return null
16
+ return hex.padEnd(32, '0').slice(0, 32)
17
+ }
23
18
 
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
- })
19
+ const HEADER_WHITELIST = new Set([
20
+ 'content-type',
21
+ 'content-length',
22
+ 'accept',
23
+ 'accept-encoding',
24
+ 'user-agent',
25
+ 'host',
26
+ 'x-request-id',
27
+ 'x-trace-id',
28
+ 'x-span-id',
29
+ 'x-parent-span-id'
30
+ ])
31
+
32
+ function pickHeaders (source) {
33
+ if (!source || typeof source !== 'object') {
34
+ return {}
39
35
  }
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
- }
36
+ const result = {}
37
+ for (const key in source) {
38
+ const lower = key.toLowerCase()
39
+ if (!HEADER_WHITELIST.has(lower)) continue
40
+
41
+ if (!Object.prototype.hasOwnProperty.call(source, key)) continue
42
+
43
+ const value = source[key]
44
+ if (Array.isArray(value)) {
45
+ result[key] = value.map(String)
46
+ } else if (value != null) {
47
+ result[key] = String(value)
55
48
  }
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
49
  }
50
+ return result
51
+ }
68
52
 
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
53
+ function createRestifyLoggingMiddleware (options = {}) {
54
+ const config = {
55
+ serviceName: options.serviceName || process.env.APP_NAME,
56
+ loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
57
+ environment: options.environment || process.env.NODE_ENV
80
58
  }
81
59
 
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']
60
+ const transport = createHttpLoggerTransport(config.loggerUrl, {})
61
+ const hostname = os.hostname()
62
+ const serviceObj = config.serviceName ? { name: config.serviceName, version: '1.0.0' } : null
63
+
64
+ function sendLog (level, message, meta = {}) {
65
+ if (!transport || typeof transport.enqueue !== 'function') return
87
66
 
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
- }
67
+ if (serviceObj) meta.service = serviceObj
68
+ if (config.environment) meta.environment = config.environment
69
+ meta.timestamp = Date.now()
70
+ meta.hostname = hostname
93
71
 
94
- return sanitized
72
+ transport.enqueue({
73
+ level,
74
+ message,
75
+ meta
76
+ }, { 'content-type': 'application/json' })
95
77
  }
96
78
 
97
- return function azifyLoggingMiddleware(req, res, next) {
79
+ return function azifyLoggingMiddleware (req, res, next) {
98
80
  const startTime = Date.now()
99
- const requestId = req.requestId || require('uuid').v4()
100
-
81
+ const requestId = req.requestId || fastUUID()
82
+
101
83
  if (res._azifySetup) {
102
84
  return next()
103
85
  }
104
86
  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('?'))
114
- }
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}')
117
87
 
118
- req._azifyRequestData = {
88
+ const traceHex = sanitizeTraceHex(req.headers['x-trace-id'])
89
+ const reqCtx = startRequestContext({
119
90
  requestId,
120
- method: req.method,
121
- 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
91
+ traceHex,
92
+ parentSpanId: req.headers['x-parent-span-id'] || null
93
+ })
94
+
95
+ let normalizedPath = req.url
96
+ if (normalizedPath.includes('?')) {
97
+ normalizedPath = normalizedPath.slice(0, normalizedPath.indexOf('?'))
131
98
  }
99
+ 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}')
100
+ normalizedPath = normalizedPath.replace(/\/[0-9]+/g, '/{id}')
132
101
 
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)
102
+ const requestSnapshot = {
103
+ id: requestId,
104
+ method: req.method,
105
+ url: req.url,
106
+ baseUrl: normalizedPath,
107
+ headers: pickHeaders(req.headers || {}),
108
+ query: req.query || {},
109
+ ip: req.connection.remoteAddress || req.socket.remoteAddress
140
110
  }
141
111
 
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()
112
+ let responseStatus
113
+ const originalStatus = res.status
114
+ if (typeof originalStatus === 'function') {
115
+ res.status = function patchedStatus (code) {
116
+ if (typeof code === 'number') {
117
+ responseStatus = code
159
118
  }
160
-
161
- return originalSend.apply(this, arguments)
119
+ return originalStatus.call(this, code)
162
120
  }
163
121
  }
164
122
 
165
-
166
- const originalStatus = res.status
167
- res.status = function(code) {
168
- res._actualStatusCode = code
169
- return originalStatus.call(this, code)
170
- }
171
-
172
123
  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])
124
+ if (typeof originalWriteHead === 'function') {
125
+ res.writeHead = function patchedWriteHead (statusCode, statusMessage, headers) {
126
+ if (typeof statusCode === 'number') {
127
+ responseStatus = statusCode
189
128
  }
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
129
+ if (typeof statusMessage === 'object') {
130
+ headers = statusMessage
215
131
  }
132
+ return originalWriteHead.call(this, statusCode, statusMessage, headers)
216
133
  }
217
- return originalJsonMethod.apply(this, arguments)
218
134
  }
219
135
 
220
- const originalEnd = res.end
221
- res.end = function(chunk, encoding) {
222
- 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
240
- }
136
+ let logSent = false
241
137
 
242
- originalEnd.call(this, chunk, encoding)
243
- }
244
-
245
- function logResponse() {
246
- const duration = Date.now() - startTime
247
-
248
- let responseBody = sentBody
138
+ function emitLog (level, message, extraMeta = {}) {
139
+ if (logSent) return
140
+ logSent = true
249
141
 
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) : ''
142
+ process.nextTick(() => {
143
+ const duration = Date.now() - startTime
144
+ const statusCode = responseStatus || res.statusCode || 200
145
+ const responseHeaders = typeof res.getHeaders === 'function' ? res.getHeaders() : {}
146
+
147
+ const meta = {
148
+ traceId: reqCtx.traceId,
149
+ spanId: reqCtx.spanId,
150
+ parentSpanId: reqCtx.parentSpanId || null,
151
+ requestId,
152
+ request: requestSnapshot,
153
+ response: {
154
+ statusCode,
155
+ headers: pickHeaders(responseHeaders),
156
+ durationMs: duration
157
+ },
158
+ ...extraMeta
274
159
  }
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)
160
+
161
+ sendLog(level, message, meta)
162
+ })
300
163
  }
301
164
 
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)
165
+ function finalize (level = 'info', errorMeta) {
166
+ if (logSent) return
167
+ const message = `${req.method} ${req.url}`
168
+ const meta = errorMeta ? { error: errorMeta } : {}
169
+ emitLog(level, message, meta)
322
170
  }
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
- }
171
+
172
+ res.on('finish', () => {
173
+ finalize('info')
339
174
  })
340
-
341
- runWithRequestContext(reqCtx, () => {
342
- wrappedNext()
175
+
176
+ res.on('close', () => {
177
+ finalize('warn', { message: 'response closed before finish' })
343
178
  })
179
+
180
+ res.on('error', (err) => {
181
+ finalize('error', {
182
+ message: err && err.message ? err.message : String(err),
183
+ name: err && err.name ? err.name : 'Error',
184
+ stack: err && err.stack ? err.stack : undefined
185
+ })
186
+ })
187
+
188
+ next()
344
189
  }
345
190
  }
346
191
 
347
192
  module.exports = createRestifyLoggingMiddleware
348
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.26",
3
+ "version": "1.0.29",
4
4
  "description": "Azify Logger Client - Centralized logging for OpenSearch",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,14 +9,15 @@
9
9
  "test": "echo \"No tests specified\" && exit 0",
10
10
  "start": "node server.js",
11
11
  "dev": "node server.js",
12
- "dev:full": "docker-compose up -d && sleep 10 && npm start",
12
+ "dev:full": "docker compose up -d && sleep 10 && npm start",
13
13
  "pm2:start": "pm2 start ecosystem.config.js",
14
14
  "pm2:stop": "pm2 stop ecosystem.config.js",
15
15
  "pm2:restart": "pm2 restart ecosystem.config.js",
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",
@@ -27,45 +28,41 @@
27
28
  "author": "Azify",
28
29
  "license": "MIT",
29
30
  "dependencies": {
31
+ "@opentelemetry/api": "1.0.4",
32
+ "@opentelemetry/auto-instrumentations-node": "0.27.0",
33
+ "@opentelemetry/core": "1.0.1",
34
+ "@opentelemetry/exporter-trace-otlp-http": "0.27.0",
35
+ "@opentelemetry/instrumentation-express": "0.27.0",
36
+ "@opentelemetry/instrumentation-http": "0.27.0",
37
+ "@opentelemetry/instrumentation-restify": "0.27.0",
38
+ "@opentelemetry/resources": "1.0.1",
39
+ "@opentelemetry/sdk-node": "0.27.0",
40
+ "@opentelemetry/semantic-conventions": "1.0.1",
30
41
  "axios": "^1.6.0",
31
42
  "cors": "^2.8.5",
32
43
  "express": "^4.18.2",
33
44
  "express-session": "^1.17.3",
45
+ "ioredis": "^5.8.2",
46
+ "js-yaml": "^4.1.0",
34
47
  "passport": "^0.6.0",
35
48
  "passport-azure-ad": "^4.3.5",
36
- "js-yaml": "^4.1.0",
37
49
  "require-in-the-middle": "^7.4.0",
38
50
  "uuid": "^9.0.1"
39
51
  },
40
- "peerDependencies": {
41
- "@opentelemetry/api": ">=1.1.0 <1.6.0",
42
- "@opentelemetry/core": ">=1.1.0 <1.6.0",
43
- "@opentelemetry/resources": ">=1.1.0 <1.6.0",
44
- "@opentelemetry/semantic-conventions": ">=1.1.0 <1.6.0",
45
- "@opentelemetry/sdk-node": ">=0.39.1 <0.42.0",
46
- "@opentelemetry/exporter-trace-otlp-http": ">=0.39.1 <0.42.0",
47
- "@opentelemetry/auto-instrumentations-node": ">=0.39.1 <0.42.0"
48
- },
49
- "optionalDependencies": {
50
- "@opentelemetry/api": "1.1.0",
51
- "@opentelemetry/core": "1.1.0",
52
- "@opentelemetry/resources": "1.1.0",
53
- "@opentelemetry/semantic-conventions": "1.1.0",
54
- "@opentelemetry/sdk-node": "0.39.1",
55
- "@opentelemetry/exporter-trace-otlp-http": "0.39.1",
56
- "@opentelemetry/auto-instrumentations-node": "0.39.1"
57
- },
58
52
  "overrides": {
59
- "@opentelemetry/api": "1.1.0",
60
- "@opentelemetry/core": "1.1.0",
61
- "@opentelemetry/resources": "1.1.0",
62
- "@opentelemetry/semantic-conventions": "1.1.0",
63
- "@opentelemetry/sdk-node": "0.39.1",
64
- "@opentelemetry/exporter-trace-otlp-http": "0.39.1",
65
- "@opentelemetry/auto-instrumentations-node": "0.39.1"
53
+ "@opentelemetry/api": "1.0.4",
54
+ "@opentelemetry/core": "1.0.1",
55
+ "@opentelemetry/resources": "1.0.1",
56
+ "@opentelemetry/semantic-conventions": "1.0.1",
57
+ "@opentelemetry/sdk-node": "0.27.0",
58
+ "@opentelemetry/exporter-trace-otlp-http": "0.27.0",
59
+ "@opentelemetry/auto-instrumentations-node": "0.27.0"
66
60
  },
67
61
  "engines": {
68
- "node": ">=12.0.0"
62
+ "node": ">=12 <=22"
63
+ },
64
+ "bin": {
65
+ "azify-logger-worker": "scripts/redis-worker.js"
69
66
  },
70
67
  "files": [
71
68
  "index.js",
@@ -78,7 +75,11 @@
78
75
  "middleware-restify.js",
79
76
  "middleware-express.js",
80
77
  "middleware-express.d.ts",
78
+ "middleware-fastify.js",
81
79
  "server.js",
80
+ "sampling.js",
81
+ "queue/",
82
+ "scripts/redis-worker.js",
82
83
  "streams/",
83
84
  "package.json",
84
85
  "README.md"