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.
@@ -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
+