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 +112 -0
- package/middleware-express.js +1 -12
- package/middleware-fastify.js +142 -55
- package/middleware-restify.js +59 -10
- package/package.json +16 -2
- package/register.js +160 -59
- package/scripts/redis-worker.js +37 -11
- package/server.js +49 -6
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.
|
package/middleware-express.js
CHANGED
|
@@ -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
|
|
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
|
package/middleware-fastify.js
CHANGED
|
@@ -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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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:
|
|
209
|
-
spanId:
|
|
210
|
-
parentSpanId:
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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:
|
|
327
|
-
spanId:
|
|
328
|
-
parentSpanId:
|
|
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,
|
package/middleware-restify.js
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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:
|
|
149
|
-
spanId:
|
|
150
|
-
parentSpanId:
|
|
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.
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
583
|
+
try {
|
|
584
|
+
if (!response || !response.config) {
|
|
585
|
+
return response
|
|
586
|
+
}
|
|
588
587
|
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
593
|
-
const
|
|
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
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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} ${
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
package/scripts/redis-worker.js
CHANGED
|
@@ -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
|
-
|
|
93
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
397
|
-
database:
|
|
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
|
|
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
|