azify-logger 1.0.28 → 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.
@@ -0,0 +1,348 @@
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
+ }
11
+
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
+ }
18
+
19
+ function safeSerializeBody(payload) {
20
+ if (payload == null) return ''
21
+ if (typeof payload === 'string') return payload
22
+ if (Buffer.isBuffer(payload)) return payload.toString('utf8')
23
+ try {
24
+ return JSON.stringify(payload)
25
+ } catch (err) {
26
+ try {
27
+ return String(payload)
28
+ } catch {
29
+ return ''
30
+ }
31
+ }
32
+ }
33
+
34
+ const HEADER_WHITELIST = new Set([
35
+ 'content-type',
36
+ 'content-length',
37
+ 'accept',
38
+ 'accept-encoding',
39
+ 'user-agent',
40
+ 'host',
41
+ 'x-request-id',
42
+ 'x-trace-id',
43
+ 'x-span-id',
44
+ 'x-parent-span-id'
45
+ ])
46
+
47
+ function pickHeaders(source) {
48
+ if (!source || typeof source !== 'object') {
49
+ return {}
50
+ }
51
+ const result = {}
52
+ for (const key in source) {
53
+ const lower = key.toLowerCase()
54
+ if (!HEADER_WHITELIST.has(lower)) continue
55
+
56
+ if (!Object.prototype.hasOwnProperty.call(source, key)) continue
57
+
58
+ const value = source[key]
59
+ if (Array.isArray(value)) {
60
+ result[key] = value.map(String)
61
+ } else if (value != null) {
62
+ result[key] = String(value)
63
+ }
64
+ }
65
+ return result
66
+ }
67
+
68
+ function createFastifyLoggingPlugin(options = {}) {
69
+ const config = {
70
+ serviceName: options.serviceName || process.env.APP_NAME,
71
+ loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
72
+ environment: options.environment || process.env.NODE_ENV,
73
+ captureResponseBody: options.captureResponseBody !== false && process.env.AZIFY_LOGGER_CAPTURE_RESPONSE_BODY !== 'false',
74
+ captureRequestBody: options.captureRequestBody !== false && process.env.AZIFY_LOGGER_CAPTURE_REQUEST_BODY !== 'false',
75
+ logRequest: options.logRequest !== false && process.env.AZIFY_LOGGER_LOG_REQUEST !== 'false',
76
+ captureHeaders: options.captureHeaders !== undefined ? options.captureHeaders : process.env.AZIFY_LOGGER_CAPTURE_HEADERS === 'true'
77
+ }
78
+
79
+ const transport = createHttpLoggerTransport(config.loggerUrl, {})
80
+ const hostname = os.hostname()
81
+ const serviceObj = config.serviceName ? { name: config.serviceName, version: '1.0.0' } : null
82
+
83
+ function sendLog(level, message, meta = {}) {
84
+ if (!transport || typeof transport.enqueue !== 'function') return
85
+
86
+ transport.enqueue({
87
+ level,
88
+ message,
89
+ meta
90
+ }, { 'content-type': 'application/json' })
91
+ }
92
+
93
+ return async function azifyFastifyPlugin(fastify, opts) {
94
+ fastify.addHook('onRequest', async (request, reply) => {
95
+ const startTime = Date.now()
96
+ const method = request.method
97
+ const url = request.url
98
+ const path = request.routerPath || url
99
+
100
+ let responseChunk = null
101
+ let responseChunkCaptured = false
102
+ let logSent = false
103
+
104
+ let requestId, traceId, spanId, parentSpanId, clientIp, query, cachedHeaders
105
+ let idsCreated = false
106
+ let headersCached = false
107
+ let reqCtx = null
108
+
109
+ const logWithContext = (level, message, meta) => {
110
+ if (!reqCtx) {
111
+ const traceHex = sanitizeTraceHex(request.headers['x-trace-id'])
112
+ reqCtx = startRequestContext({
113
+ requestId: requestId || request.requestId || fastUUID(),
114
+ traceHex,
115
+ parentSpanId: request.headers['x-parent-span-id'] || null
116
+ })
117
+ }
118
+ meta.traceId = meta.traceId || reqCtx.traceId
119
+ meta.spanId = meta.spanId || reqCtx.spanId
120
+ meta.parentSpanId = meta.parentSpanId || reqCtx.parentSpanId
121
+ sendLog(level, message, meta)
122
+ }
123
+
124
+ function ensureIds() {
125
+ if (idsCreated) return
126
+ idsCreated = true
127
+
128
+ query = request.query || null
129
+
130
+ if (!reqCtx) {
131
+ const traceHex = sanitizeTraceHex(request.headers['x-trace-id'])
132
+ reqCtx = startRequestContext({
133
+ requestId: request.requestId || fastUUID(),
134
+ traceHex,
135
+ parentSpanId: request.headers['x-parent-span-id'] || null
136
+ })
137
+ }
138
+
139
+ requestId = reqCtx.requestId || request.requestId || fastUUID()
140
+ traceId = reqCtx.traceId
141
+ spanId = reqCtx.spanId
142
+ parentSpanId = reqCtx.parentSpanId || request.headers['x-parent-span-id'] || null
143
+
144
+ clientIp = request.ip || request.socket?.remoteAddress || 'unknown'
145
+ request.requestId = requestId
146
+ }
147
+
148
+ function ensureHeaders() {
149
+ if (headersCached) return
150
+ headersCached = true
151
+ if (config.captureHeaders) {
152
+ cachedHeaders = pickHeaders(request.headers || {})
153
+ } else {
154
+ cachedHeaders = {}
155
+ }
156
+ }
157
+
158
+ function emitResponseLog(meta, chunk) {
159
+ if (!config.captureResponseBody || chunk == null) {
160
+ logWithContext('info', `[RESPONSE] ${method} ${url}`, meta)
161
+ return
162
+ }
163
+
164
+ if (!meta.response) meta.response = {}
165
+ meta.response.body = safeSerializeBody(chunk)
166
+ logWithContext('info', `[RESPONSE] ${method} ${url}`, meta)
167
+ }
168
+
169
+ request.azifyLogger = {
170
+ startTime,
171
+ method,
172
+ url,
173
+ path,
174
+ responseChunk: null,
175
+ responseChunkCaptured: false,
176
+ logSent: false,
177
+ requestId: null,
178
+ traceId: null,
179
+ spanId: null,
180
+ parentSpanId: null,
181
+ clientIp: null,
182
+ query: null,
183
+ cachedHeaders: null,
184
+ idsCreated: false,
185
+ headersCached: false,
186
+ reqCtx: null
187
+ }
188
+
189
+ if (config.logRequest) {
190
+ process.nextTick(() => {
191
+ ensureIds()
192
+ ensureHeaders()
193
+
194
+ const requestObj = {
195
+ id: requestId,
196
+ method,
197
+ url,
198
+ path,
199
+ ip: clientIp
200
+ }
201
+ if (query) requestObj.query = query
202
+ if (config.captureHeaders && cachedHeaders) requestObj.headers = cachedHeaders
203
+ if (config.captureRequestBody && request.body !== undefined && request.body != null) {
204
+ requestObj.body = safeSerializeBody(request.body)
205
+ }
206
+
207
+ const meta = {
208
+ traceId: reqCtx.traceId,
209
+ spanId: reqCtx.spanId,
210
+ parentSpanId: reqCtx.parentSpanId || null,
211
+ requestId,
212
+ request: requestObj,
213
+ timestamp: Date.now(),
214
+ hostname
215
+ }
216
+ if (serviceObj) meta.service = serviceObj
217
+ if (config.environment) meta.environment = config.environment
218
+
219
+ logWithContext('info', `[REQUEST] ${method} ${url}`, meta)
220
+ })
221
+ }
222
+ })
223
+
224
+ fastify.addHook('onSend', async (request, reply, payload) => {
225
+ const logger = request.azifyLogger
226
+ if (!logger || logger.logSent) return payload
227
+
228
+ logger.logSent = true
229
+
230
+ if (config.captureResponseBody && payload != null && !logger.responseChunkCaptured) {
231
+ logger.responseChunk = payload
232
+ logger.responseChunkCaptured = true
233
+ }
234
+
235
+ const startTime = logger.startTime
236
+ const method = logger.method
237
+ const url = logger.url
238
+ const path = logger.path
239
+
240
+ let requestId, traceId, spanId, parentSpanId, clientIp, query, cachedHeaders
241
+ let idsCreated = false
242
+ let headersCached = false
243
+ let reqCtx = null
244
+
245
+ const logWithContext = (level, message, meta) => {
246
+ if (!reqCtx) {
247
+ const traceHex = sanitizeTraceHex(request.headers['x-trace-id'])
248
+ reqCtx = startRequestContext({
249
+ requestId: requestId || request.requestId || fastUUID(),
250
+ traceHex,
251
+ parentSpanId: request.headers['x-parent-span-id'] || null
252
+ })
253
+ }
254
+ meta.traceId = meta.traceId || reqCtx.traceId
255
+ meta.spanId = meta.spanId || reqCtx.spanId
256
+ meta.parentSpanId = meta.parentSpanId || reqCtx.parentSpanId
257
+ sendLog(level, message, meta)
258
+ }
259
+
260
+ function ensureIds() {
261
+ if (idsCreated) return
262
+ idsCreated = true
263
+
264
+ query = request.query || null
265
+
266
+ if (!reqCtx) {
267
+ const traceHex = sanitizeTraceHex(request.headers['x-trace-id'])
268
+ reqCtx = startRequestContext({
269
+ requestId: request.requestId || fastUUID(),
270
+ traceHex,
271
+ parentSpanId: request.headers['x-parent-span-id'] || null
272
+ })
273
+ }
274
+
275
+ requestId = reqCtx.requestId || request.requestId || fastUUID()
276
+ traceId = reqCtx.traceId
277
+ spanId = reqCtx.spanId
278
+ parentSpanId = reqCtx.parentSpanId || request.headers['x-parent-span-id'] || null
279
+
280
+ clientIp = request.ip || request.socket?.remoteAddress || 'unknown'
281
+ }
282
+
283
+ function ensureHeaders() {
284
+ if (headersCached) return
285
+ headersCached = true
286
+ if (config.captureHeaders) {
287
+ cachedHeaders = pickHeaders(request.headers || {})
288
+ } else {
289
+ cachedHeaders = {}
290
+ }
291
+ }
292
+
293
+ function emitResponseLog(meta, chunk) {
294
+ if (!config.captureResponseBody || chunk == null) {
295
+ logWithContext('info', `[RESPONSE] ${method} ${url}`, meta)
296
+ return
297
+ }
298
+
299
+ if (!meta.response) meta.response = {}
300
+ meta.response.body = safeSerializeBody(chunk)
301
+ logWithContext('info', `[RESPONSE] ${method} ${url}`, meta)
302
+ }
303
+
304
+ process.nextTick(() => {
305
+ ensureIds()
306
+
307
+ const statusCode = reply.statusCode || 200
308
+ const duration = Date.now() - startTime
309
+ const response = { statusCode, durationMs: duration }
310
+
311
+ if (config.captureHeaders) {
312
+ ensureHeaders()
313
+ }
314
+
315
+ const requestObj = {
316
+ id: requestId,
317
+ method,
318
+ url,
319
+ path,
320
+ ip: clientIp
321
+ }
322
+ if (query) requestObj.query = query
323
+ if (config.captureHeaders && cachedHeaders) requestObj.headers = cachedHeaders
324
+
325
+ const meta = {
326
+ traceId: reqCtx.traceId,
327
+ spanId: reqCtx.spanId,
328
+ parentSpanId: reqCtx.parentSpanId || null,
329
+ requestId,
330
+ request: requestObj,
331
+ response,
332
+ timestamp: Date.now(),
333
+ hostname
334
+ }
335
+ if (serviceObj) meta.service = serviceObj
336
+ if (config.environment) meta.environment = config.environment
337
+
338
+ const chunkToProcess = (logger.responseChunk !== null && logger.responseChunkCaptured) ? logger.responseChunk : null
339
+ emitResponseLog(meta, chunkToProcess)
340
+ })
341
+
342
+ return payload
343
+ })
344
+ }
345
+ }
346
+
347
+ module.exports = createFastifyLoggingPlugin
348
+
@@ -1,8 +1,21 @@
1
- const { runWithRequestContext, startRequestContext, getRequestContext } = require('./store')
1
+ const { startRequestContext } = require('./store')
2
2
  const { createHttpLoggerTransport } = require('./streams/httpQueue')
3
- const { randomUUID } = require('crypto')
4
3
  const os = require('os')
5
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
+ }
11
+
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
+ }
18
+
6
19
  const HEADER_WHITELIST = new Set([
7
20
  'content-type',
8
21
  'content-length',
@@ -21,10 +34,17 @@ function pickHeaders (source) {
21
34
  return {}
22
35
  }
23
36
  const result = {}
24
- for (const [key, value] of Object.entries(source)) {
37
+ for (const key in source) {
25
38
  const lower = key.toLowerCase()
26
- if (HEADER_WHITELIST.has(lower)) {
27
- result[key] = Array.isArray(value) ? value.map(String) : (value != null ? String(value) : value)
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)
28
48
  }
29
49
  }
30
50
  return result
@@ -37,42 +57,40 @@ function createRestifyLoggingMiddleware (options = {}) {
37
57
  environment: options.environment || process.env.NODE_ENV
38
58
  }
39
59
 
40
- const transport = createHttpLoggerTransport(config.loggerUrl)
60
+ const transport = createHttpLoggerTransport(config.loggerUrl, {})
61
+ const hostname = os.hostname()
62
+ const serviceObj = config.serviceName ? { name: config.serviceName, version: '1.0.0' } : null
41
63
 
42
64
  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) { }
65
+ if (!transport || typeof transport.enqueue !== 'function') return
66
+
67
+ if (serviceObj) meta.service = serviceObj
68
+ if (config.environment) meta.environment = config.environment
69
+ meta.timestamp = Date.now()
70
+ meta.hostname = hostname
71
+
72
+ transport.enqueue({
73
+ level,
74
+ message,
75
+ meta
76
+ }, { 'content-type': 'application/json' })
59
77
  }
60
78
 
61
79
  return function azifyLoggingMiddleware (req, res, next) {
62
80
  const startTime = Date.now()
63
- const requestId = req.requestId || randomUUID()
81
+ const requestId = req.requestId || fastUUID()
64
82
 
65
83
  if (res._azifySetup) {
66
84
  return next()
67
85
  }
68
86
  res._azifySetup = true
69
87
 
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 })
88
+ const traceHex = sanitizeTraceHex(req.headers['x-trace-id'])
89
+ const reqCtx = startRequestContext({
90
+ requestId,
91
+ traceHex,
92
+ parentSpanId: req.headers['x-parent-span-id'] || null
93
+ })
76
94
 
77
95
  let normalizedPath = req.url
78
96
  if (normalizedPath.includes('?')) {
@@ -115,29 +133,37 @@ function createRestifyLoggingMiddleware (options = {}) {
115
133
  }
116
134
  }
117
135
 
136
+ let logSent = false
137
+
118
138
  function emitLog (level, message, extraMeta = {}) {
119
- const duration = Date.now() - startTime
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
135
- }
139
+ if (logSent) return
140
+ logSent = true
141
+
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
159
+ }
136
160
 
137
- sendLog(level, message, meta)
161
+ sendLog(level, message, meta)
162
+ })
138
163
  }
139
164
 
140
165
  function finalize (level = 'info', errorMeta) {
166
+ if (logSent) return
141
167
  const message = `${req.method} ${req.url}`
142
168
  const meta = errorMeta ? { error: errorMeta } : {}
143
169
  emitLog(level, message, meta)
@@ -159,18 +185,7 @@ function createRestifyLoggingMiddleware (options = {}) {
159
185
  })
160
186
  })
161
187
 
162
- runWithRequestContext(reqCtx, () => {
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
- }
173
- })
188
+ next()
174
189
  }
175
190
  }
176
191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.28",
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,7 +9,7 @@
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",
@@ -28,27 +28,26 @@
28
28
  "author": "Azify",
29
29
  "license": "MIT",
30
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",
31
41
  "axios": "^1.6.0",
32
- "ioredis": "^5.4.1",
33
42
  "cors": "^2.8.5",
34
43
  "express": "^4.18.2",
35
44
  "express-session": "^1.17.3",
45
+ "ioredis": "^5.8.2",
46
+ "js-yaml": "^4.1.0",
36
47
  "passport": "^0.6.0",
37
48
  "passport-azure-ad": "^4.3.5",
38
- "js-yaml": "^4.1.0",
39
49
  "require-in-the-middle": "^7.4.0",
40
- "uuid": "^9.0.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"
50
+ "uuid": "^9.0.1"
52
51
  },
53
52
  "overrides": {
54
53
  "@opentelemetry/api": "1.0.4",
@@ -59,7 +58,6 @@
59
58
  "@opentelemetry/exporter-trace-otlp-http": "0.27.0",
60
59
  "@opentelemetry/auto-instrumentations-node": "0.27.0"
61
60
  },
62
- "optionalDependencies": {},
63
61
  "engines": {
64
62
  "node": ">=12 <=22"
65
63
  },
@@ -77,6 +75,7 @@
77
75
  "middleware-restify.js",
78
76
  "middleware-express.js",
79
77
  "middleware-express.d.ts",
78
+ "middleware-fastify.js",
80
79
  "server.js",
81
80
  "sampling.js",
82
81
  "queue/",