azify-logger 1.0.28 → 1.0.30

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.
package/README.md CHANGED
@@ -10,15 +10,15 @@ Sistema de logging centralizado com OpenTelemetry e OpenSearch para múltiplas a
10
10
  |----------|-----|
11
11
  | **Development** | `http://localhost:3001/log` |
12
12
  | **Staging** | `https://logsdashboard.azify.dev/send` |
13
- | **Production** | `https://logsdashboard.azify.prd/send` | (a configurar)
13
+ | **Production** | `https://cadence.aztech.host/send` |
14
14
 
15
15
  ### URLs para acessar os logs:
16
16
 
17
- | Ambiente | URL |
18
- |----------|-----|
19
- | **Development** | Grafana: `http://localhost:3002` |
17
+ | Ambiente | URL |
18
+ |----------|-----------------------------------|
19
+ | **Development** | `http://localhost:3002` |
20
20
  | **Staging** | `https://logsdashboard.azify.dev` |
21
- | **Production** | `https://logsdashboard.azify.com` |
21
+ | **Production** | `https://cadence.aztech.host` |
22
22
 
23
23
  ## 📦 Instalação
24
24
 
@@ -49,16 +49,31 @@ APP_NAME=nome-app
49
49
  require('azify-logger')
50
50
  const express = require('express')
51
51
  const app = express()
52
- // Logs automáticos via OpenTelemetry
52
+ const azifyMiddleware = require('azify-logger/middleware-express')
53
+ app.use(azifyMiddleware())
54
+ ```
55
+
56
+ ### Para aplicações Fastify:
57
+
58
+ ```javascript
59
+ const fastify = require('fastify')()
60
+ const azifyPlugin = require('azify-logger/middleware-fastify')
61
+
62
+ await fastify.register(azifyPlugin, {
63
+ serviceName: 'minha-app'
64
+ })
65
+
66
+ await fastify.listen({ port: 3000 })
53
67
  ```
54
68
 
55
69
  ## ⚙️ Variáveis de Ambiente
56
70
 
57
- | Variável | Padrão | Descrição |
58
- |----------|-------|-----------|
59
- | `APP_NAME` | - | Nome da aplicação |
60
- | `AZIFY_LOGGER_URL` | `http://localhost:3001/log` | URL do logger |
61
- | `NODE_ENV` | `development` | Ambiente |
71
+ | Variável | Padrão | Descrição |
72
+ |----------|------------------------------------|-----------|
73
+ | `APP_NAME` | - | Nome da aplicação |
74
+ | `AZIFY_LOGGER_URL` | `http://localhost:3001/log` | URL do logger |
75
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318/v1/traces` | Endpoint OTLP para traces (opcional) |
76
+ | `NODE_ENV` | `development` | Ambiente |
62
77
 
63
78
  ## 🎯 O Que Você Ganha
64
79
 
@@ -67,6 +82,27 @@ const app = express()
67
82
  - ✅ **Trace Consistente**: REQUEST e RESPONSE com mesmo traceId/spanId
68
83
  - ✅ **Genérico**: Funciona com Bunyan, Pino, console.log ou qualquer logger
69
84
  - ✅ **Centralizado**: Todos os logs no OpenSearch
85
+ - ✅ **Tracing Completo**: Traces exportados via OTLP para Grafana Tempo (opcional)
86
+
87
+ ## 📡 OpenTelemetry Collector
88
+
89
+ O `azify-logger` inclui um OpenTelemetry Collector configurado para receber traces via OTLP e exportá-los para Grafana Tempo.
90
+
91
+ ### Configuração
92
+
93
+ Para habilitar o envio de traces, configure a variável de ambiente na sua aplicação:
94
+
95
+ ```bash
96
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
97
+ ```
98
+
99
+ ### URLs
100
+
101
+ | Ambiente | URL |
102
+ |----------|-----|
103
+ | **Development** | `http://localhost:4318/v1/traces` |
104
+ | **Staging** | `https://logsdashboard.azify.dev/v1/traces` |
105
+ | **Production** | `https://cadence.aztech.host/v1/traces` |
70
106
 
71
107
  ## 🔍 Como Funciona
72
108
 
@@ -192,11 +228,11 @@ O deploy é feito automaticamente via GitHub Actions.
192
228
 
193
229
  **Comandos seguros** (preservam dados):
194
230
  ```bash
195
- docker-compose stop
196
- docker-compose restart
231
+ docker compose stop
232
+ docker compose restart
197
233
  ```
198
234
 
199
235
  **Comandos destrutivos** (APAGAM logs):
200
236
  ```bash
201
- docker-compose down -v # ⚠️ APAGA VOLUMES!
237
+ docker compose down -v # ⚠️ APAGA VOLUMES!
202
238
  ```
package/index.js CHANGED
@@ -341,7 +341,8 @@ module.exports.streams = {
341
341
  }
342
342
  module.exports.middleware = {
343
343
  restify: require('./middleware-restify'),
344
- express: require('./middleware-express')
344
+ express: require('./middleware-express'),
345
+ fastify: require('./middleware-fastify')
345
346
  }
346
347
  module.exports.startLoggerWorker = require('./queue/workerManager').startLoggerWorker
347
348
  module.exports.stopLoggerWorker = require('./queue/workerManager').stopLoggerWorker
@@ -1,8 +1,63 @@
1
- const { runWithRequestContext, startRequestContext, getRequestContext } = require('./store')
1
+ const { startRequestContext, runWithRequestContext, getRequestContext } = require('./store')
2
2
  const { createHttpLoggerTransport } = require('./streams/httpQueue')
3
- const { randomUUID } = require('crypto')
3
+ const { Worker } = require('worker_threads')
4
+ const path = require('path')
4
5
  const os = require('os')
5
6
 
7
+ // Importar OpenTelemetry para sincronizar contexto
8
+ let trace, otelContext
9
+ try {
10
+ const otelApi = require('@opentelemetry/api')
11
+ trace = otelApi.trace
12
+ otelContext = otelApi.context
13
+ } catch (_) {
14
+ trace = { getSpan: () => null }
15
+ otelContext = { active: () => ({}) }
16
+ }
17
+
18
+ function fastUUID() {
19
+ const timestamp = Date.now().toString(36)
20
+ const randomPart = Math.random().toString(36).substring(2, 15)
21
+ const randomPart2 = Math.random().toString(36).substring(2, 15)
22
+ return `${timestamp}-${randomPart}-${randomPart2}`.substring(0, 36)
23
+ }
24
+
25
+ function sanitizeTraceHex(value) {
26
+ if (!value || typeof value !== 'string') return null
27
+ const hex = value.replace(/[^0-9a-fA-F]/g, '').toLowerCase()
28
+ if (!hex) return null
29
+ return hex.padEnd(32, '0').slice(0, 32)
30
+ }
31
+
32
+ function safeSerializeBody(payload) {
33
+ if (payload == null) return ''
34
+ if (typeof payload === 'string') return payload
35
+ if (Buffer.isBuffer(payload)) return payload.toString('utf8')
36
+ try {
37
+ return JSON.stringify(payload)
38
+ } catch (err) {
39
+ try {
40
+ return String(payload)
41
+ } catch {
42
+ return ''
43
+ }
44
+ }
45
+ }
46
+
47
+ function normalizeChunkToBuffer(chunk) {
48
+ if (chunk == null) return null
49
+ if (Buffer.isBuffer(chunk)) return chunk
50
+ if (typeof chunk === 'string') return Buffer.from(chunk)
51
+ if (chunk && chunk.type === 'Buffer' && Array.isArray(chunk.data)) {
52
+ return Buffer.from(chunk.data)
53
+ }
54
+ try {
55
+ return Buffer.from(String(chunk))
56
+ } catch {
57
+ return null
58
+ }
59
+ }
60
+
6
61
  const HEADER_WHITELIST = new Set([
7
62
  'content-type',
8
63
  'content-length',
@@ -52,27 +107,52 @@ function createExpressLoggingMiddleware(options = {}) {
52
107
 
53
108
  const hostname = os.hostname()
54
109
 
110
+ const serviceObj = config.serviceName ? { name: config.serviceName, version: '1.0.0' } : null
111
+
112
+ const workerPool = []
113
+ const cpuCount = Math.max(1, (os.cpus() || []).length || 1)
114
+ const WORKER_POOL_SIZE = Math.min(8, Math.max(2, cpuCount))
115
+ const maxWorkers = Math.min(WORKER_POOL_SIZE, cpuCount)
116
+ let workerIndex = 0
117
+ let poolInitialized = false
118
+
119
+ function initWorkerPool() {
120
+ try {
121
+ const workerPath = path.join(__dirname, 'utils', 'bodyWorker.js')
122
+ for (let i = 0; i < maxWorkers; i++) {
123
+ try {
124
+ const worker = new Worker(workerPath)
125
+ workerPool.push(worker)
126
+ } catch (err) {
127
+ break
128
+ }
129
+ }
130
+ } catch (err) {
131
+ }
132
+ }
133
+
134
+ function ensureWorkerPool() {
135
+ if (!poolInitialized && (config.captureResponseBody || config.captureRequestBody)) {
136
+ poolInitialized = true
137
+ initWorkerPool()
138
+ }
139
+ }
140
+
141
+ function getWorker() {
142
+ if (workerPool.length === 0) return null
143
+ const worker = workerPool[workerIndex % workerPool.length]
144
+ workerIndex++
145
+ return worker
146
+ }
147
+
55
148
  function sendLog(level, message, meta = {}) {
56
149
  if (!transport || typeof transport.enqueue !== 'function') return
57
150
 
58
- try {
59
- const metaObj = {
60
- service: {
61
- name: config.serviceName,
62
- version: '1.0.0'
63
- },
64
- environment: config.environment,
65
- timestamp: new Date().toISOString(),
66
- hostname: hostname,
67
- ...meta
68
- }
69
-
70
- transport.enqueue({
71
- level,
72
- message,
73
- meta: metaObj
74
- }, { 'content-type': 'application/json' })
75
- } catch (err) { }
151
+ transport.enqueue({
152
+ level,
153
+ message,
154
+ meta
155
+ }, { 'content-type': 'application/json' })
76
156
  }
77
157
 
78
158
  return function azifyExpressLoggingMiddleware(req, res, next) {
@@ -85,181 +165,211 @@ function createExpressLoggingMiddleware(options = {}) {
85
165
  let responseChunkCaptured = false
86
166
  let logSent = false
87
167
 
88
- let requestId, traceId, spanId, parentSpanId, reqCtx, clientIp, query, cachedHeaders
168
+ let requestId, traceId, spanId, parentSpanId, clientIp, query, cachedHeaders
89
169
  let idsCreated = false
90
170
  let headersCached = false
171
+ let reqCtx = null
172
+
173
+ // Função para extrair traceId/spanId do contexto OTEL ativo
174
+ function getOtelTraceContext() {
175
+ try {
176
+ const activeContext = otelContext.active()
177
+ const span = trace.getSpan(activeContext)
178
+ if (span) {
179
+ const spanContext = span.spanContext()
180
+ if (spanContext && spanContext.traceId && spanContext.spanId) {
181
+ // Converter traceId de hex para formato UUID
182
+ const traceHex = spanContext.traceId.replace(/-/g, '')
183
+ return {
184
+ traceId: traceHex.length === 32 ? `${traceHex.substring(0, 8)}-${traceHex.substring(8, 12)}-${traceHex.substring(12, 16)}-${traceHex.substring(16, 20)}-${traceHex.substring(20, 32)}` : spanContext.traceId,
185
+ spanId: spanContext.spanId,
186
+ parentSpanId: null // OTEL gerencia isso internamente
187
+ }
188
+ }
189
+ }
190
+ } catch (_) {}
191
+ return null
192
+ }
193
+
194
+ // Criar contexto inicial sincronizado com OTEL
195
+ function ensureRequestContext() {
196
+ if (reqCtx) return reqCtx
197
+
198
+ // Tentar pegar do OTEL primeiro
199
+ const otelCtx = getOtelTraceContext()
200
+ const traceHex = otelCtx ? otelCtx.traceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(req.headers['x-trace-id'])
201
+
202
+ reqCtx = startRequestContext({
203
+ requestId: req.requestId || fastUUID(),
204
+ traceHex: traceHex || undefined,
205
+ parentSpanId: otelCtx?.parentSpanId || req.headers['x-parent-span-id'] || null
206
+ })
207
+
208
+ // Se OTEL tem contexto, usar os IDs do OTEL
209
+ if (otelCtx) {
210
+ reqCtx.traceId = otelCtx.traceId
211
+ reqCtx.spanId = otelCtx.spanId
212
+ }
213
+
214
+ return reqCtx
215
+ }
216
+
217
+ // Garantir que o contexto está disponível e sincronizado com OTEL ANTES de processar
218
+ const ctx = ensureRequestContext()
219
+
220
+ // Executar a requisição dentro do contexto AsyncLocalStorage
221
+ // Isso garante que todos os logs intermediários peguem o mesmo traceId
222
+ runWithRequestContext(ctx, () => {
223
+ next()
224
+ })
225
+
226
+ const logWithContext = (level, message, meta) => {
227
+ // Sempre tentar pegar do OTEL primeiro (mais confiável)
228
+ const otelCtx = getOtelTraceContext()
229
+ const ctx = getRequestContext() || ensureRequestContext()
230
+
231
+ meta.traceId = otelCtx?.traceId || meta.traceId || ctx.traceId
232
+ meta.spanId = otelCtx?.spanId || meta.spanId || ctx.spanId
233
+ meta.parentSpanId = otelCtx?.parentSpanId || meta.parentSpanId || ctx.parentSpanId
234
+ sendLog(level, message, meta)
235
+ }
91
236
 
92
237
  function ensureIds() {
93
238
  if (idsCreated) return
94
239
  idsCreated = true
95
240
 
96
- query = req.query
241
+ query = req.query || null
97
242
 
98
- const currentCtx = getRequestContext()
99
- const headerTraceId = req.headers['x-trace-id']
100
- const headerParentSpanId = req.headers['x-parent-span-id']
243
+ const ctx = ensureRequestContext()
101
244
 
102
- if (currentCtx?.traceId) {
103
- traceId = currentCtx.traceId
104
- } else if (headerTraceId) {
105
- traceId = headerTraceId
245
+ // Priorizar OTEL se disponível
246
+ const otelCtx = getOtelTraceContext()
247
+ if (otelCtx) {
248
+ traceId = otelCtx.traceId
249
+ spanId = otelCtx.spanId
250
+ parentSpanId = otelCtx.parentSpanId
106
251
  } else {
107
- traceId = randomUUID()
252
+ traceId = ctx.traceId
253
+ spanId = ctx.spanId
254
+ parentSpanId = ctx.parentSpanId || req.headers['x-parent-span-id'] || null
108
255
  }
109
256
 
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)
124
- }
125
- }
257
+ requestId = ctx.requestId || req.requestId || fastUUID()
126
258
 
127
259
  clientIp = req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || 'unknown'
128
-
129
- reqCtx = startRequestContext({ requestId, traceId, spanId, parentSpanId })
260
+ req.requestId = requestId
130
261
  }
131
262
 
132
263
  function ensureHeaders() {
133
264
  if (headersCached) return
134
265
  headersCached = true
135
- cachedHeaders = config.captureHeaders ? pickHeaders(req.headers || {}) : {}
266
+ if (config.captureHeaders) {
267
+ cachedHeaders = pickHeaders(req.headers || {})
268
+ } else {
269
+ cachedHeaders = {}
270
+ }
136
271
  }
137
272
 
273
+ function emitResponseLog(meta, chunk) {
274
+ if (!config.captureResponseBody || chunk == null) {
275
+ logWithContext('info', `[RESPONSE] ${method} ${url}`, meta)
276
+ return
277
+ }
278
+
279
+ if (!meta.response) meta.response = {}
280
+ meta.response.body = safeSerializeBody(chunk)
281
+ logWithContext('info', `[RESPONSE] ${method} ${url}`, meta)
282
+ }
283
+
138
284
  if (config.logRequest) {
139
- setImmediate(() => {
140
- try {
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
151
- }
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) { }
285
+ process.nextTick(() => {
286
+ ensureIds()
287
+ ensureHeaders()
288
+
289
+ const request = {
290
+ id: requestId,
291
+ method,
292
+ url,
293
+ path,
294
+ ip: clientIp
295
+ }
296
+ if (query) request.query = query
297
+ if (config.captureHeaders && cachedHeaders) request.headers = cachedHeaders
298
+ if (config.captureRequestBody && req.body !== undefined && req.body != null) {
299
+ request.body = safeSerializeBody(req.body)
300
+ }
301
+
302
+ const meta = {
303
+ traceId: reqCtx.traceId,
304
+ spanId: reqCtx.spanId,
305
+ parentSpanId: reqCtx.parentSpanId || null,
306
+ requestId,
307
+ request,
308
+ timestamp: Date.now(),
309
+ hostname
310
+ }
311
+ if (serviceObj) meta.service = serviceObj
312
+ if (config.environment) meta.environment = config.environment
313
+
314
+ logWithContext('info', `[REQUEST] ${method} ${url}`, meta)
163
315
  })
164
316
  }
165
-
317
+
166
318
  const originalEnd = res.end.bind(res)
167
319
 
168
320
  res.end = (chunk, encoding) => {
321
+ if (logSent) {
322
+ return originalEnd(chunk, encoding)
323
+ }
324
+ logSent = true
325
+
169
326
  const result = originalEnd(chunk, encoding)
170
327
 
171
- setImmediate(() => {
172
- if (logSent) return
173
- logSent = true
328
+ if (chunk != null && config.captureResponseBody && !responseChunkCaptured) {
329
+ responseChunk = chunk
330
+ responseChunkCaptured = true
331
+ }
332
+
333
+ process.nextTick(() => {
334
+ ensureIds()
174
335
 
175
- try {
176
- if (chunk != null && config.captureResponseBody && !responseChunkCaptured) {
177
- responseChunk = chunk
178
- responseChunkCaptured = true
179
- }
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
189
- }
190
-
191
- if (config.captureHeaders) {
192
- ensureHeaders()
193
- }
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) { }
336
+ const statusCode = res.statusCode || 200
337
+ const duration = Date.now() - startTime
338
+ const response = { statusCode, durationMs: duration }
339
+
340
+ if (config.captureHeaders) {
341
+ ensureHeaders()
342
+ }
343
+
344
+ const requestObj = {
345
+ id: requestId,
346
+ method,
347
+ url,
348
+ path,
349
+ ip: clientIp
350
+ }
351
+ if (query) requestObj.query = query
352
+ if (config.captureHeaders && cachedHeaders) requestObj.headers = cachedHeaders
353
+
354
+ const meta = {
355
+ traceId: reqCtx.traceId,
356
+ spanId: reqCtx.spanId,
357
+ parentSpanId: reqCtx.parentSpanId || null,
358
+ requestId,
359
+ request: requestObj,
360
+ response,
361
+ timestamp: Date.now(),
362
+ hostname
363
+ }
364
+ if (serviceObj) meta.service = serviceObj
365
+ if (config.environment) meta.environment = config.environment
366
+
367
+ const chunkToProcess = (responseChunk !== null && responseChunkCaptured) ? responseChunk : null
368
+ emitResponseLog(meta, chunkToProcess)
214
369
  })
215
370
 
216
371
  return result
217
372
  }
218
-
219
- try {
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
262
- }
263
373
  }
264
374
  }
265
375