azify-logger 1.0.30 → 1.0.31

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
@@ -205,6 +205,22 @@ curl -X POST http://localhost:3001/log \
205
205
 
206
206
  Acesse `http://localhost:3002` e faça login com Azure AD.
207
207
 
208
+ ### Apps fora da mesma máquina / rede (Azure)
209
+
210
+ Por padrão, o `azify-logger` **só aceita chamadas dos próprios serviços locais** (mesma VM / rede privada).
211
+
212
+ Se sua aplicação **não estiver rodando na mesma máquina/VM** do `azify-logger`:
213
+
214
+ - Descubra o **IP público** da aplicação cliente
215
+ - Envie esse IP para que ele seja incluído na configuração interna (`ALLOWED_SOURCE_IPS`)
216
+
217
+ Isso vale tanto para:
218
+
219
+ - Envio de logs (`/log` e `/send`)
220
+ - Envio de traces (`/v1/traces`)
221
+
222
+ Enquanto o IP não for incluído nessa lista, chamadas vindas de fora da mesma máquina/rede serão bloqueadas com `403 Forbidden`.
223
+
208
224
  ## 🚀 Deploy
209
225
 
210
226
  ### Staging - Deploy Automático
@@ -222,6 +238,102 @@ O deploy é feito automaticamente via GitHub Actions.
222
238
  3. Configure: **Environment:** `production`
223
239
  4. Clique em **Run workflow**
224
240
 
241
+ ## 📦 Retenção e Arquivamento de Logs
242
+
243
+ > **⚠️ Importante**: A retenção é configurada **apenas no servidor `azify-logger`**. As aplicações que usam a lib **não precisam se preocupar** com retenção - elas apenas enviam logs normalmente.
244
+
245
+ O `azify-logger` inclui um sistema automático de retenção que:
246
+
247
+ - ✅ **Mantém sempre os últimos 30 dias** de logs disponíveis no OpenSearch para consulta imediata
248
+ - ✅ **Compacta e arquiva** logs mais antigos automaticamente
249
+ - ✅ **Envia para Azure Blob Storage** para armazenamento de longo prazo
250
+ - ✅ **Remove logs antigos** do OpenSearch após arquivamento bem-sucedido
251
+
252
+ ### Como Funciona
253
+
254
+ 1. **Agendamento**: O serviço de retenção executa automaticamente 1x por dia às 3h da manhã (configurável via `RETENTION_RUN_AT_HOUR`)
255
+ 2. **Identificação**: Busca todos os logs com mais de 30 dias no OpenSearch
256
+ 3. **Arquivamento**:
257
+ - Exporta logs mais antigos que 30 dias
258
+ - Compacta em formato ZIP
259
+ - Envia para Azure Blob Storage na estrutura `logs/{app}/{YYYYMM}/{DD}/`
260
+ - Remove do OpenSearch após confirmação de upload bem-sucedido
261
+
262
+ ### Configuração (Apenas no Servidor azify-logger)
263
+
264
+ A configuração de retenção é feita **apenas no servidor `azify-logger`**, no arquivo `env/app.env`:
265
+
266
+ ```bash
267
+ RETENTION_DAYS=30 # Dias de logs mantidos no OpenSearch (padrão: 30)
268
+ RETENTION_RUN_AT_HOUR=3 # Horário de execução diária (0-23, padrão: 3h da manhã)
269
+
270
+ **⚠️ As aplicações que usam a lib `azify-logger` não precisam configurar nada relacionado à retenção.**
271
+
272
+ ### Execução Manual
273
+
274
+ Para executar a retenção manualmente (útil para testes):
275
+
276
+ ```bash
277
+ # Executar uma vez
278
+ npm run retention -- --once
279
+
280
+ # Ou via Docker
281
+ docker exec azify-retention-manager node scripts/retention-manager.js --once
282
+ ```
283
+
284
+ ### Estrutura no Blob Storage
285
+
286
+ Os arquivos são organizados da seguinte forma:
287
+
288
+ ```
289
+ {container}/
290
+ └── logs/
291
+ └── {YYYYMM}/ (ex: 202512/)
292
+ └── log-YYYY-MM-DD-HH-MM-SS-UUID.zip
293
+ ```
294
+
295
+ Cada arquivo ZIP contém:
296
+ - `metadata.json`: Metadados do índice (nome, data de exportação, contagem de documentos, dias de retenção)
297
+ - `{indexName}-{timestamp}.json`: Todos os documentos exportados em JSON
298
+
299
+ O sistema sempre mantém **os últimos 30 dias** de logs disponíveis no OpenSearch/Grafana.
300
+
301
+ ### Scripts Úteis
302
+
303
+ #### Consulta e Validação de Blob Storage
304
+
305
+ ```bash
306
+ # Listar todos os arquivos no blob storage
307
+ npm run list:blob
308
+
309
+ # Listar arquivos ZIP recentes (última hora)
310
+ npm run list:blob:recent
311
+
312
+ # Baixar um arquivo ZIP específico
313
+ npm run download:blob "logs/harmony/202512/05/log-2025-12-05-14-29-01.zip" ~/Downloads
314
+
315
+ # Baixar todos os arquivos de uma pasta
316
+ npm run download:blob "logs/harmony/202512/05/*" ~/Downloads
317
+ ```
318
+
319
+ #### Reimportação de Logs
320
+
321
+ Para reimportar logs arquivados de volta para o OpenSearch (útil para análise de logs antigos):
322
+
323
+ ```bash
324
+ # Reimportar um arquivo ZIP específico
325
+ npm run reimport:logs ~/Downloads/log-2025-12-05-18-31-32.zip
326
+
327
+ # Reimportar todos os ZIPs de uma pasta
328
+ npm run reimport:logs ~/Downloads/arquivos-exportados/
329
+ ```
330
+
331
+ **Importante:**
332
+ - Os logs serão reimportados no índice original
333
+ - Os timestamps originais são preservados
334
+ - A aplicação já deve estar configurada no Grafana (organização e datasource)
335
+ - Após a reimportação, os logs estarão disponíveis no Grafana imediatamente
336
+
225
337
  ## ⚠️ Preservação de Dados
226
338
 
227
339
  **IMPORTANTE:** Logs do OpenSearch são armazenados em volume Docker persistente.
@@ -4,7 +4,6 @@ const { Worker } = require('worker_threads')
4
4
  const path = require('path')
5
5
  const os = require('os')
6
6
 
7
- // Importar OpenTelemetry para sincronizar contexto
8
7
  let trace, otelContext
9
8
  try {
10
9
  const otelApi = require('@opentelemetry/api')
@@ -170,7 +169,6 @@ function createExpressLoggingMiddleware(options = {}) {
170
169
  let headersCached = false
171
170
  let reqCtx = null
172
171
 
173
- // Função para extrair traceId/spanId do contexto OTEL ativo
174
172
  function getOtelTraceContext() {
175
173
  try {
176
174
  const activeContext = otelContext.active()
@@ -178,12 +176,11 @@ function createExpressLoggingMiddleware(options = {}) {
178
176
  if (span) {
179
177
  const spanContext = span.spanContext()
180
178
  if (spanContext && spanContext.traceId && spanContext.spanId) {
181
- // Converter traceId de hex para formato UUID
182
179
  const traceHex = spanContext.traceId.replace(/-/g, '')
183
180
  return {
184
181
  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
182
  spanId: spanContext.spanId,
186
- parentSpanId: null // OTEL gerencia isso internamente
183
+ parentSpanId: null
187
184
  }
188
185
  }
189
186
  }
@@ -191,11 +188,9 @@ function createExpressLoggingMiddleware(options = {}) {
191
188
  return null
192
189
  }
193
190
 
194
- // Criar contexto inicial sincronizado com OTEL
195
191
  function ensureRequestContext() {
196
192
  if (reqCtx) return reqCtx
197
193
 
198
- // Tentar pegar do OTEL primeiro
199
194
  const otelCtx = getOtelTraceContext()
200
195
  const traceHex = otelCtx ? otelCtx.traceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(req.headers['x-trace-id'])
201
196
 
@@ -205,7 +200,6 @@ function createExpressLoggingMiddleware(options = {}) {
205
200
  parentSpanId: otelCtx?.parentSpanId || req.headers['x-parent-span-id'] || null
206
201
  })
207
202
 
208
- // Se OTEL tem contexto, usar os IDs do OTEL
209
203
  if (otelCtx) {
210
204
  reqCtx.traceId = otelCtx.traceId
211
205
  reqCtx.spanId = otelCtx.spanId
@@ -214,17 +208,13 @@ function createExpressLoggingMiddleware(options = {}) {
214
208
  return reqCtx
215
209
  }
216
210
 
217
- // Garantir que o contexto está disponível e sincronizado com OTEL ANTES de processar
218
211
  const ctx = ensureRequestContext()
219
212
 
220
- // Executar a requisição dentro do contexto AsyncLocalStorage
221
- // Isso garante que todos os logs intermediários peguem o mesmo traceId
222
213
  runWithRequestContext(ctx, () => {
223
214
  next()
224
215
  })
225
216
 
226
217
  const logWithContext = (level, message, meta) => {
227
- // Sempre tentar pegar do OTEL primeiro (mais confiável)
228
218
  const otelCtx = getOtelTraceContext()
229
219
  const ctx = getRequestContext() || ensureRequestContext()
230
220
 
@@ -242,7 +232,6 @@ function createExpressLoggingMiddleware(options = {}) {
242
232
 
243
233
  const ctx = ensureRequestContext()
244
234
 
245
- // Priorizar OTEL se disponível
246
235
  const otelCtx = getOtelTraceContext()
247
236
  if (otelCtx) {
248
237
  traceId = otelCtx.traceId
@@ -1,7 +1,17 @@
1
- const { startRequestContext } = require('./store')
1
+ const { startRequestContext, runWithRequestContext, getRequestContext } = require('./store')
2
2
  const { createHttpLoggerTransport } = require('./streams/httpQueue')
3
3
  const os = require('os')
4
4
 
5
+ let trace, otelContext
6
+ try {
7
+ const otelApi = require('@opentelemetry/api')
8
+ trace = otelApi.trace
9
+ otelContext = otelApi.context
10
+ } catch (_) {
11
+ trace = { getSpan: () => null }
12
+ otelContext = { active: () => ({}) }
13
+ }
14
+
5
15
  function fastUUID() {
6
16
  const timestamp = Date.now().toString(36)
7
17
  const randomPart = Math.random().toString(36).substring(2, 15)
@@ -92,6 +102,50 @@ function createFastifyLoggingPlugin(options = {}) {
92
102
 
93
103
  return async function azifyFastifyPlugin(fastify, opts) {
94
104
  fastify.addHook('onRequest', async (request, reply) => {
105
+ let reqCtx = null
106
+
107
+ function getOtelTraceContext() {
108
+ try {
109
+ const activeContext = otelContext.active()
110
+ const span = trace.getSpan(activeContext)
111
+ if (span) {
112
+ const spanContext = span.spanContext()
113
+ if (spanContext && spanContext.traceId && spanContext.spanId) {
114
+ const traceHex = spanContext.traceId.replace(/-/g, '')
115
+ return {
116
+ 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,
117
+ spanId: spanContext.spanId,
118
+ parentSpanId: null
119
+ }
120
+ }
121
+ }
122
+ } catch (_) {}
123
+ return null
124
+ }
125
+
126
+ function ensureRequestContext() {
127
+ if (reqCtx) return reqCtx
128
+
129
+ const otelCtx = getOtelTraceContext()
130
+ const traceHex = otelCtx ? otelCtx.traceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(request.headers['x-trace-id'])
131
+
132
+ reqCtx = startRequestContext({
133
+ requestId: request.requestId || fastUUID(),
134
+ traceHex: traceHex || undefined,
135
+ parentSpanId: otelCtx?.parentSpanId || request.headers['x-parent-span-id'] || null
136
+ })
137
+
138
+ if (otelCtx) {
139
+ reqCtx.traceId = otelCtx.traceId
140
+ reqCtx.spanId = otelCtx.spanId
141
+ }
142
+
143
+ return reqCtx
144
+ }
145
+
146
+ const ctx = ensureRequestContext()
147
+
148
+ runWithRequestContext(ctx, () => {})
95
149
  const startTime = Date.now()
96
150
  const method = request.method
97
151
  const url = request.url
@@ -104,20 +158,14 @@ function createFastifyLoggingPlugin(options = {}) {
104
158
  let requestId, traceId, spanId, parentSpanId, clientIp, query, cachedHeaders
105
159
  let idsCreated = false
106
160
  let headersCached = false
107
- let reqCtx = null
108
161
 
109
162
  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
163
+ const otelCtx = getOtelTraceContext()
164
+ const ctx = getRequestContext() || ensureRequestContext()
165
+
166
+ meta.traceId = otelCtx?.traceId || meta.traceId || ctx.traceId
167
+ meta.spanId = otelCtx?.spanId || meta.spanId || ctx.spanId
168
+ meta.parentSpanId = otelCtx?.parentSpanId || meta.parentSpanId || ctx.parentSpanId
121
169
  sendLog(level, message, meta)
122
170
  }
123
171
 
@@ -127,20 +175,20 @@ function createFastifyLoggingPlugin(options = {}) {
127
175
 
128
176
  query = request.query || null
129
177
 
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
- }
178
+ const ctx = ensureRequestContext()
138
179
 
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
180
+ const otelCtx = getOtelTraceContext()
181
+ if (otelCtx) {
182
+ traceId = otelCtx.traceId
183
+ spanId = otelCtx.spanId
184
+ parentSpanId = otelCtx.parentSpanId
185
+ } else {
186
+ traceId = ctx.traceId
187
+ spanId = ctx.spanId
188
+ parentSpanId = ctx.parentSpanId || request.headers['x-parent-span-id'] || null
189
+ }
143
190
 
191
+ requestId = ctx.requestId || request.requestId || fastUUID()
144
192
  clientIp = request.ip || request.socket?.remoteAddress || 'unknown'
145
193
  request.requestId = requestId
146
194
  }
@@ -204,10 +252,13 @@ function createFastifyLoggingPlugin(options = {}) {
204
252
  requestObj.body = safeSerializeBody(request.body)
205
253
  }
206
254
 
255
+ const otelCtx = getOtelTraceContext()
256
+ const ctx = getRequestContext() || ensureRequestContext()
257
+
207
258
  const meta = {
208
- traceId: reqCtx.traceId,
209
- spanId: reqCtx.spanId,
210
- parentSpanId: reqCtx.parentSpanId || null,
259
+ traceId: otelCtx?.traceId || ctx.traceId,
260
+ spanId: otelCtx?.spanId || ctx.spanId,
261
+ parentSpanId: otelCtx?.parentSpanId || ctx.parentSpanId || null,
211
262
  requestId,
212
263
  request: requestObj,
213
264
  timestamp: Date.now(),
@@ -240,20 +291,53 @@ function createFastifyLoggingPlugin(options = {}) {
240
291
  let requestId, traceId, spanId, parentSpanId, clientIp, query, cachedHeaders
241
292
  let idsCreated = false
242
293
  let headersCached = false
243
- let reqCtx = null
244
294
 
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
- })
295
+ function getOtelTraceContext() {
296
+ try {
297
+ const activeContext = otelContext.active()
298
+ const span = trace.getSpan(activeContext)
299
+ if (span) {
300
+ const spanContext = span.spanContext()
301
+ if (spanContext && spanContext.traceId && spanContext.spanId) {
302
+ const traceHex = spanContext.traceId.replace(/-/g, '')
303
+ return {
304
+ 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,
305
+ spanId: spanContext.spanId,
306
+ parentSpanId: null
307
+ }
308
+ }
309
+ }
310
+ } catch (_) {}
311
+ return null
312
+ }
313
+
314
+ function ensureRequestContext() {
315
+ if (reqCtx) return reqCtx
316
+
317
+ const otelCtx = getOtelTraceContext()
318
+ const traceHex = otelCtx ? otelCtx.traceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(request.headers['x-trace-id'])
319
+
320
+ reqCtx = startRequestContext({
321
+ requestId: request.requestId || fastUUID(),
322
+ traceHex: traceHex || undefined,
323
+ parentSpanId: otelCtx?.parentSpanId || request.headers['x-parent-span-id'] || null
324
+ })
325
+
326
+ if (otelCtx) {
327
+ reqCtx.traceId = otelCtx.traceId
328
+ reqCtx.spanId = otelCtx.spanId
253
329
  }
254
- meta.traceId = meta.traceId || reqCtx.traceId
255
- meta.spanId = meta.spanId || reqCtx.spanId
256
- meta.parentSpanId = meta.parentSpanId || reqCtx.parentSpanId
330
+
331
+ return reqCtx
332
+ }
333
+
334
+ const logWithContext = (level, message, meta) => {
335
+ const otelCtx = getOtelTraceContext()
336
+ const ctx = getRequestContext() || ensureRequestContext()
337
+
338
+ meta.traceId = otelCtx?.traceId || meta.traceId || ctx.traceId
339
+ meta.spanId = otelCtx?.spanId || meta.spanId || ctx.spanId
340
+ meta.parentSpanId = otelCtx?.parentSpanId || meta.parentSpanId || ctx.parentSpanId
257
341
  sendLog(level, message, meta)
258
342
  }
259
343
 
@@ -263,20 +347,20 @@ function createFastifyLoggingPlugin(options = {}) {
263
347
 
264
348
  query = request.query || null
265
349
 
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
- }
350
+ const ctx = ensureRequestContext()
274
351
 
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
352
+ const otelCtx = getOtelTraceContext()
353
+ if (otelCtx) {
354
+ traceId = otelCtx.traceId
355
+ spanId = otelCtx.spanId
356
+ parentSpanId = otelCtx.parentSpanId
357
+ } else {
358
+ traceId = ctx.traceId
359
+ spanId = ctx.spanId
360
+ parentSpanId = ctx.parentSpanId || request.headers['x-parent-span-id'] || null
361
+ }
279
362
 
363
+ requestId = ctx.requestId || request.requestId || fastUUID()
280
364
  clientIp = request.ip || request.socket?.remoteAddress || 'unknown'
281
365
  }
282
366
 
@@ -322,10 +406,13 @@ function createFastifyLoggingPlugin(options = {}) {
322
406
  if (query) requestObj.query = query
323
407
  if (config.captureHeaders && cachedHeaders) requestObj.headers = cachedHeaders
324
408
 
409
+ const otelCtx = getOtelTraceContext()
410
+ const ctx = getRequestContext() || ensureRequestContext()
411
+
325
412
  const meta = {
326
- traceId: reqCtx.traceId,
327
- spanId: reqCtx.spanId,
328
- parentSpanId: reqCtx.parentSpanId || null,
413
+ traceId: otelCtx?.traceId || ctx.traceId,
414
+ spanId: otelCtx?.spanId || ctx.spanId,
415
+ parentSpanId: otelCtx?.parentSpanId || ctx.parentSpanId || null,
329
416
  requestId,
330
417
  request: requestObj,
331
418
  response,
@@ -1,7 +1,17 @@
1
- const { startRequestContext } = require('./store')
1
+ const { startRequestContext, runWithRequestContext, getRequestContext } = require('./store')
2
2
  const { createHttpLoggerTransport } = require('./streams/httpQueue')
3
3
  const os = require('os')
4
4
 
5
+ let trace, otelContext
6
+ try {
7
+ const otelApi = require('@opentelemetry/api')
8
+ trace = otelApi.trace
9
+ otelContext = otelApi.context
10
+ } catch (_) {
11
+ trace = { getSpan: () => null }
12
+ otelContext = { active: () => ({}) }
13
+ }
14
+
5
15
  function fastUUID() {
6
16
  const timestamp = Date.now().toString(36)
7
17
  const randomPart = Math.random().toString(36).substring(2, 15)
@@ -78,18 +88,54 @@ function createRestifyLoggingMiddleware (options = {}) {
78
88
 
79
89
  return function azifyLoggingMiddleware (req, res, next) {
80
90
  const startTime = Date.now()
81
- const requestId = req.requestId || fastUUID()
82
91
 
83
92
  if (res._azifySetup) {
84
93
  return next()
85
94
  }
86
95
  res._azifySetup = true
87
96
 
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
97
+ function getOtelTraceContext() {
98
+ try {
99
+ const activeContext = otelContext.active()
100
+ const span = trace.getSpan(activeContext)
101
+ if (span) {
102
+ const spanContext = span.spanContext()
103
+ if (spanContext && spanContext.traceId && spanContext.spanId) {
104
+ const traceHex = spanContext.traceId.replace(/-/g, '')
105
+ return {
106
+ 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,
107
+ spanId: spanContext.spanId,
108
+ parentSpanId: null
109
+ }
110
+ }
111
+ }
112
+ } catch (_) {}
113
+ return null
114
+ }
115
+
116
+ function ensureRequestContext() {
117
+ const otelCtx = getOtelTraceContext()
118
+ const traceHex = otelCtx ? otelCtx.traceId.replace(/-/g, '').substring(0, 32) : sanitizeTraceHex(req.headers['x-trace-id'])
119
+
120
+ const reqCtx = startRequestContext({
121
+ requestId: req.requestId || fastUUID(),
122
+ traceHex: traceHex || undefined,
123
+ parentSpanId: otelCtx?.parentSpanId || req.headers['x-parent-span-id'] || null
124
+ })
125
+
126
+ if (otelCtx) {
127
+ reqCtx.traceId = otelCtx.traceId
128
+ reqCtx.spanId = otelCtx.spanId
129
+ }
130
+
131
+ return reqCtx
132
+ }
133
+
134
+ const reqCtx = ensureRequestContext()
135
+ const requestId = reqCtx.requestId
136
+
137
+ runWithRequestContext(reqCtx, () => {
138
+ next()
93
139
  })
94
140
 
95
141
  let normalizedPath = req.url
@@ -143,11 +189,14 @@ function createRestifyLoggingMiddleware (options = {}) {
143
189
  const duration = Date.now() - startTime
144
190
  const statusCode = responseStatus || res.statusCode || 200
145
191
  const responseHeaders = typeof res.getHeaders === 'function' ? res.getHeaders() : {}
192
+
193
+ const otelCtx = getOtelTraceContext()
194
+ const ctx = getRequestContext() || reqCtx
146
195
 
147
196
  const meta = {
148
- traceId: reqCtx.traceId,
149
- spanId: reqCtx.spanId,
150
- parentSpanId: reqCtx.parentSpanId || null,
197
+ traceId: otelCtx?.traceId || ctx.traceId,
198
+ spanId: otelCtx?.spanId || ctx.spanId,
199
+ parentSpanId: otelCtx?.parentSpanId || ctx.parentSpanId || null,
151
200
  requestId,
152
201
  request: requestSnapshot,
153
202
  response: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "Azify Logger Client - Centralized logging for OpenSearch",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -17,7 +17,18 @@
17
17
  "docker:up": "docker-compose up -d",
18
18
  "docker:down": "docker-compose down",
19
19
  "docker:logs": "docker-compose logs -f",
20
- "worker": "node scripts/redis-worker.js"
20
+ "worker": "node scripts/redis-worker.js",
21
+ "validate:retention": "node scripts/validate-retention.js",
22
+ "inspect:zip": "node scripts/inspect-zip.js",
23
+ "retention": "node scripts/retention-manager.js",
24
+ "list:blob": "node scripts/list-blob-files.js",
25
+ "list:blob:recent": "node scripts/download-blob-zip.js",
26
+ "check:old-logs": "node scripts/check-old-logs.js",
27
+ "validate:blob": "node scripts/validate-blob-zip.js",
28
+ "validate:blob:keep": "KEEP_VALIDATION_FILES=true node scripts/validate-blob-zip.js",
29
+ "download:blob": "node scripts/download-blob-zip.js",
30
+ "reimport:logs": "node scripts/reimport-logs.js",
31
+ "check:server-time": "node -e \"const now = new Date(); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; console.log('🕐 Hora do servidor:', now.toLocaleString('pt-BR', {timeZone: tz})); console.log('🌍 Timezone:', tz); console.log('🕐 Hora em São Paulo:', now.toLocaleString('pt-BR', {timeZone: 'America/Sao_Paulo'})); console.log('\\n💡 Configure RETENTION_RUN_AT_HOUR com a hora do servidor (0-23)');\""
21
32
  },
22
33
  "keywords": [
23
34
  "logging",
@@ -28,6 +39,7 @@
28
39
  "author": "Azify",
29
40
  "license": "MIT",
30
41
  "dependencies": {
42
+ "@azure/storage-blob": "^12.17.0",
31
43
  "@opentelemetry/api": "1.0.4",
32
44
  "@opentelemetry/auto-instrumentations-node": "0.27.0",
33
45
  "@opentelemetry/core": "1.0.1",
@@ -38,6 +50,8 @@
38
50
  "@opentelemetry/resources": "1.0.1",
39
51
  "@opentelemetry/sdk-node": "0.27.0",
40
52
  "@opentelemetry/semantic-conventions": "1.0.1",
53
+ "adm-zip": "^0.5.16",
54
+ "archiver": "^6.0.1",
41
55
  "axios": "^1.6.0",
42
56
  "cors": "^2.8.5",
43
57
  "express": "^4.18.2",
package/register.js CHANGED
@@ -580,68 +580,143 @@ try {
580
580
 
581
581
  instance.interceptors.response.use(
582
582
  (response) => {
583
- const url = buildUrl(response.config)
584
- const testMeta = { url }
585
- if (isLoggerApiCall(testMeta)) {
586
- return response
587
- }
583
+ try {
584
+ if (!response || !response.config) {
585
+ return response
586
+ }
588
587
 
589
- const marker = response.config?.__azifyLogger
590
- const childCtx = response.config?.__azifyChildCtx
588
+ const url = buildUrl(response.config) || response.config?.url || response.request?.responseURL || 'unknown'
589
+ const testMeta = { url }
590
+ if (isLoggerApiCall(testMeta)) {
591
+ return response
592
+ }
591
593
 
592
- if (marker && marker.meta) {
593
- const duration = Number((performance.now() - marker.start).toFixed(2))
594
+ const marker = response.config?.__azifyLogger
595
+ const method = (response.config?.method || 'get').toUpperCase()
596
+ const requestHeaders = response.config?.headers || {}
597
+ const hasTraceHeaders = !!(requestHeaders['x-trace-id'] || requestHeaders['X-Trace-ID'] || requestHeaders['x-span-id'] || requestHeaders['X-Span-ID'])
594
598
  const shouldLogResponse =
595
599
  HTTP_CLIENT_MODE === 'all' ||
596
600
  (HTTP_CLIENT_MODE === 'errors' && response.status >= 400)
597
601
 
598
- if (shouldLogResponse) {
599
- const meta = {
600
- ...marker.meta,
601
- statusCode: response.status,
602
- responseTimeMs: duration,
603
- responseHeaders: response.headers,
604
- responseBody: response.data
602
+ if (shouldLogResponse || hasTraceHeaders) {
603
+ const finalUrl = (url && url !== 'unknown') ? url : (marker?.meta?.url || response.config?.url || response.request?.responseURL || url)
604
+
605
+ if (finalUrl && finalUrl !== 'unknown') {
606
+ let meta
607
+ let duration = 0
608
+
609
+ if (marker && marker.meta) {
610
+ duration = Number((performance.now() - marker.start).toFixed(2))
611
+ meta = {
612
+ ...marker.meta,
613
+ url: finalUrl,
614
+ statusCode: response.status,
615
+ responseTimeMs: duration,
616
+ responseHeaders: response.headers,
617
+ responseBody: response.data
618
+ }
619
+ } else {
620
+ const requestHeaders = response.config?.headers || {}
621
+ const ctx = getRequestContext()
622
+ const traceId = requestHeaders['x-trace-id'] || requestHeaders['X-Trace-ID'] || ctx?.traceId || randomUUID()
623
+ const spanId = requestHeaders['x-span-id'] || requestHeaders['X-Span-ID'] || randomBytes(8).toString('hex')
624
+ const parentSpanId = requestHeaders['x-parent-span-id'] || requestHeaders['X-Parent-Span-ID'] || ctx?.spanId || null
625
+ const requestId = requestHeaders['x-request-id'] || requestHeaders['X-Request-ID'] || ctx?.requestId || randomUUID()
626
+
627
+ meta = {
628
+ traceId,
629
+ spanId,
630
+ parentSpanId,
631
+ requestId,
632
+ method,
633
+ url: finalUrl,
634
+ statusCode: response.status,
635
+ responseTimeMs: duration,
636
+ responseHeaders: response.headers,
637
+ responseBody: response.data
638
+ }
605
639
  }
606
640
 
607
641
  markSource(meta, 'http-client')
608
- const message = `[RESPONSE] ${meta.method} ${meta.url} ${response.status} ${duration}ms`
642
+ const message = `[RESPONSE] ${meta.method} ${meta.url} ${response.status} ${meta.responseTimeMs}ms`
609
643
  const level = response.status >= 500 ? 'error' : response.status >= 400 ? 'warn' : 'info'
610
644
 
611
645
  sendOutboundLog(level, message, meta)
646
+ }
612
647
  }
648
+ } catch (err) {
613
649
  }
614
650
 
615
651
  return response
616
652
  },
617
653
  (error) => {
618
- const config = error?.config
619
- if (config) {
620
- const url = buildUrl(config)
621
- const testMeta = { url }
622
- if (isLoggerApiCall(testMeta)) {
623
- return Promise.reject(error)
654
+ try {
655
+ const config = error?.config
656
+ if (config) {
657
+ const url = buildUrl(config)
658
+ const testMeta = { url }
659
+ if (isLoggerApiCall(testMeta)) {
660
+ return Promise.reject(error)
661
+ }
624
662
  }
625
- }
626
663
 
627
- const marker = config?.__azifyLogger
628
- const childCtx = config?.__azifyChildCtx
629
-
630
- if (marker && marker.meta) {
631
- const duration = Number((performance.now() - marker.start).toFixed(2))
632
- const meta = {
633
- ...marker.meta,
634
- responseTimeMs: duration,
635
- error: {
636
- name: error?.name || 'Error',
637
- message: error?.message || String(error),
638
- stack: error?.stack
664
+ const marker = config?.__azifyLogger
665
+ const method = (config?.method || 'get').toUpperCase()
666
+ const url = config ? buildUrl(config) : (error?.request?.responseURL || error?.config?.url || 'unknown')
667
+ const shouldLogError = HTTP_CLIENT_MODE === 'all' || HTTP_CLIENT_MODE === 'errors'
668
+
669
+ if (shouldLogError && url && url !== 'unknown') {
670
+ let meta
671
+ let duration = 0
672
+
673
+ if (marker && marker.meta) {
674
+ duration = Number((performance.now() - marker.start).toFixed(2))
675
+ meta = {
676
+ ...marker.meta,
677
+ responseTimeMs: duration,
678
+ error: {
679
+ name: error?.name || 'Error',
680
+ message: error?.message || String(error),
681
+ stack: error?.stack,
682
+ code: error?.code,
683
+ status: error?.response?.status,
684
+ statusText: error?.response?.statusText
685
+ }
686
+ }
687
+ } else {
688
+ const requestHeaders = config?.headers || {}
689
+ const ctx = getRequestContext()
690
+ const traceId = requestHeaders['x-trace-id'] || requestHeaders['X-Trace-ID'] || ctx?.traceId || randomUUID()
691
+ const spanId = requestHeaders['x-span-id'] || requestHeaders['X-Span-ID'] || randomBytes(8).toString('hex')
692
+ const parentSpanId = requestHeaders['x-parent-span-id'] || requestHeaders['X-Parent-Span-ID'] || ctx?.spanId || null
693
+ const requestId = requestHeaders['x-request-id'] || requestHeaders['X-Request-ID'] || ctx?.requestId || randomUUID()
694
+
695
+ meta = {
696
+ traceId,
697
+ spanId,
698
+ parentSpanId,
699
+ requestId,
700
+ method,
701
+ url,
702
+ responseTimeMs: duration,
703
+ error: {
704
+ name: error?.name || 'Error',
705
+ message: error?.message || String(error),
706
+ stack: error?.stack,
707
+ code: error?.code,
708
+ status: error?.response?.status,
709
+ statusText: error?.response?.statusText
710
+ }
711
+ }
639
712
  }
713
+
714
+ markSource(meta, 'http-client')
715
+ const message = `[ERROR] ${meta.method} ${meta.url}`
716
+
717
+ sendOutboundLog('error', message, meta)
640
718
  }
641
- markSource(meta, 'http-client')
642
- const message = `[ERROR] ${meta.method} ${meta.url}`
643
-
644
- sendOutboundLog('error', message, meta)
719
+ } catch (err) {
645
720
  }
646
721
 
647
722
  return Promise.reject(error)
@@ -735,18 +810,19 @@ try {
735
810
  })
736
811
 
737
812
  const duration = Number((performance.now() - start).toFixed(2))
813
+ const hasTraceHeaders = !!(requestMeta.traceId && requestMeta.spanId)
738
814
  const shouldLogResponse =
739
815
  HTTP_CLIENT_MODE === 'all' ||
740
- (HTTP_CLIENT_MODE === 'errors' && response.status >= 400)
816
+ (HTTP_CLIENT_MODE === 'errors' && response.status >= 400) ||
817
+ hasTraceHeaders
741
818
 
742
819
  if (shouldLogResponse) {
743
- (async () => {
820
+ const logResponse = async () => {
821
+ let responseBody = null
822
+ const contentType = response.headers.get('content-type') || ''
823
+
744
824
  try {
745
825
  const clonedResponse = response.clone()
746
- const contentType = response.headers.get('content-type') || ''
747
-
748
- let responseBody = null
749
-
750
826
  if (contentType.includes('application/json') || contentType.includes('text/')) {
751
827
  const bodyText = await clonedResponse.text()
752
828
  if (contentType.includes('application/json')) {
@@ -759,7 +835,26 @@ try {
759
835
  responseBody = bodyText
760
836
  }
761
837
  }
838
+ } catch (cloneError) {
839
+ try {
840
+ if (contentType.includes('application/json') || contentType.includes('text/')) {
841
+ const bodyText = await response.text()
842
+ if (contentType.includes('application/json')) {
843
+ try {
844
+ responseBody = JSON.parse(bodyText)
845
+ } catch (_) {
846
+ responseBody = bodyText
847
+ }
848
+ } else {
849
+ responseBody = bodyText
850
+ }
851
+ }
852
+ } catch (_) {
853
+ responseBody = null
854
+ }
855
+ }
762
856
 
857
+ try {
763
858
  const responseMeta = {
764
859
  ...requestMeta,
765
860
  statusCode: response.status,
@@ -773,21 +868,27 @@ try {
773
868
 
774
869
  const level = response.status >= 500 ? 'error' : response.status >= 400 ? 'warn' : 'info'
775
870
  sendOutboundLog(level, message, responseMeta)
776
- } catch (_) {
777
- const responseMeta = {
778
- ...requestMeta,
779
- statusCode: response.status,
780
- responseTimeMs: duration,
781
- responseHeaders: Object.fromEntries(response.headers.entries()),
782
- responseBody: null
871
+ } catch (err) {
872
+ try {
873
+ const responseMeta = {
874
+ ...requestMeta,
875
+ statusCode: response.status,
876
+ responseTimeMs: duration,
877
+ responseHeaders: Object.fromEntries(response.headers.entries()),
878
+ responseBody: null
879
+ }
880
+
881
+ markSource(responseMeta, 'http-client')
882
+ const message = `[RESPONSE] ${method} ${url} ${response.status} ${duration}ms`
883
+
884
+ const level = response.status >= 500 ? 'error' : response.status >= 400 ? 'warn' : 'info'
885
+ sendOutboundLog(level, message, responseMeta)
886
+ } catch (_) {
783
887
  }
784
-
785
- markSource(responseMeta, 'http-client')
786
- const message = `[RESPONSE] ${method} ${url} ${response.status} ${duration}ms`
787
- const level = response.status >= 500 ? 'error' : response.status >= 400 ? 'warn' : 'info'
788
- sendOutboundLog(level, message, responseMeta)
789
888
  }
790
- })()
889
+ }
890
+
891
+ logResponse().catch(() => {})
791
892
  }
792
893
 
793
894
  return response
@@ -81,6 +81,15 @@ process.on('uncaughtException', (err) => {
81
81
  })
82
82
 
83
83
  process.on('unhandledRejection', (reason) => {
84
+ const errMsg = String(reason && reason.message ? reason.message : reason)
85
+ const errCode = String(reason && reason.code ? reason.code : '')
86
+
87
+ if (errMsg.includes('BUSYGROUP') ||
88
+ errMsg.includes('Consumer Group name already exists') ||
89
+ errCode === 'BUSYGROUP' ||
90
+ (reason && reason.command && reason.command.name === 'xgroup' && reason.command.args && reason.command.args[0] === 'CREATE' && errMsg.includes('already exists'))) {
91
+ return
92
+ }
84
93
  console.error('[azify-logger][worker] unhandledRejection:', reason)
85
94
  process.exit(1)
86
95
  })
@@ -89,9 +98,16 @@ async function ensureGroup() {
89
98
  try {
90
99
  await redis.xgroup('CREATE', STREAM_KEY, WORKER_GROUP, '0', 'MKSTREAM')
91
100
  } catch (err) {
92
- if (!String(err && err.message).includes('BUSYGROUP')) {
93
- throw err
101
+ const errMsg = String(err && err.message ? err.message : err)
102
+ const errCode = String(err && err.code ? err.code : '')
103
+
104
+ if (errMsg.includes('BUSYGROUP') ||
105
+ errMsg.includes('Consumer Group name already exists') ||
106
+ errCode === 'BUSYGROUP' ||
107
+ (err && err.command && err.command.name === 'xgroup' && err.command.args && err.command.args[0] === 'CREATE' && errMsg.includes('already exists'))) {
108
+ return
94
109
  }
110
+ throw err
95
111
  }
96
112
  }
97
113
 
@@ -355,16 +371,17 @@ async function consumeLoop() {
355
371
  consecutiveNoGroupErrors = 0
356
372
  } catch (err) {
357
373
  const errMsg = err && err.message ? err.message : String(err)
358
- if (!errMsg.includes('BUSYGROUP')) {
359
- const now = Date.now()
360
- if (now - lastRedisErrorLog > 5000) {
361
- console.error('[azify-logger][worker] erro ao garantir grupo:', errMsg)
362
- lastRedisErrorLog = now
363
- }
364
- await sleep(1000)
365
- } else {
374
+ if (errMsg.includes('BUSYGROUP') || errMsg.includes('Consumer Group name already exists')) {
366
375
  groupEnsured = true
376
+ consecutiveNoGroupErrors = 0
377
+ continue
378
+ }
379
+ const now = Date.now()
380
+ if (now - lastRedisErrorLog > 5000) {
381
+ console.error('[azify-logger][worker] erro ao garantir grupo:', errMsg)
382
+ lastRedisErrorLog = now
367
383
  }
384
+ await sleep(1000)
368
385
  continue
369
386
  }
370
387
  }
@@ -434,6 +451,15 @@ ensureGroup()
434
451
  return consumeLoop()
435
452
  })
436
453
  .catch((err) => {
437
- console.error('[azify-logger][worker] não foi possível iniciar:', err)
454
+ const errMsg = err && err.message ? err.message : String(err)
455
+ if (errMsg.includes('BUSYGROUP') || errMsg.includes('Consumer Group name already exists')) {
456
+ console.log('[azify-logger][worker] Consumer Group já existe, continuando...')
457
+ console.log('[azify-logger][worker] consumindo stream', STREAM_KEY, 'como', CONSUMER_NAME)
458
+ if (process.send) {
459
+ process.send({ type: 'azify-logger:ready', pid: process.pid, stream: STREAM_KEY })
460
+ }
461
+ return consumeLoop()
462
+ }
463
+ console.error('[azify-logger][worker] não foi possível iniciar:', errMsg)
438
464
  process.exit(1)
439
465
  })
package/server.js CHANGED
@@ -72,7 +72,7 @@ function isPrivateOrLocalhost(ip) {
72
72
  }
73
73
 
74
74
  function validateNetworkAccess(req, res, next) {
75
- if (req.path === '/health' || req.path === '/') {
75
+ if (req.path === '/health' || req.path === '/' || req.path === '/v1/traces') {
76
76
  return next()
77
77
  }
78
78
 
@@ -385,6 +385,7 @@ async function setupGrafanaForApp(appName) {
385
385
  console.log(`[setupGrafana] 📊 Criando datasource para ${appName}...`)
386
386
 
387
387
  const datasourceUid = `opensearch-${appName.toLowerCase()}`
388
+ const indexName = `logs-${appName}`
388
389
  const datasourceConfig = {
389
390
  name: `OpenSearch-${appName}`,
390
391
  type: 'grafana-opensearch-datasource',
@@ -393,8 +394,8 @@ async function setupGrafanaForApp(appName) {
393
394
  uid: datasourceUid,
394
395
  isDefault: true,
395
396
  jsonData: {
396
- index: `logs-${appName}`,
397
- database: `logs-${appName}`,
397
+ index: indexName,
398
+ database: indexName,
398
399
  timeField: '@timestamp',
399
400
  esVersion: '2.11.1',
400
401
  version: '2.11.1',
@@ -481,7 +482,6 @@ async function setupGrafanaForApp(appName) {
481
482
  }
482
483
  }
483
484
 
484
- // Datasource Grafana Tempo para tracing completo (opcional - não bloqueia criação da org)
485
485
  try {
486
486
  const tempoDatasourceUid = `tempo-${appName.toLowerCase()}`
487
487
  const tempoUrl = runningInDocker ? 'http://azify-tempo:3200' : 'http://localhost:3200'
@@ -949,6 +949,19 @@ async function handleLog(req, res) {
949
949
  message = decodeHtmlEntities(message)
950
950
  }
951
951
 
952
+ const requestPath = meta?.request?.path || meta?.request?.url || meta?.request?.baseUrl || meta?.url || meta?.path || ''
953
+ const requestPathLower = String(requestPath).toLowerCase()
954
+ const messageLower = String(message).toLowerCase()
955
+
956
+ const isSwaggerPath = (
957
+ requestPathLower.includes('/api-docs') ||
958
+ requestPathLower.includes('/swagger-ui') ||
959
+ requestPathLower.includes('/swagger.json') ||
960
+ requestPathLower.includes('/swagger.yaml') ||
961
+ requestPathLower.includes('/swagger.yml') ||
962
+ (requestPathLower.includes('/favicon') && (requestPathLower.includes('/api-docs') || requestPathLower.includes('/swagger')))
963
+ )
964
+
952
965
  const shouldFilterLog = (
953
966
  message.includes('prisma:query') ||
954
967
  message.includes('prisma:info') ||
@@ -958,11 +971,21 @@ async function handleLog(req, res) {
958
971
  message.includes('UPDATE `') ||
959
972
  message.includes('DELETE `') ||
960
973
  message.includes('%s: %s') ||
961
- message.includes('Application Startup Time')
974
+ message.includes('Application Startup Time') ||
975
+ (isSwaggerPath && (
976
+ messageLower.includes('swaggerdoc') ||
977
+ messageLower.includes('"openapi"') ||
978
+ messageLower.includes('swagger-ui-bundle') ||
979
+ messageLower.includes('swagger-ui-standalone') ||
980
+ messageLower.includes('swagger-ui.css') ||
981
+ messageLower.includes('[request]') ||
982
+ messageLower.includes('[response]') ||
983
+ requestPathLower.includes('/favicon')
984
+ ))
962
985
  )
963
986
 
964
987
  if (shouldFilterLog) {
965
- return res.json({ success: true, message: 'Log filtrado (Prisma verboso)' })
988
+ return res.json({ success: true, message: 'Log filtrado' })
966
989
  }
967
990
 
968
991
  const requestId = meta && meta.requestId
@@ -1188,6 +1211,26 @@ async function handleLog(req, res) {
1188
1211
  }
1189
1212
 
1190
1213
  app.post('/log', (req, res) => handleLog(req, res))
1214
+
1215
+ app.post('/v1/traces', async (req, res) => {
1216
+ try {
1217
+ const collectorUrl = process.env.OTEL_COLLECTOR_URL || 'http://localhost:4318'
1218
+ const response = await axios.post(`${collectorUrl}/v1/traces`, req.body, {
1219
+ headers: {
1220
+ 'Content-Type': 'application/json'
1221
+ },
1222
+ data: req.body,
1223
+ timeout: 30000
1224
+ })
1225
+ res.status(response.status).json(response.data)
1226
+ } catch (err) {
1227
+ if (err.response) {
1228
+ res.status(err.response.status).json(err.response.data)
1229
+ } else {
1230
+ res.status(500).json({ error: err.message })
1231
+ }
1232
+ }
1233
+ })
1191
1234
  app.post('/send', (req, res) => handleLog(req, res))
1192
1235
 
1193
1236
  const port = process.env.PORT || 3001