azify-logger 1.0.23 → 1.0.25

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
@@ -6,36 +6,22 @@ Sistema de logging centralizado com OpenTelemetry e OpenSearch para múltiplas a
6
6
 
7
7
  ### URLs para configurar nas aplicações:
8
8
 
9
- | Ambiente | URL |
10
- |----------|-------------------------------------------------------|
11
- | **Development** | `http://localhost:3001/log` |
12
- | **Staging** | `https://logsdashboard.azify.dev/send` |
13
- | **Production** | `https://logsdashboard.azify.prd/send` (a configurar) |
9
+ | Ambiente | URL |
10
+ |----------|-----|
11
+ | **Development** | `http://localhost:3001/log` |
12
+ | **Staging** | `https://logsdashboard.azify.dev/send` |
13
+ | **Production** | `https://logsdashboard.azify.prd/send` | (a configurar)
14
14
 
15
+ ### URLs para acessar os logs:
15
16
 
16
- ### URLs para acessar os logs e dashs:
17
-
18
- | Ambiente | URL |
19
- |----------|--------------------------------------------------|
20
- | **Development** | `http://localhost:5601` |
21
- | **Staging** | `https://logsdashboard.azify.dev` |
22
- | **Production** | `https://logsdashboard.azify.com` (a configurar) |
23
-
17
+ | Ambiente | URL |
18
+ |----------|-----|
19
+ | **Development** | Grafana: `http://localhost:3002` |
20
+ | **Staging** | `https://logsdashboard.azify.dev` |
21
+ | **Production** | `https://logsdashboard.azify.com` |
24
22
 
25
23
  ## 📦 Instalação
26
24
 
27
- Na sua aplicação, adicione ao `package.json`:
28
-
29
- ```json
30
- {
31
- "dependencies": {
32
- "azify-logger": "latest"
33
- }
34
- }
35
- ```
36
-
37
- Ou via npm:
38
-
39
25
  ```bash
40
26
  npm install azify-logger
41
27
  ```
@@ -44,91 +30,36 @@ npm install azify-logger
44
30
 
45
31
  ### Para aplicações Restify:
46
32
 
47
- **1. No arquivo de inicialização:**
48
33
  ```javascript
49
- #!/usr/bin/env node
34
+ // 1. No arquivo de inicialização
50
35
  require('azify-logger/register-otel');
51
- // Resto do código...
52
- ```
53
36
 
54
- **2. No servidor Restify:**
55
- ```javascript
37
+ // 2. No servidor
56
38
  import { middleware as azifyMiddleware } from 'azify-logger'
57
-
58
39
  const server = restify.createServer()
59
40
  server.use(azifyMiddleware.restify())
60
- ```
61
41
 
62
- **3. Variável de ambiente:**
63
- ```bash
42
+ // 3. Variável de ambiente
64
43
  APP_NAME=nome-app
65
44
  ```
66
45
 
67
- **PRONTO! 🎉**
68
-
69
46
  ### Para aplicações Express:
70
47
 
71
48
  ```javascript
72
49
  require('azify-logger')
73
-
74
50
  const express = require('express')
75
51
  const app = express()
76
52
  // Logs automáticos via OpenTelemetry
77
53
  ```
78
54
 
79
- ### ❌ O que NÃO precisa:
80
- - ❌ Instalar dependências OpenTelemetry separadamente
81
- - ❌ Configurar tracing manualmente
82
-
83
55
  ## ⚙️ Variáveis de Ambiente
84
56
 
85
- | Variável | Padrão | Descrição |
86
- |----------|-----------------------------------|-----------|
87
- | `APP_NAME` | - | Nome da aplicação |
88
- | `AZIFY_LOGGER_URL` | `http://localhost:3001/log` | URL do logger |
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 |
89
61
  | `NODE_ENV` | `development` | Ambiente |
90
62
 
91
- ### 🌐 URLs por Ambiente
92
-
93
- **Desenvolvimento (Local):**
94
- ```env
95
- APP_NAME=minha-app
96
- AZIFY_LOGGER_URL=http://localhost:3001/log
97
- NODE_ENV=development
98
- ```
99
-
100
- **Staging:**
101
- ```env
102
- APP_NAME=minha-app
103
- AZIFY_LOGGER_URL=https://logsdashboard.azify.dev/send
104
- NODE_ENV=staging
105
- ```
106
-
107
- **Production:**
108
- ```env
109
- APP_NAME=minha-app
110
- AZIFY_LOGGER_URL=https://logsdashboard.azify.prd/send (a conigurar)
111
- NODE_ENV=production
112
- ```
113
-
114
- ### 🐳 Docker Compose (mesma network)
115
-
116
- Apenas se estiver usando Docker Compose com rede compartilhada:
117
-
118
- ```env
119
- AZIFY_LOGGER_URL=http://azify-logger:3001/log
120
- ```
121
-
122
- ### Docker e Node antigos
123
-
124
- Se seu container usa uma versão antiga do Node e você ver erros de inicialização do OpenTelemetry (ex.: `Cannot find module 'node:events'` saindo de `google-logging-utils` ou `@opentelemetry/resource-detector-gcp`), defina no container:
125
-
126
- ```bash
127
- AZIFY_LOGGER_AUTOREG_DISABLE=1
128
- ```
129
-
130
- Isso evita carregar `register-otel.js` e mantém os envios de log via streams/middleware normalmente, permitindo visualização no OpenSearch.
131
-
132
63
  ## 🎯 O Que Você Ganha
133
64
 
134
65
  - ✅ **Zero Config**: OpenTelemetry habilitado automaticamente
@@ -137,89 +68,61 @@ Isso evita carregar `register-otel.js` e mantém os envios de log via streams/mi
137
68
  - ✅ **Genérico**: Funciona com Bunyan, Pino, console.log ou qualquer logger
138
69
  - ✅ **Centralizado**: Todos os logs no OpenSearch
139
70
 
140
- ## 🔧 Uso Avançado
71
+ ## 🔍 Como Funciona
141
72
 
142
- ### Com Bunyan existente (adicionar stream)
73
+ 1. **OpenTelemetry** cria um span para cada request HTTP
74
+ 2. **Middleware** captura request e response com o **mesmo span ativo**
75
+ 3. **Logger** envia ambos os logs com **traceId e spanId iguais**
76
+ 4. **OpenSearch** indexa e permite buscar por traceId
143
77
 
144
- Se sua app **já tem Bunyan** e você quer adicionar o azify-logger como stream adicional:
78
+ ## 📖 Arquitetura
145
79
 
146
- ```javascript
147
- // Forma segura com try/catch (não quebra se não tiver instalado)
148
- let createAzifyBunyanStream
149
- try {
150
- createAzifyBunyanStream = require('azify-logger/streams/bunyan')
151
- } catch (_) {
152
- createAzifyBunyanStream = null
153
- }
154
-
155
- const bunyan = require('bunyan')
156
- const streams = [
157
- { level: 'info', stream: process.stdout }
158
- ]
159
-
160
- // Adiciona stream do azify-logger se disponível
161
- if (createAzifyBunyanStream) {
162
- streams.push({
163
- level: 'info',
164
- type: 'raw',
165
- stream: createAzifyBunyanStream({
166
- loggerUrl: process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
167
- serviceName: process.env.APP_NAME || 'app'
168
- loggerUrl: process.env.AZIFY_LOGGER_URL,
169
- serviceName: process.env.APP_NAME
170
- })
171
- })
172
- }
173
-
174
- const logger = bunyan.createLogger({ name: 'app', streams })
175
- logger.info('Teste') // Vai para stdout E para azify-logger
176
80
  ```
177
-
178
- ### Com Bunyan (auto-patch - mais simples)
179
-
180
- ```javascript
181
- require('azify-logger')
182
-
183
- const bunyan = require('bunyan')
184
- const log = bunyan.createLogger({ name: 'nome-app' })
185
-
186
- log.info('Teste') // Automaticamente enviado para azify-logger
81
+ ┌─────────────┐
82
+ │ aplicação │
83
+ └──────┬──────┘
84
+ │ HTTP POST
85
+
86
+ ┌─────────────┐
87
+ azify-logger│ (recebe logs)
88
+ └──────┬──────┘
89
+
90
+
91
+ ┌─────────────┐
92
+ │ OpenSearch │ (armazena em logs-{appName})
93
+ └──────┬──────┘
94
+
95
+
96
+ ┌─────────────┐
97
+ │ Grafana │ (visualiza - Organizations por app)
98
+ └─────────────┘
187
99
  ```
188
100
 
189
- ### Com Pino
190
-
191
- ```javascript
192
- const { streams } = require('azify-logger')
193
- const pino = require('pino')
101
+ ## 📈 Grafana - Visualização de Logs
194
102
 
195
- const logger = pino({ level: 'info' }, streams.createPinoStream({
196
- loggerUrl: 'http://localhost:3001/log',
197
- serviceName: 'nome-app'
198
- }))
103
+ O azify-logger usa Grafana para visualização dos logs com controle de acesso por aplicação.
199
104
 
200
- logger.info('Teste')
201
- ```
105
+ ### Funcionalidades
202
106
 
203
- ### Logger Direto (sem Bunyan/Pino)
107
+ - **📊 Explore**: Busca e visualização de logs em tempo real
108
+ - **📈 Dashboards**: Dashboards automáticos com métricas (Total de Requisições, Taxa de Sucesso, Erros, Tempo Médio de Resposta, etc.)
204
109
 
205
- ```javascript
206
- const { createAzifyLogger } = require('azify-logger')
110
+ ### Controle de Acesso Automático
207
111
 
208
- const logger = createAzifyLogger({
209
- serviceName: 'nome-app',
210
- loggerUrl: 'http://localhost:3001/log'
211
- })
112
+ Quando uma aplicação envia logs pela primeira vez, o sistema automaticamente:
212
113
 
213
- logger.info('Mensagem', { userId: '123' })
214
- logger.error('Erro', new Error('Falha'), { context: 'payment' })
215
- ```
114
+ 1. Cria uma **Organization** no Grafana com o nome da aplicação
115
+ 2. Cria um **Datasource** filtrado para o índice `logs-{appName}`
116
+ 3. ✅ Cria um **Dashboard** completo com métricas
117
+ 4. ✅ Isola completamente os logs da aplicação
216
118
 
217
- ---
218
-
219
- ## 🛠️ Setup do Serviço de Logging (Infra)
220
-
221
- Se você precisa subir a infraestrutura do azify-logger:
119
+ **Gerenciar acesso:**
120
+ - Acesse Grafana como Server Admin
121
+ - em **Administration** **Organizations**
122
+ - Adicione usuários à Organization da app
123
+ - **⚠️ Importante**: Use role **Editor** ou **Admin** para acesso ao Explorer (onde estão os logs). Role **Viewer** não tem acesso ao Explorer.
222
124
 
125
+ ## 🛠️ Setup Local
223
126
 
224
127
  ```bash
225
128
  ./start-docker.sh
@@ -230,12 +133,22 @@ Aguarde alguns minutos. Você verá:
230
133
  ```
231
134
  ✅ Tudo pronto!
232
135
  📊 OpenSearch: http://localhost:9200
233
- 🎨 OpenSearch Dashboards: http://localhost:5601
136
+ 📈 Grafana: http://localhost:3002
234
137
  📝 Logger API: http://localhost:3001/log
235
- 🔭 OTEL Collector: http://localhost:4318
236
138
  ```
237
139
 
238
- ### 2. Testar
140
+ ### Configuração de Ambiente
141
+
142
+ Copie os arquivos de exemplo e preencha com seus valores:
143
+
144
+ ```bash
145
+ cp env/app.env.example env/app.env
146
+ cp env/grafana.env.example env/grafana.env
147
+ ```
148
+
149
+ Edite `env/app.env` e `env/grafana.env` com suas configurações do Azure AD e outras variáveis.
150
+
151
+ ### Testar
239
152
 
240
153
  ```bash
241
154
  curl -X POST http://localhost:3001/log \
@@ -243,97 +156,30 @@ curl -X POST http://localhost:3001/log \
243
156
  -d '{
244
157
  "level": "info",
245
158
  "message": "Teste",
246
- "meta": {"service": {"name": "teste"}}
159
+ "meta": {"service": {"name": "minha-app"}}
247
160
  }'
248
161
  ```
249
162
 
250
- ### 3. Ver logs
163
+ Acesse `http://localhost:3002` e faça login com Azure AD.
251
164
 
252
- **OpenSearch Dashboards:**
253
- ```bash
254
- http://localhost:5601
255
- ```
165
+ ## 🚀 Deploy
256
166
 
257
- **Como usar:**
258
- Acesse: http://localhost:5601
259
-
260
- ## 📊 Estrutura dos Logs
261
-
262
- Cada log no OpenSearch contém:
263
-
264
- ```json
265
- {
266
- "timestamp": "2025-10-01T10:15:30.123Z",
267
- "level": "info",
268
- "message": "[REQUEST] POST /api/payment",
269
- "service": {
270
- "name": "nome-app",
271
- "version": "1.0.0"
272
- },
273
- "traceId": "abc123...",
274
- "spanId": "xyz789...",
275
- "method": "POST",
276
- "url": "/api/payment",
277
- "requestBody": { ... },
278
- "statusCode": 200,
279
- "responseTime": 123.45,
280
- "responseBody": { ... },
281
- "environment": "production",
282
- "hostname": "app-server-01"
283
- }
284
- ```
285
-
286
- ## 🔍 Como Funciona
167
+ ### Staging - Deploy Automático
287
168
 
288
- 1. **OpenTelemetry** cria um span para cada request HTTP
289
- 2. **Middleware** captura request e response com o **mesmo span ativo**
290
- 3. **Logger** envia ambos os logs com **traceId e spanId iguais**
291
- 4. **OpenSearch** indexa e permite buscar por traceId
292
-
293
- ---
294
-
295
- ## 📖 Arquitetura
296
-
297
- ```
298
- ┌─────────────┐
299
- │ aplicação │
300
- └──────┬──────┘
301
- │ HTTP POST
302
-
303
- ┌─────────────┐
304
- │ azify-logger│ (recebe logs)
305
- └──────┬──────┘
306
-
307
-
308
- ┌─────────────┐
309
- │ OpenSearch │ (armazena)
310
- └──────┬──────┘
311
-
312
-
313
- ┌─────────────┐
314
- │ Dashboards |
315
- | OpenSearch │ (visualiza)
316
- └─────────────┘
169
+ ```bash
170
+ git push origin master
317
171
  ```
318
172
 
319
- ## 📈 OpenSearch Dashboards
173
+ O deploy é feito automaticamente via GitHub Actions.
320
174
 
321
- O azify-logger usa OpenSearch Dashboards para visualização dos logs:
175
+ ### Production - Deploy Manual
322
176
 
323
- ### 🎯 Funcionalidades Disponíveis
177
+ 1. Acesse: **GitHub** → **Actions** → **Deploy**
178
+ 2. Clique em **Run workflow**
179
+ 3. Configure: **Environment:** `production`
180
+ 4. Clique em **Run workflow**
324
181
 
325
- 1. **📊 Discover** - Visualização e busca de logs
326
- - Filtros por serviço, app, traceId, statusCode
327
- - Busca em tempo real
328
- - Visualização detalhada de cada log
329
-
330
- 2. **📈 Dashboard** - "Application Health Dashboard"
331
- - **Taxa de Sucesso** - Percentual de requisições 2xx
332
- - **Latência Média** - Tempo médio de resposta
333
- - **Erros 4xx/5xx** - Contador de erros
334
- - **Requisições por Minuto** - Gráfico temporal
335
-
336
- ### ⚠️ Preservação de Dados
182
+ ## ⚠️ Preservação de Dados
337
183
 
338
184
  **IMPORTANTE:** Logs do OpenSearch são armazenados em volume Docker persistente.
339
185
 
@@ -341,10 +187,9 @@ O azify-logger usa OpenSearch Dashboards para visualização dos logs:
341
187
  ```bash
342
188
  docker-compose stop
343
189
  docker-compose restart
344
- pm2 stop azify-logger
345
190
  ```
346
191
 
347
192
  **Comandos destrutivos** (APAGAM logs):
348
193
  ```bash
349
194
  docker-compose down -v # ⚠️ APAGA VOLUMES!
350
- ```
195
+ ```
@@ -382,17 +382,8 @@ function createExpressLoggingMiddleware(options = {}) {
382
382
 
383
383
  const statusCode = res._actualStatusCode || res._statusCode || res.statusCode || 200
384
384
 
385
- let parsedResponseBody = serializedResponseBody
386
- try {
387
- if (typeof serializedResponseBody === 'string' && serializedResponseBody.trim().startsWith('{')) {
388
- parsedResponseBody = JSON.parse(serializedResponseBody)
389
- } else if (typeof serializedResponseBody === 'string' && serializedResponseBody.trim().startsWith('[')) {
390
- parsedResponseBody = JSON.parse(serializedResponseBody)
391
- }
392
- } catch (_) {}
393
-
394
385
  const responseMessage = serializedResponseBody && serializedResponseBody.length > 0
395
- ? `[RESPONSE] ${req.method} ${req.url} ${statusCode}`
386
+ ? `[RESPONSE] ${serializedResponseBody}`
396
387
  : `[RESPONSE] ${req.method} ${req.url} ${statusCode} ${duration}ms`
397
388
 
398
389
  const responseData = {
@@ -401,7 +392,7 @@ function createExpressLoggingMiddleware(options = {}) {
401
392
  statusCode: statusCode,
402
393
  responseTime: duration,
403
394
  responseHeaders: res.getHeaders ? res.getHeaders() : {},
404
- responseBody: parsedResponseBody // Use parsed object instead of string
395
+ responseBody: serializedResponseBody
405
396
  }
406
397
 
407
398
  try { res._azifyResponseLogged = true } catch (_) {}
@@ -282,20 +282,8 @@ function createRestifyLoggingMiddleware(options = {}) {
282
282
 
283
283
  const statusCode = res._actualStatusCode || res._statusCode || res.statusCode || 200
284
284
 
285
- // Try to parse serializedResponseBody back to object for cleaner display in Grafana
286
- let parsedResponseBody = serializedResponseBody
287
- try {
288
- if (typeof serializedResponseBody === 'string' && serializedResponseBody.trim().startsWith('{')) {
289
- parsedResponseBody = JSON.parse(serializedResponseBody)
290
- } else if (typeof serializedResponseBody === 'string' && serializedResponseBody.trim().startsWith('[')) {
291
- parsedResponseBody = JSON.parse(serializedResponseBody)
292
- }
293
- } catch (_) {
294
- // Keep as string if parsing fails
295
- }
296
-
297
285
  const responseMessage = serializedResponseBody && serializedResponseBody.length > 0
298
- ? `[RESPONSE] ${req.method} ${req.url} ${statusCode}`
286
+ ? `[RESPONSE] ${serializedResponseBody}`
299
287
  : `[RESPONSE] ${req.method} ${req.url} ${statusCode} ${duration}ms`
300
288
 
301
289
  const responseData = {
@@ -304,7 +292,7 @@ function createRestifyLoggingMiddleware(options = {}) {
304
292
  statusCode: statusCode,
305
293
  responseTime: duration,
306
294
  responseHeaders: res.getHeaders ? res.getHeaders() : {},
307
- responseBody: parsedResponseBody // Use parsed object instead of string
295
+ responseBody: serializedResponseBody
308
296
  }
309
297
 
310
298
  try { res._azifyResponseLogged = true } catch (_) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "Azify Logger Client - Centralized logging for OpenSearch",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -30,18 +30,30 @@
30
30
  "axios": "^1.6.0",
31
31
  "cors": "^2.8.5",
32
32
  "express": "^4.18.2",
33
+ "express-session": "^1.17.3",
34
+ "passport": "^0.6.0",
35
+ "passport-azure-ad": "^4.3.5",
36
+ "js-yaml": "^4.1.0",
33
37
  "require-in-the-middle": "^7.4.0",
34
- "uuid": "^9.0.1"
38
+ "uuid": "^9.0.1",
39
+ "@opentelemetry/api": "1.1.0",
40
+ "@opentelemetry/core": "1.1.0",
41
+ "@opentelemetry/resources": "1.1.0",
42
+ "@opentelemetry/semantic-conventions": "1.1.0",
43
+ "@opentelemetry/sdk-node": "0.39.1",
44
+ "@opentelemetry/exporter-trace-otlp-http": "0.39.1",
45
+ "@opentelemetry/auto-instrumentations-node": "0.39.1"
35
46
  },
36
- "optionalDependencies": {
37
- "@opentelemetry/api": "^1.9.0",
38
- "@opentelemetry/auto-instrumentations-node": "^0.40.0",
39
- "@opentelemetry/core": "^1.28.0",
40
- "@opentelemetry/exporter-trace-otlp-http": "^0.45.0",
41
- "@opentelemetry/resources": "^1.28.0",
42
- "@opentelemetry/sdk-node": "^0.45.0",
43
- "@opentelemetry/semantic-conventions": "^1.28.0"
47
+ "overrides": {
48
+ "@opentelemetry/api": "1.1.0",
49
+ "@opentelemetry/core": "1.1.0",
50
+ "@opentelemetry/resources": "1.1.0",
51
+ "@opentelemetry/semantic-conventions": "1.1.0",
52
+ "@opentelemetry/sdk-node": "0.39.1",
53
+ "@opentelemetry/exporter-trace-otlp-http": "0.39.1",
54
+ "@opentelemetry/auto-instrumentations-node": "0.39.1"
44
55
  },
56
+ "optionalDependencies": {},
45
57
  "engines": {
46
58
  "node": ">=12.0.0"
47
59
  },
package/register.js CHANGED
@@ -380,6 +380,155 @@ try {
380
380
  }
381
381
  } catch (_) {}
382
382
 
383
+ try {
384
+ if (typeof globalThis.fetch === 'function') {
385
+ const g = globalThis
386
+ if (!g.__azifyLoggerFetchPatched) {
387
+ g.__azifyLoggerFetchPatched = true
388
+
389
+ const { getRequestContext, runWithRequestContext } = require('./store')
390
+ const { randomUUID, randomBytes } = require('crypto')
391
+ const { performance } = require('perf_hooks')
392
+ const axios = require('axios')
393
+ const os = require('os')
394
+
395
+ const serviceName = process.env.APP_NAME || 'app'
396
+ const loggerUrl = process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log'
397
+ const environment = process.env.NODE_ENV || 'development'
398
+
399
+ async function sendLog(level, message, meta) {
400
+ const payload = {
401
+ level,
402
+ message,
403
+ meta: {
404
+ ...meta,
405
+ service: {
406
+ name: serviceName,
407
+ version: (meta && meta.service && meta.service.version) || '1.0.0'
408
+ },
409
+ environment,
410
+ timestamp: new Date().toISOString(),
411
+ hostname: os.hostname()
412
+ }
413
+ }
414
+
415
+ try {
416
+ await axios.post(loggerUrl, payload, { timeout: 5000 })
417
+ } catch (error) {
418
+ console.error('Erro ao enviar log:', error && error.message ? error.message : error)
419
+ }
420
+ }
421
+
422
+ const originalFetch = globalThis.fetch.bind(globalThis)
423
+
424
+ function sanitizeHeaders(headers) {
425
+ const SENSITIVE_HEADER_KEYS = new Set([
426
+ 'authorization',
427
+ 'cookie',
428
+ 'set-cookie',
429
+ 'x-api-key',
430
+ 'x-auth-token',
431
+ 'x-access-token',
432
+ 'proxy-authorization'
433
+ ])
434
+
435
+ const sanitized = {}
436
+ headers.forEach((value, key) => {
437
+ const lower = key.toLowerCase()
438
+ if (SENSITIVE_HEADER_KEYS.has(lower)) {
439
+ sanitized[lower] = '***'
440
+ } else {
441
+ sanitized[lower] = value
442
+ }
443
+ })
444
+ return sanitized
445
+ }
446
+
447
+ function ensureRequest(input, init) {
448
+ if (typeof Request !== 'undefined' && input instanceof Request) {
449
+ return init ? new Request(input, init) : input
450
+ }
451
+ return new Request(input, init)
452
+ }
453
+
454
+ globalThis.fetch = async function patchedFetch(input, init) {
455
+ const request = ensureRequest(input, init)
456
+ const method = request.method.toUpperCase()
457
+ const url = request.url
458
+
459
+ const ctx = getRequestContext()
460
+ const traceId = (ctx && ctx.traceId) || randomUUID()
461
+ const parentSpanId = (ctx && ctx.spanId) || null
462
+ const requestId = (ctx && ctx.requestId) || randomUUID()
463
+ const spanId = randomBytes(8).toString('hex')
464
+
465
+ request.headers.set('x-trace-id', traceId)
466
+ request.headers.set('x-span-id', spanId)
467
+ request.headers.set('x-parent-span-id', parentSpanId || '')
468
+ request.headers.set('x-request-id', requestId)
469
+
470
+ const requestMeta = {
471
+ traceId,
472
+ spanId,
473
+ parentSpanId,
474
+ requestId,
475
+ method,
476
+ url,
477
+ headers: sanitizeHeaders(request.headers)
478
+ }
479
+
480
+ const start = performance.now()
481
+ void sendLog('info', `[REQUEST] ${method} ${url}`, requestMeta)
482
+
483
+ const childCtx = {
484
+ traceId,
485
+ spanId,
486
+ parentSpanId,
487
+ requestId
488
+ }
489
+
490
+ try {
491
+ const response = await runWithRequestContext(childCtx, function() {
492
+ return originalFetch(request)
493
+ })
494
+
495
+ const duration = Number((performance.now() - start).toFixed(2))
496
+ const responseMeta = {
497
+ ...requestMeta,
498
+ statusCode: response.status,
499
+ responseTimeMs: duration
500
+ }
501
+
502
+ const message = `[RESPONSE] ${method} ${url} ${response.status} ${duration}ms`
503
+ if (response.status >= 500) {
504
+ void sendLog('error', message, responseMeta)
505
+ } else if (response.status >= 400) {
506
+ void sendLog('warn', message, responseMeta)
507
+ } else {
508
+ void sendLog('info', message, responseMeta)
509
+ }
510
+
511
+ return response
512
+ } catch (error) {
513
+ const duration = Number((performance.now() - start).toFixed(2))
514
+ const errorMeta = {
515
+ ...requestMeta,
516
+ responseTimeMs: duration
517
+ }
518
+ const err = error instanceof Error ? error : new Error(String(error))
519
+ errorMeta.error = {
520
+ name: err.name,
521
+ message: err.message,
522
+ stack: err.stack
523
+ }
524
+ void sendLog('error', `[ERROR] ${method} ${url}`, errorMeta)
525
+ throw error
526
+ }
527
+ }
528
+ }
529
+ }
530
+ } catch (_) {}
531
+
383
532
  try {
384
533
  const Bull = require('bull')
385
534
  const { runWithRequestContext } = require('./store')