azify-logger 1.0.26 → 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,413 +1,264 @@
1
- const axios = require('axios')
2
- const { als, runWithRequestContext, startRequestContext, getRequestContext } = require('./store')
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 {}
22
+ }
23
+ const result = {}
24
+ for (const key in source) {
25
+ const lower = key.toLowerCase()
26
+ if (!HEADER_WHITELIST.has(lower)) continue
27
+
28
+ if (!Object.prototype.hasOwnProperty.call(source, key)) continue
29
+
30
+ const value = source[key]
31
+ if (Array.isArray(value)) {
32
+ result[key] = value.map(String)
33
+ } else if (value != null) {
34
+ result[key] = String(value)
35
+ }
36
+ }
37
+ return result
38
+ }
3
39
 
4
- /**
5
- * Creates an Express middleware for automatic request/response logging with azify-logger
6
- * @param {Object} options - Configuration options
7
- * @param {string} [options.serviceName] - Name of the service (defaults to APP_NAME env var or 'assemble')
8
- * @param {string} [options.loggerUrl] - URL of the azify-logger service (defaults to AZIFY_LOGGER_URL env var or 'http://localhost:3001')
9
- * @param {string} [options.environment] - Environment name (defaults to NODE_ENV env var or 'development')
10
- * @returns {Function} Express middleware function
11
- * @example
12
- * const azifyMiddleware = require('azify-logger/middleware-express');
13
- * app.use(azifyMiddleware({ serviceName: 'my-app' }));
14
- */
15
40
  function createExpressLoggingMiddleware(options = {}) {
16
41
  const config = {
17
- serviceName: options.serviceName || process.env.APP_NAME || 'assemble',
18
- loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001',
19
- environment: options.environment || process.env.NODE_ENV || 'development'
42
+ serviceName: options.serviceName || process.env.APP_NAME,
43
+ loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
44
+ environment: options.environment || process.env.NODE_ENV,
45
+ captureResponseBody: options.captureResponseBody !== false && process.env.AZIFY_LOGGER_CAPTURE_RESPONSE_BODY !== 'false',
46
+ captureRequestBody: options.captureRequestBody !== false && process.env.AZIFY_LOGGER_CAPTURE_REQUEST_BODY !== 'false',
47
+ logRequest: options.logRequest !== false && process.env.AZIFY_LOGGER_LOG_REQUEST !== 'false',
48
+ captureHeaders: options.captureHeaders !== undefined ? options.captureHeaders : process.env.AZIFY_LOGGER_CAPTURE_HEADERS === 'true'
20
49
  }
21
50
 
22
- /**
23
- * Sends a log entry to the azify-logger service
24
- * @param {string} level - Log level (info, error, warn, debug)
25
- * @param {string} message - Log message
26
- * @param {Object} [meta={}] - Additional metadata to include in the log
27
- * @private
28
- */
29
- async function sendLog(level, message, meta = {}) {
30
- const logData = {
31
- level,
32
- message,
33
- meta: {
34
- ...meta,
51
+ const transport = createHttpLoggerTransport(config.loggerUrl, {})
52
+
53
+ const hostname = os.hostname()
54
+
55
+ function sendLog(level, message, meta = {}) {
56
+ if (!transport || typeof transport.enqueue !== 'function') return
57
+
58
+ try {
59
+ const metaObj = {
35
60
  service: {
36
61
  name: config.serviceName,
37
62
  version: '1.0.0'
38
63
  },
39
64
  environment: config.environment,
40
65
  timestamp: new Date().toISOString(),
41
- hostname: require('os').hostname()
66
+ hostname: hostname,
67
+ ...meta
42
68
  }
43
- }
44
-
45
- try {
46
- await axios.post(`${config.loggerUrl}`, logData, {
47
- timeout: 5000
48
- })
49
- } catch (error) {
50
- console.error('Erro ao enviar log:', error.message)
51
- }
69
+
70
+ transport.enqueue({
71
+ level,
72
+ message,
73
+ meta: metaObj
74
+ }, { 'content-type': 'application/json' })
75
+ } catch (err) { }
52
76
  }
53
77
 
54
- /**
55
- * Express middleware function that logs requests and responses
56
- * @param {Object} req - Express request object
57
- * @param {Object} res - Express response object
58
- * @param {Function} next - Express next function
59
- * @returns {void}
60
- */
61
78
  return function azifyExpressLoggingMiddleware(req, res, next) {
62
79
  const startTime = Date.now()
63
- const requestId = req.requestId || require('uuid').v4()
64
- const middlewareId = require('uuid').v4().substring(0, 8)
65
-
66
- let existingTraceId = null
67
- let existingSpanId = null
68
- let existingParentSpanId = null
69
-
70
- if (req.headers['x-trace-id']) {
71
- existingTraceId = req.headers['x-trace-id']
72
- existingSpanId = req.headers['x-span-id']
73
- existingParentSpanId = req.headers['x-parent-span-id']
74
- }
75
-
76
- const currentCtx = getRequestContext()
77
- if (currentCtx && currentCtx.traceId) {
78
- existingTraceId = currentCtx.traceId
79
- existingSpanId = currentCtx.spanId
80
- existingParentSpanId = currentCtx.parentSpanId
81
- }
80
+ const method = req.method
81
+ const url = req.url
82
+ const path = req.path || url
82
83
 
83
- let reqCtx
84
- if (existingTraceId) {
85
- reqCtx = {
86
- traceId: existingTraceId,
87
- spanId: existingSpanId || require('uuid').v4().substring(0, 16),
88
- parentSpanId: existingParentSpanId,
89
- requestId: requestId
90
- }
91
- } else {
92
- reqCtx = startRequestContext({ requestId })
93
- }
84
+ let responseChunk = null
85
+ let responseChunkCaptured = false
86
+ let logSent = false
94
87
 
95
- const requestTraceId = reqCtx.traceId
96
- const requestSpanId = reqCtx.spanId
97
- const requestParentSpanId = reqCtx.parentSpanId
98
-
99
- const originalConsole = {
100
- log: console.log,
101
- info: console.info,
102
- warn: console.warn,
103
- error: console.error
104
- }
105
-
106
- console.log = (...args) => {
107
- const message = args.map(String).join(' ')
108
- sendLog('info', message, {
109
- traceId: requestTraceId,
110
- spanId: requestSpanId,
111
- parentSpanId: requestParentSpanId,
112
- requestId: requestId
113
- })
114
- }
115
-
116
- console.info = (...args) => {
117
- const message = args.map(String).join(' ')
118
- sendLog('info', message, {
119
- traceId: requestTraceId,
120
- spanId: requestSpanId,
121
- parentSpanId: requestParentSpanId,
122
- requestId: requestId
123
- })
124
- }
125
-
126
- console.warn = (...args) => {
127
- const message = args.map(String).join(' ')
128
- sendLog('warn', message, {
129
- traceId: requestTraceId,
130
- spanId: requestSpanId,
131
- parentSpanId: requestParentSpanId,
132
- requestId: requestId
133
- })
134
- }
135
-
136
- console.error = (...args) => {
137
- const message = args.map(String).join(' ')
138
- sendLog('error', message, {
139
- traceId: requestTraceId,
140
- spanId: requestSpanId,
141
- parentSpanId: requestParentSpanId,
142
- requestId: requestId
143
- })
144
- }
88
+ let requestId, traceId, spanId, parentSpanId, reqCtx, clientIp, query, cachedHeaders
89
+ let idsCreated = false
90
+ let headersCached = false
145
91
 
146
- let baseUrl = req.url
147
- if (baseUrl.includes('?')) {
148
- baseUrl = baseUrl.substring(0, baseUrl.indexOf('?'))
149
- }
150
- 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}')
151
- baseUrl = baseUrl.replace(/\/[0-9]+/g, '/{id}')
152
-
153
- function sanitizeHeaders(headers) {
154
- const sanitized = { ...headers }
155
- const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token', 'x-access-token']
92
+ function ensureIds() {
93
+ if (idsCreated) return
94
+ idsCreated = true
156
95
 
157
- for (const key of Object.keys(sanitized)) {
158
- if (sensitiveHeaders.includes(key.toLowerCase())) {
159
- sanitized[key] = '***'
160
- }
161
- }
96
+ query = req.query
162
97
 
163
- return sanitized
164
- }
165
-
166
- function sanitizeBody(body) {
167
- if (!body || typeof body !== 'object') return body
98
+ const currentCtx = getRequestContext()
99
+ const headerTraceId = req.headers['x-trace-id']
100
+ const headerParentSpanId = req.headers['x-parent-span-id']
168
101
 
169
- const sanitized = Array.isArray(body) ? [...body] : { ...body }
170
- const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'api_key', 'accessToken', 'access_token', 'refreshToken', 'refresh_token', 'clientSecret', 'client_secret']
102
+ if (currentCtx?.traceId) {
103
+ traceId = currentCtx.traceId
104
+ } else if (headerTraceId) {
105
+ traceId = headerTraceId
106
+ } else {
107
+ traceId = randomUUID()
108
+ }
171
109
 
172
- for (const key of Object.keys(sanitized)) {
173
- if (sensitiveFields.includes(key) || key.toLowerCase().includes('password') || key.toLowerCase().includes('secret')) {
174
- sanitized[key] = '***'
110
+ parentSpanId = currentCtx?.spanId || headerParentSpanId || null
111
+
112
+ if (!req.requestId) {
113
+ const singleUUID = randomUUID()
114
+ requestId = singleUUID
115
+ spanId = singleUUID.replace(/-/g, '').substring(0, 16)
116
+ } else {
117
+ requestId = req.requestId
118
+ if (requestId.length >= 16) {
119
+ spanId = requestId.replace(/-/g, '').substring(0, 16)
120
+ } else if (traceId.length >= 16) {
121
+ spanId = traceId.replace(/-/g, '').substring(0, 16)
122
+ } else {
123
+ spanId = randomUUID().replace(/-/g, '').substring(0, 16)
175
124
  }
176
125
  }
177
126
 
178
- return sanitized
179
- }
180
-
181
- req._azifyRequestData = {
182
- requestId,
183
- method: req.method,
184
- url: req.url,
185
- baseUrl: baseUrl,
186
- path: req.url,
187
- headers: sanitizeHeaders(req.headers || {}),
188
- query: req.query || {},
189
- userAgent: (req.headers && req.headers['user-agent']) || 'unknown',
190
- ip: (req.connection && req.connection.remoteAddress) || (req.socket && req.socket.remoteAddress) || req.ip || 'unknown',
191
- traceId: requestTraceId,
192
- spanId: requestSpanId,
193
- parentSpanId: requestParentSpanId
127
+ clientIp = req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || 'unknown'
128
+
129
+ reqCtx = startRequestContext({ requestId, traceId, spanId, parentSpanId })
194
130
  }
195
-
196
- if (req.method === 'GET') {
197
- sendLog('info', `[REQUEST] ${req.method} ${req.url}`, req._azifyRequestData)
198
- } else {
199
- if (req.body !== undefined) {
200
- req._azifyRequestData.requestBody = sanitizeBody(req.body)
201
- }
202
- sendLog('info', `[REQUEST] ${req.method} ${req.url}`, req._azifyRequestData)
131
+
132
+ function ensureHeaders() {
133
+ if (headersCached) return
134
+ headersCached = true
135
+ cachedHeaders = config.captureHeaders ? pickHeaders(req.headers || {}) : {}
203
136
  }
204
-
205
- res.on('finish', () => {
206
- if (!responseLogged) {
207
- logResponse()
208
- responseLogged = true
209
- }
210
- console.log = originalConsole.log
211
- console.info = originalConsole.info
212
- console.warn = originalConsole.warn
213
- console.error = originalConsole.error
214
- })
215
-
216
- res.on('close', () => {
217
- if (!responseLogged) {
218
- logResponse()
219
- responseLogged = true
220
- }
221
- console.log = originalConsole.log
222
- console.info = originalConsole.info
223
- console.warn = originalConsole.warn
224
- console.error = originalConsole.error
225
- })
226
-
227
- let sentBody
228
- let responseLogged = false
229
137
 
230
- const originalSend = res.send && res.send.bind(res)
231
- if (originalSend) {
232
- res.send = function patchedSend() {
138
+ if (config.logRequest) {
139
+ setImmediate(() => {
233
140
  try {
234
- if (arguments.length === 1) {
235
- sentBody = arguments[0]
236
- } else if (arguments.length >= 2) {
237
- sentBody = typeof arguments[0] === 'number' ? arguments[1] : (arguments[1] || arguments[0])
141
+ ensureIds()
142
+ ensureHeaders()
143
+ const request = {
144
+ id: requestId,
145
+ method,
146
+ url,
147
+ path,
148
+ query: query || {},
149
+ headers: cachedHeaders,
150
+ ip: clientIp
238
151
  }
239
- } catch (_) {}
240
-
241
- if (!responseLogged) {
242
- logResponse()
243
- responseLogged = true
244
- }
245
-
246
- return originalSend.apply(this, arguments)
247
- }
248
- }
249
-
250
- const originalStatus = res.status
251
- res.status = function(code) {
252
- res._actualStatusCode = code
253
- return originalStatus.call(this, code)
152
+ if (req.body && config.captureRequestBody) {
153
+ request.body = req.body
154
+ }
155
+ sendLog('info', `[REQUEST] ${method} ${url}`, {
156
+ traceId: reqCtx.traceId,
157
+ spanId: reqCtx.spanId,
158
+ parentSpanId: reqCtx.parentSpanId,
159
+ requestId: requestId,
160
+ request: request
161
+ })
162
+ } catch (err) { }
163
+ })
254
164
  }
255
165
 
256
- const originalWriteHead = res.writeHead
257
- res.writeHead = function(statusCode, statusMessage, headers) {
258
- res._actualStatusCode = statusCode
259
- if (typeof statusMessage === 'object') {
260
- headers = statusMessage
261
- statusMessage = undefined
262
- }
263
- return originalWriteHead.call(this, statusCode, statusMessage, headers)
264
- }
166
+ const originalEnd = res.end.bind(res)
265
167
 
266
- const originalJsonMethod = res.json
267
- res.json = function(code, body) {
268
- try {
269
- if (arguments.length === 1) {
270
- sentBody = arguments[0]
271
- } else if (arguments.length >= 2) {
272
- sentBody = typeof arguments[0] === 'number' ? arguments[1] : (arguments[1] || arguments[0])
273
- }
274
- } catch (_) {}
275
-
276
- if (typeof code === 'number') {
277
- res._actualStatusCode = code
278
- } else {
279
- const errorObj = arguments.length === 1 ? arguments[0] : (typeof arguments[0] === 'number' ? arguments[1] : arguments[0])
168
+ res.end = (chunk, encoding) => {
169
+ const result = originalEnd(chunk, encoding)
170
+
171
+ setImmediate(() => {
172
+ if (logSent) return
173
+ logSent = true
280
174
 
281
- if (errorObj && errorObj.constructor && errorObj.constructor.name === 'ErrCtor') {
282
- const errorName = errorObj.toString()
283
- if (errorName.includes('InternalServerError') || errorName.includes('InternalError')) {
284
- res._actualStatusCode = 500
285
- } else if (errorName.includes('BadRequest') || errorName.includes('BadDigest')) {
286
- res._actualStatusCode = 400
287
- } else if (errorName.includes('NotFound')) {
288
- res._actualStatusCode = 404
289
- } else if (errorName.includes('Unauthorized')) {
290
- res._actualStatusCode = 401
291
- } else if (errorName.includes('Forbidden')) {
292
- res._actualStatusCode = 403
293
- } else {
294
- res._actualStatusCode = 500
175
+ try {
176
+ if (chunk != null && config.captureResponseBody && !responseChunkCaptured) {
177
+ responseChunk = chunk
178
+ responseChunkCaptured = true
295
179
  }
296
- } else {
297
- res._actualStatusCode = res.statusCode || 200
298
- }
299
- }
300
-
301
- if (!responseLogged) {
302
- logResponse()
303
- responseLogged = true
304
- }
305
-
306
- return originalJsonMethod.apply(this, arguments)
307
- }
308
-
309
- res.on('finish', function() {
310
- if (!responseLogged) {
311
- logResponse()
312
- responseLogged = true
313
- }
314
- })
315
-
316
- const originalEnd = res.end
317
- res.end = function(chunk, encoding) {
318
- const duration = Date.now() - startTime
319
-
320
- let responseBody = sentBody
321
- try {
322
- if (responseBody == null && chunk) {
323
- if (Buffer.isBuffer(chunk)) {
324
- responseBody = chunk.toString('utf8')
325
- } else if (typeof chunk === 'string') {
326
- responseBody = chunk
327
- } else {
328
- responseBody = JSON.stringify(chunk)
180
+
181
+ ensureIds()
182
+
183
+ const statusCode = res.statusCode || 200
184
+ const duration = Date.now() - startTime
185
+
186
+ const response = { statusCode, durationMs: duration }
187
+ if (responseChunk !== null && responseChunkCaptured && config.captureResponseBody) {
188
+ response.body = responseChunk
329
189
  }
330
- }
331
- } catch (_) {}
332
-
333
- if (!responseLogged) {
334
- logResponse()
335
- responseLogged = true
336
- }
337
-
338
- originalEnd.call(this, chunk, encoding)
339
- }
340
-
341
- /**
342
- * Logs the response data to azify-logger
343
- * @private
344
- */
345
- function logResponse() {
346
- const duration = Date.now() - startTime
347
-
348
- let responseBody = sentBody
349
-
350
- let serializedResponseBody
351
- try {
352
- if (typeof responseBody === 'string') {
353
- serializedResponseBody = responseBody
354
- } else if (Array.isArray(responseBody)) {
355
- serializedResponseBody = JSON.stringify(responseBody)
356
- } else if (responseBody && typeof responseBody === 'object') {
357
- if (responseBody.toJSON && typeof responseBody.toJSON === 'function') {
358
- serializedResponseBody = JSON.stringify(responseBody.toJSON())
359
- } else if (responseBody.toString && typeof responseBody.toString === 'function' && responseBody.toString() !== '[object Object]') {
360
- serializedResponseBody = responseBody.toString()
361
- } else {
362
- serializedResponseBody = JSON.stringify(responseBody, (key, value) => {
363
- if (typeof value === 'function') {
364
- return '[Function]'
365
- }
366
- if (value instanceof Error) {
367
- return { name: value.name, message: value.message, stack: value.stack }
368
- }
369
- return value
370
- }, null, 0)
190
+
191
+ if (config.captureHeaders) {
192
+ ensureHeaders()
371
193
  }
372
- } else {
373
- serializedResponseBody = responseBody != null ? String(responseBody) : ''
374
- }
375
- } catch (error) {
376
- try {
377
- serializedResponseBody = JSON.stringify(responseBody, null, 2)
378
- } catch (secondError) {
379
- serializedResponseBody = '[Complex object - serialization failed]'
380
- }
381
- }
382
-
383
- const statusCode = res._actualStatusCode || res._statusCode || res.statusCode || 200
384
-
385
- const responseMessage = serializedResponseBody && serializedResponseBody.length > 0
386
- ? `[RESPONSE] ${serializedResponseBody}`
387
- : `[RESPONSE] ${req.method} ${req.url} ${statusCode} ${duration}ms`
388
-
389
- const responseData = {
390
- ...req._azifyRequestData,
391
- requestBody: req.body,
392
- statusCode: statusCode,
393
- responseTime: duration,
394
- responseHeaders: res.getHeaders ? res.getHeaders() : {},
395
- responseBody: serializedResponseBody
396
- }
194
+
195
+ const meta = {
196
+ traceId: reqCtx.traceId,
197
+ spanId: reqCtx.spanId,
198
+ parentSpanId: reqCtx.parentSpanId,
199
+ requestId: requestId,
200
+ request: {
201
+ id: requestId,
202
+ method,
203
+ url,
204
+ path,
205
+ query: query || {},
206
+ headers: config.captureHeaders ? (cachedHeaders || {}) : {},
207
+ ip: clientIp
208
+ },
209
+ response: response
210
+ }
211
+
212
+ sendLog('info', `[RESPONSE] ${method} ${url}`, meta)
213
+ } catch (err) { }
214
+ })
397
215
 
398
- try { res._azifyResponseLogged = true } catch (_) {}
399
- sendLog('info', responseMessage, responseData)
216
+ return result
400
217
  }
401
218
 
402
- req._azifyContext = reqCtx
403
-
404
219
  try {
405
- runWithRequestContext(reqCtx, () => {
406
- next()
407
- })
408
- } catch (error) {
409
- console.error('Error in azify middleware:', error)
410
220
  next()
221
+ } catch (err) {
222
+ if (!logSent) {
223
+ logSent = true
224
+ ensureIds()
225
+ runWithRequestContext(reqCtx, () => {
226
+ process.stderr.write(`[azify-logger][middleware] Erro no next(): ${err?.message || String(err)}\n`)
227
+ setImmediate(() => {
228
+ try {
229
+ const statusCode = res.statusCode || 500
230
+ const duration = Date.now() - startTime
231
+
232
+ ensureHeaders()
233
+
234
+ const response = { statusCode, durationMs: duration }
235
+ const meta = {
236
+ traceId: reqCtx.traceId,
237
+ spanId: reqCtx.spanId,
238
+ parentSpanId: reqCtx.parentSpanId,
239
+ requestId: requestId,
240
+ request: {
241
+ id: requestId,
242
+ method,
243
+ url,
244
+ path,
245
+ query: query || {},
246
+ headers: cachedHeaders,
247
+ ip: clientIp
248
+ },
249
+ response: response,
250
+ error: {
251
+ message: err?.message || String(err),
252
+ name: err?.name || 'Error',
253
+ stack: err?.stack
254
+ }
255
+ }
256
+ sendLog('error', `[RESPONSE] ${method} ${url}`, meta)
257
+ } catch (logErr) { }
258
+ })
259
+ })
260
+ }
261
+ throw err
411
262
  }
412
263
  }
413
264
  }