azify-logger 1.0.26 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -22,6 +22,11 @@ try {
22
22
 
23
23
  const app = express()
24
24
 
25
+ const allowedExternalIPs = (process.env.ALLOWED_SOURCE_IPS || '')
26
+ .split(',')
27
+ .map(ip => ip.trim())
28
+ .filter(Boolean)
29
+
25
30
  app.set('trust proxy', 1)
26
31
 
27
32
  app.use(express.json({ limit: '10mb' }))
@@ -77,7 +82,11 @@ function validateNetworkAccess(req, res, next) {
77
82
  req.connection.remoteAddress ||
78
83
  req.socket.remoteAddress
79
84
 
80
- if (!IS_LOCAL && !isPrivateOrLocalhost(clientIP)) {
85
+ if (
86
+ !IS_LOCAL &&
87
+ !isPrivateOrLocalhost(clientIP) &&
88
+ !(allowedExternalIPs.length > 0 && allowedExternalIPs.includes(clientIP))
89
+ ) {
81
90
  return res.status(403).json({
82
91
  success: false,
83
92
  message: 'Forbidden. Access denied - endpoint only accessible from same server (localhost/private IPs).',
@@ -111,7 +120,7 @@ async function ensureIndexTemplate() {
111
120
  mappings: {
112
121
  properties: {
113
122
  '@timestamp': { type: 'date' },
114
- level: { type: 'keyword' },
123
+ level: { type: 'text', fields: { keyword: { type: 'keyword' } } },
115
124
  message: { type: 'text' },
116
125
  service: {
117
126
  properties: {
@@ -162,21 +171,40 @@ function escapeForSQLite(str) {
162
171
  return str.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r')
163
172
  }
164
173
 
174
+ function runSqlite(sql) {
175
+ const dbPath = process.env.GRAFANA_SQLITE_PATH || '/var/lib/grafana/grafana.db'
176
+ const grafanaContainer = process.env.GRAFANA_DOCKER_CONTAINER || 'azify-grafana'
177
+ const { execSync } = require('child_process')
178
+ const localDbExists = fs.existsSync(dbPath)
179
+ const escapedForLocal = sql.replace(/"/g, '""')
180
+ const escapedForDocker = sql.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')
181
+
182
+ const command = localDbExists
183
+ ? `sqlite3 ${dbPath} "${escapedForLocal}"`
184
+ : `docker exec ${grafanaContainer} sh -c "sqlite3 ${dbPath} \\"${escapedForDocker}\\""`;
185
+
186
+ try {
187
+ if (!localDbExists) {
188
+ console.log(`[runSqlite] Executando comando via docker exec no container ${grafanaContainer}`)
189
+ }
190
+ return execSync(command, { encoding: 'utf8' }).trim()
191
+ } catch (error) {
192
+ const stderr = error.stderr?.toString()?.trim()
193
+ throw new Error(stderr || error.message)
194
+ }
195
+ }
196
+
165
197
  async function createOrgViaSQLite(appName) {
166
198
  const dbPath = '/var/lib/grafana/grafana.db'
167
199
 
168
200
  try {
169
201
  console.log(`[createOrgViaSQLite] Verificando database em ${dbPath}...`)
170
202
  if (!fs.existsSync(dbPath)) {
171
- console.error(`[createOrgViaSQLite] Database não encontrado em ${dbPath}`)
172
- throw new Error(`Database não encontrado em ${dbPath}`)
203
+ console.log(`[createOrgViaSQLite] Banco não acessível localmente; tentando via docker exec`)
173
204
  }
174
-
175
- const { execSync } = require('child_process')
176
-
205
+
177
206
  console.log(`[createOrgViaSQLite] Verificando se Organization '${appName}' já existe...`)
178
- const checkCmd = `sqlite3 ${dbPath} "SELECT id FROM org WHERE name='${appName}';" 2>/dev/null || echo ""`
179
- const existingId = execSync(checkCmd, { encoding: 'utf8' }).trim()
207
+ const existingId = runSqlite(`SELECT id FROM org WHERE name='${appName}';`)
180
208
 
181
209
  if (existingId && existingId !== '') {
182
210
  console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' já existe (ID: ${existingId})`)
@@ -184,8 +212,7 @@ async function createOrgViaSQLite(appName) {
184
212
  }
185
213
 
186
214
  console.log(`[createOrgViaSQLite] Criando Organization '${appName}'...`)
187
- const createCmd = `sqlite3 ${dbPath} "INSERT INTO org (name, version, created, updated) VALUES ('${appName}', 1, datetime('now'), datetime('now')); SELECT id FROM org WHERE name='${appName}';" 2>/dev/null`
188
- const newId = execSync(createCmd, { encoding: 'utf8' }).trim()
215
+ const newId = runSqlite(`INSERT INTO org (name, version, created, updated) VALUES ('${appName}', 1, datetime('now'), datetime('now')); SELECT id FROM org WHERE name='${appName}';`)
189
216
 
190
217
  if (newId && newId !== '') {
191
218
  console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' criada com sucesso (ID: ${newId})`)
@@ -201,10 +228,14 @@ async function createOrgViaSQLite(appName) {
201
228
  }
202
229
 
203
230
  async function setupGrafanaForApp(appName) {
204
- const grafanaUrl = process.env.GRAFANA_URL || 'http://azify-grafana:3000'
231
+ const runningInDocker = fs.existsSync('/.dockerenv')
232
+ const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
233
+ const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
205
234
  const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
206
235
  const grafanaAdminPassword = process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
207
- const opensearchUrl = process.env.OPENSEARCH_URL || 'http://azify-opensearch:9200'
236
+ const defaultOpensearchUrl = runningInDocker ? 'http://azify-opensearch:9200' : 'http://127.0.0.1:9200'
237
+ const opensearchUrl = process.env.OPENSEARCH_URL || defaultOpensearchUrl
238
+ const grafanaOpensearchUrl = process.env.GRAFANA_OPENSEARCH_URL || (runningInDocker ? 'http://azify-opensearch:9200' : opensearchUrl)
208
239
 
209
240
  if (!setupGrafanaForApp._cache) {
210
241
  setupGrafanaForApp._cache = new Set()
@@ -252,11 +283,11 @@ async function setupGrafanaForApp(appName) {
252
283
  org = await createOrgViaSQLite(appName)
253
284
  if (!org) {
254
285
  console.error(`[setupGrafana] ❌ Não foi possível criar Organization via SQLite para ${appName}`)
255
- return
286
+ throw new Error(`SQLite não criou organization para ${appName}`)
256
287
  }
257
288
  } catch (sqliteError) {
258
289
  console.error(`[setupGrafana] ❌ Erro ao criar Organization via SQLite: ${sqliteError.message}`)
259
- return
290
+ throw sqliteError
260
291
  }
261
292
  }
262
293
  } else {
@@ -266,18 +297,18 @@ async function setupGrafanaForApp(appName) {
266
297
  org = await createOrgViaSQLite(appName)
267
298
  if (!org) {
268
299
  console.error(`[setupGrafana] ❌ Não foi possível criar Organization via SQLite para ${appName}`)
269
- return
300
+ throw new Error(`SQLite não criou organization para ${appName}`)
270
301
  }
271
302
  } catch (sqliteError) {
272
303
  console.error(`[setupGrafana] ❌ Erro ao criar Organization via SQLite: ${sqliteError.message}`)
273
- return
304
+ throw sqliteError
274
305
  }
275
306
  }
276
307
  }
277
308
 
278
309
  if (!org) {
279
310
  console.error(`[setupGrafana] ❌ Organization não encontrada/criada para ${appName}`)
280
- return
311
+ throw new Error(`Organization não encontrada ou criada para ${appName}`)
281
312
  }
282
313
 
283
314
  console.log(`[setupGrafana] ✅ Organization encontrada/criada: ${org.name} (ID: ${org.id})`)
@@ -330,20 +361,21 @@ async function setupGrafanaForApp(appName) {
330
361
  }
331
362
  } catch (userError) {
332
363
  const dbUserId = await new Promise((resolve) => {
333
- const { execSync } = require('child_process')
334
364
  try {
335
- const result = execSync(`sqlite3 /var/lib/grafana/grafana.db "SELECT id FROM user WHERE email='${trimmedEmail}';"`, { encoding: 'utf8' }).trim()
365
+ const result = runSqlite(`SELECT id FROM user WHERE email='${trimmedEmail}';`)
336
366
  resolve(result || null)
337
- } catch {
367
+ } catch (err) {
368
+ console.error(`[setupGrafana] ❌ Falha ao buscar usuário ${trimmedEmail} via SQLite: ${err.message}`)
338
369
  resolve(null)
339
370
  }
340
371
  })
341
372
 
342
373
  if (dbUserId) {
343
- const { execSync } = require('child_process')
344
374
  try {
345
- execSync(`sqlite3 /var/lib/grafana/grafana.db "INSERT OR IGNORE INTO org_user (org_id, user_id, role, created, updated) VALUES (${org.id}, ${dbUserId}, 'Admin', datetime('now'), datetime('now')); UPDATE org_user SET role='Admin' WHERE org_id=${org.id} AND user_id=${dbUserId};"`, { encoding: 'utf8' })
346
- } catch {
375
+ runSqlite(`INSERT OR IGNORE INTO org_user (org_id, user_id, role, created, updated) VALUES (${org.id}, ${dbUserId}, 'Admin', datetime('now'), datetime('now')); UPDATE org_user SET role='Admin' WHERE org_id=${org.id} AND user_id=${dbUserId};`)
376
+ } catch (fallbackError) {
377
+ console.error(`[setupGrafana] ❌ Falha ao promover ${trimmedEmail} via SQLite: ${fallbackError.message}`)
378
+ throw fallbackError
347
379
  }
348
380
  }
349
381
  }
@@ -357,7 +389,7 @@ async function setupGrafanaForApp(appName) {
357
389
  name: `OpenSearch-${appName}`,
358
390
  type: 'grafana-opensearch-datasource',
359
391
  access: 'proxy',
360
- url: opensearchUrl,
392
+ url: grafanaOpensearchUrl,
361
393
  uid: datasourceUid,
362
394
  isDefault: true,
363
395
  jsonData: {
@@ -367,7 +399,7 @@ async function setupGrafanaForApp(appName) {
367
399
  esVersion: '2.11.1',
368
400
  version: '2.11.1',
369
401
  logMessageField: 'message',
370
- logLevelField: 'level',
402
+ logLevelField: 'level.keyword',
371
403
  maxConcurrentShardRequests: 5,
372
404
  includeFrozen: false,
373
405
  xpack: false,
@@ -416,25 +448,16 @@ async function setupGrafanaForApp(appName) {
416
448
  if (createError.response?.status === 401) {
417
449
  console.log(`[setupGrafana] 🔄 Tentando criar datasource via SQLite...`)
418
450
  try {
419
- const { execSync } = require('child_process')
420
451
  const dsJson = JSON.stringify(datasourceConfig.jsonData)
421
452
  const dsName = escapeForSQLite(datasourceConfig.name)
422
- const opensearchUrlEscaped = escapeForSQLite(opensearchUrl)
453
+ const opensearchUrlEscaped = escapeForSQLite(grafanaOpensearchUrl)
423
454
  const dsUidEscaped = escapeForSQLite(datasourceUid)
424
455
  const jsonEscaped = dsJson.replace(/'/g, "''")
425
- const tempFile = `/tmp/ds_${org.id}_${Date.now()}.sql`
426
- const sql = `INSERT INTO data_source (org_id, version, type, name, access, url, is_default, json_data, uid, created, updated, basic_auth, with_credentials) VALUES (${org.id}, 1, 'grafana-opensearch-datasource', '${dsName}', 'proxy', '${opensearchUrlEscaped}', ${datasourceConfig.isDefault ? 1 : 0}, '${jsonEscaped}', '${dsUidEscaped}', datetime('now'), datetime('now'), 0, 0); SELECT last_insert_rowid();`
427
- fs.writeFileSync(tempFile, sql)
428
- const cmd = `sqlite3 /var/lib/grafana/grafana.db < ${tempFile}`
429
- const dsId = execSync(cmd, { encoding: 'utf8' }).trim()
430
- fs.unlinkSync(tempFile)
456
+ runSqlite(`INSERT INTO data_source (org_id, version, type, name, access, url, is_default, json_data, uid, created, updated, basic_auth, with_credentials) VALUES (${org.id}, 1, 'grafana-opensearch-datasource', '${dsName}', 'proxy', '${opensearchUrlEscaped}', ${datasourceConfig.isDefault ? 1 : 0}, '${jsonEscaped}', '${dsUidEscaped}', datetime('now'), datetime('now'), 0, 0);`)
457
+ const dsId = runSqlite('SELECT last_insert_rowid();')
431
458
  console.log(`[setupGrafana] ✅ Datasource criado via SQLite: ${datasourceUid} (ID: ${dsId})`)
432
459
  } catch (sqliteError) {
433
460
  console.error(`[setupGrafana] ❌ Erro ao criar datasource via SQLite: ${sqliteError.message}`)
434
- try {
435
- const tempFiles = fs.readdirSync('/tmp').filter(f => f.startsWith('ds_'))
436
- tempFiles.forEach(f => { try { fs.unlinkSync(`/tmp/${f}`) } catch {} })
437
- } catch {}
438
461
  }
439
462
  }
440
463
  }
@@ -443,25 +466,16 @@ async function setupGrafanaForApp(appName) {
443
466
  if (error.response?.status === 401) {
444
467
  console.log(`[setupGrafana] 🔄 Tentando criar datasource via SQLite (erro 401 na verificação)...`)
445
468
  try {
446
- const { execSync } = require('child_process')
447
469
  const dsJson = JSON.stringify(datasourceConfig.jsonData)
448
470
  const dsName = escapeForSQLite(datasourceConfig.name)
449
- const opensearchUrlEscaped = escapeForSQLite(opensearchUrl)
471
+ const opensearchUrlEscaped = escapeForSQLite(grafanaOpensearchUrl)
450
472
  const dsUidEscaped = escapeForSQLite(datasourceUid)
451
473
  const jsonEscaped = dsJson.replace(/'/g, "''")
452
- const tempFile = `/tmp/ds2_${org.id}_${Date.now()}.sql`
453
- const sql = `INSERT INTO data_source (org_id, version, type, name, access, url, is_default, json_data, uid, created, updated, basic_auth, with_credentials) VALUES (${org.id}, 1, 'grafana-opensearch-datasource', '${dsName}', 'proxy', '${opensearchUrlEscaped}', ${datasourceConfig.isDefault ? 1 : 0}, '${jsonEscaped}', '${dsUidEscaped}', datetime('now'), datetime('now'), 0, 0); SELECT last_insert_rowid();`
454
- fs.writeFileSync(tempFile, sql)
455
- const cmd = `sqlite3 /var/lib/grafana/grafana.db < ${tempFile}`
456
- const dsId = execSync(cmd, { encoding: 'utf8' }).trim()
457
- fs.unlinkSync(tempFile)
474
+ runSqlite(`INSERT INTO data_source (org_id, version, type, name, access, url, is_default, json_data, uid, created, updated, basic_auth, with_credentials) VALUES (${org.id}, 1, 'grafana-opensearch-datasource', '${dsName}', 'proxy', '${opensearchUrlEscaped}', ${datasourceConfig.isDefault ? 1 : 0}, '${jsonEscaped}', '${dsUidEscaped}', datetime('now'), datetime('now'), 0, 0);`)
475
+ const dsId = runSqlite('SELECT last_insert_rowid();')
458
476
  console.log(`[setupGrafana] ✅ Datasource criado via SQLite: ${datasourceUid} (ID: ${dsId})`)
459
477
  } catch (sqliteError) {
460
478
  console.error(`[setupGrafana] ❌ Erro ao criar datasource via SQLite: ${sqliteError.message}`)
461
- try {
462
- const tempFiles = fs.readdirSync('/tmp').filter(f => f.startsWith('ds'))
463
- tempFiles.forEach(f => { try { fs.unlinkSync(`/tmp/${f}`) } catch {} })
464
- } catch {}
465
479
  }
466
480
  }
467
481
  }
@@ -491,7 +505,7 @@ async function setupGrafanaForApp(appName) {
491
505
  datasource: { uid: datasourceUid, type: 'grafana-opensearch-datasource' },
492
506
  targets: [{
493
507
  refId: 'A',
494
- query: `message:"[REQUEST]" AND NOT message:"[RESPONSE]" AND ${indexFilter}`,
508
+ query: `message:"[RESPONSE]" AND ${indexFilter}`,
495
509
  bucketAggs: [{
496
510
  id: '2',
497
511
  type: 'date_histogram',
@@ -711,7 +725,6 @@ async function setupGrafanaForApp(appName) {
711
725
  if (error.response?.status === 401 && typeof dashboardConfig !== 'undefined' && dashboardConfig && dashboardConfig.dashboard) {
712
726
  console.log(`[setupGrafana] 🔄 Tentando criar dashboard via SQLite...`)
713
727
  try {
714
- const { execSync } = require('child_process')
715
728
  const dashboardUid = `dashboard-${appName.toLowerCase()}`
716
729
  const dashboardTitle = `Application Health - ${appName}`
717
730
  const dashboardJson = JSON.stringify(dashboardConfig.dashboard)
@@ -720,19 +733,11 @@ async function setupGrafanaForApp(appName) {
720
733
  const slug = dashboardTitle.toLowerCase().replace(/[^a-z0-9-]/g, '-')
721
734
  const slugEscaped = escapeForSQLite(slug)
722
735
  const uidEscaped = escapeForSQLite(dashboardUid)
723
- const tempFile = `/tmp/dash_${org.id}_${Date.now()}.sql`
724
- const sql = `INSERT INTO dashboard (org_id, version, slug, title, data, created, updated, uid, is_folder, folder_id) VALUES (${org.id}, 0, '${slugEscaped}', '${escapedTitle}', '${escapedJson}', datetime('now'), datetime('now'), '${uidEscaped}', 0, 0); SELECT last_insert_rowid();`
725
- fs.writeFileSync(tempFile, sql)
726
- const cmd = `sqlite3 /var/lib/grafana/grafana.db < ${tempFile}`
727
- const dashboardId = execSync(cmd, { encoding: 'utf8' }).trim()
728
- fs.unlinkSync(tempFile)
736
+ runSqlite(`INSERT INTO dashboard (org_id, version, slug, title, data, created, updated, uid, is_folder, folder_id) VALUES (${org.id}, 0, '${slugEscaped}', '${escapedTitle}', '${escapedJson}', datetime('now'), datetime('now'), '${uidEscaped}', 0, 0);`)
737
+ const dashboardId = runSqlite('SELECT last_insert_rowid();')
729
738
  console.log(`[setupGrafana] ✅ Dashboard criado via SQLite: ${dashboardTitle} (ID: ${dashboardId})`)
730
739
  } catch (sqliteError) {
731
740
  console.error(`[setupGrafana] ❌ Erro ao criar dashboard via SQLite: ${sqliteError.message}`)
732
- try {
733
- const tempFiles = fs.readdirSync('/tmp').filter(f => f.startsWith('dash_'))
734
- tempFiles.forEach(f => { try { fs.unlinkSync(`/tmp/${f}`) } catch {} })
735
- } catch {}
736
741
  }
737
742
  } else {
738
743
  console.error(`[setupGrafana] ⚠️ Não foi possível criar dashboard via SQLite: dashboardConfig não disponível`)
@@ -932,7 +937,7 @@ async function handleLog(req, res) {
932
937
  version: (meta && meta.service && meta.service.version) || '1.0.0'
933
938
  },
934
939
  appName: (meta && meta.appName) || (meta && meta.service && meta.service.name) || undefined,
935
- environment: (meta && meta.environment) || process.env.NODE_ENV || 'development',
940
+ environment: (meta && meta.environment) || process.env.NODE_ENV,
936
941
  hostname: (meta && meta.hostname) || os.hostname(),
937
942
  traceId: traceContext.traceId,
938
943
  spanId: traceContext.spanId,
@@ -952,22 +957,91 @@ async function handleLog(req, res) {
952
957
  }
953
958
  }
954
959
 
955
- if (key === 'responseBody') {
956
- if (typeof value === 'object' && value !== null) {
957
- try {
958
- value = JSON.stringify(value)
959
- } catch (e) {
960
- return
960
+ const truncateBody = (bodyValue, forResponse = false) => {
961
+ if (forResponse) {
962
+ if (typeof bodyValue === 'string') {
963
+ return bodyValue
964
+ } else if (Buffer.isBuffer(bodyValue)) {
965
+ return bodyValue.toString('utf8')
966
+ } else if (typeof bodyValue === 'object' && bodyValue !== null) {
967
+ try {
968
+ return JSON.stringify(bodyValue)
969
+ } catch (_) {
970
+ return String(bodyValue)
971
+ }
961
972
  }
962
- } else if (typeof value === 'string' && value.length > 10000) {
963
- value = value.substring(0, 10000) + '... [truncated]'
973
+ return String(bodyValue)
964
974
  }
965
- if (typeof value === 'object' && value !== null) {
966
- return
975
+
976
+ if (typeof bodyValue === 'string') {
977
+ if (bodyValue.trim().startsWith('{') || bodyValue.trim().startsWith('[')) {
978
+ try {
979
+ let parsed = JSON.parse(bodyValue)
980
+ if (typeof parsed === 'object') {
981
+ return parsed
982
+ }
983
+ } catch (_) { }
984
+ }
985
+ return bodyValue
986
+ } else if (typeof bodyValue === 'object' && bodyValue !== null) {
987
+ return bodyValue
988
+ } else if (Buffer.isBuffer(bodyValue)) {
989
+ try {
990
+ let str = bodyValue.toString('utf8')
991
+ if (str.trim().startsWith('{') || str.trim().startsWith('[')) {
992
+ try {
993
+ return JSON.parse(str)
994
+ } catch (_) {
995
+ return str
996
+ }
997
+ }
998
+ return str
999
+ } catch (_) {
1000
+ return '[Unable to serialize Buffer]'
1001
+ }
967
1002
  }
1003
+ return bodyValue
968
1004
  }
969
1005
 
970
- logEntry[key] = value
1006
+ if (key === 'request' && value && typeof value === 'object' && value.body !== undefined) {
1007
+ const processedBody = truncateBody(value.body, false)
1008
+ if (typeof processedBody === 'string' && processedBody.length > 5000) {
1009
+ logEntry.requestBody = processedBody
1010
+ delete value.body
1011
+ } else {
1012
+ value.body = processedBody
1013
+ }
1014
+ } else if (key === 'response' && value && typeof value === 'object') {
1015
+ if (value.statusCode != null) {
1016
+ logEntry.statusCode = value.statusCode
1017
+ }
1018
+ if (value.durationMs != null) {
1019
+ logEntry.responseTime = value.durationMs
1020
+ }
1021
+ if (value.body !== undefined) {
1022
+ const processedBody = truncateBody(value.body, true)
1023
+ let bodyString
1024
+ if (typeof processedBody === 'string') {
1025
+ bodyString = processedBody
1026
+ } else {
1027
+ bodyString = JSON.stringify(processedBody)
1028
+ }
1029
+ logEntry.responseBody = bodyString
1030
+ const { body, ...responseWithoutBody } = value
1031
+ logEntry[key] = responseWithoutBody
1032
+ } else {
1033
+ logEntry[key] = value
1034
+ }
1035
+ } else if (key === 'responseBody' || key === 'requestBody') {
1036
+ if (key === 'responseBody') {
1037
+ value = truncateBody(value, true)
1038
+ } else {
1039
+ value = truncateBody(value, false)
1040
+ }
1041
+ logEntry[key] = value
1042
+ } else {
1043
+ logEntry[key] = value
1044
+ }
971
1045
  }
972
1046
  })
973
1047
  }
@@ -1004,6 +1078,27 @@ async function handleLog(req, res) {
1004
1078
 
1005
1079
  res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
1006
1080
  } catch (error) {
1081
+ const status = error?.response?.status
1082
+ const errorMsg = error?.response?.data?.error?.reason ||
1083
+ error?.response?.data?.message ||
1084
+ error?.message ||
1085
+ 'Erro desconhecido'
1086
+
1087
+ console.error('❌ Falha ao enviar log para OpenSearch', {
1088
+ status,
1089
+ message: errorMsg
1090
+ })
1091
+
1092
+ if (error?.response?.data) {
1093
+ try {
1094
+ console.error('📦 Resposta OpenSearch:', JSON.stringify(error.response.data).substring(0, 2000))
1095
+ } catch (_) {
1096
+ console.error('📦 Resposta OpenSearch (raw):', error.response.data)
1097
+ }
1098
+ } else if (error?.code) {
1099
+ console.error('⚙️ Código de erro:', error.code)
1100
+ }
1101
+
1007
1102
  res.status(500).json({ success: false, message: 'Erro ao enviar log para OpenSearch' })
1008
1103
  }
1009
1104
  }
package/store.js CHANGED
@@ -2,8 +2,14 @@ const { AsyncLocalStorage } = require('async_hooks')
2
2
 
3
3
  const als = new AsyncLocalStorage()
4
4
 
5
- function generateId(bytes = 16) {
6
- return require('crypto').randomBytes(bytes).toString('hex')
5
+ function fastGenerateId(length = 16) {
6
+ const chars = '0123456789abcdef'
7
+ let result = ''
8
+ const totalChars = length * 2
9
+ for (let i = 0; i < totalChars; i++) {
10
+ result += chars[Math.floor(Math.random() * chars.length)]
11
+ }
12
+ return result
7
13
  }
8
14
 
9
15
  function toTraceId(hex32) {
@@ -12,8 +18,8 @@ function toTraceId(hex32) {
12
18
  }
13
19
 
14
20
  function startRequestContext(initial = {}) {
15
- const traceHex = initial.traceHex || generateId(16)
16
- const spanHex = initial.spanHex || generateId(8)
21
+ const traceHex = initial.traceHex || fastGenerateId(16)
22
+ const spanHex = initial.spanHex || fastGenerateId(8)
17
23
  const ctx = {
18
24
  traceId: toTraceId(traceHex),
19
25
  spanId: spanHex,
@@ -1,9 +1,35 @@
1
1
  import { Writable } from 'stream';
2
2
 
3
+ export interface HttpTransportOptions {
4
+ timeout?: number;
5
+ maxQueueSize?: number;
6
+ batchSize?: number;
7
+ flushInterval?: number;
8
+ maxSockets?: number;
9
+ failureThreshold?: number;
10
+ failureCooldown?: number;
11
+ redisUrl?: string;
12
+ redisQueueKey?: string;
13
+ redisMaxQueueLen?: number;
14
+ redisWorker?: boolean;
15
+ }
16
+
3
17
  export interface BunyanStreamOptions {
4
18
  loggerUrl?: string;
5
19
  serviceName?: string;
6
20
  environment?: string;
21
+ transportOptions?: HttpTransportOptions;
22
+ timeout?: number;
23
+ maxQueueSize?: number;
24
+ batchSize?: number;
25
+ flushInterval?: number;
26
+ maxSockets?: number;
27
+ failureThreshold?: number;
28
+ failureCooldown?: number;
29
+ redisUrl?: string;
30
+ redisQueueKey?: string;
31
+ redisMaxQueueLen?: number;
32
+ redisWorker?: boolean;
7
33
  }
8
34
 
9
35
  /**
package/streams/bunyan.js CHANGED
@@ -1,12 +1,12 @@
1
- const { AsyncLocalStorage } = require('async_hooks')
2
- const axios = require('axios')
3
-
4
- const globalContext = new Map()
1
+ const { createHttpLoggerTransport } = require('./httpQueue')
2
+ const { shouldSample, markSource } = require('../sampling')
5
3
 
6
4
  function createBunyanStream(options = {}) {
7
- const loggerUrl = options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001'
8
- const serviceName = options.serviceName || process.env.APP_NAME || 'app'
9
- const environment = options.environment || process.env.NODE_ENV || 'development'
5
+ const loggerUrl = options.loggerUrl || process.env.AZIFY_LOGGER_URL
6
+ const serviceName = options.serviceName || process.env.APP_NAME
7
+ const environment = options.environment || process.env.NODE_ENV
8
+
9
+ const transport = createHttpLoggerTransport(loggerUrl, extractTransportOptions(options))
10
10
 
11
11
  return {
12
12
  write(record) {
@@ -52,11 +52,42 @@ function createBunyanStream(options = {}) {
52
52
  ...(spanId && { spanId })
53
53
  }
54
54
 
55
+ markSource(meta, 'logger')
56
+
57
+ if (!shouldSample(level, 'logger')) {
58
+ return
59
+ }
60
+
55
61
  const payload = { level, message: record.msg || record.message || 'log', meta }
56
62
 
57
- axios.post(`${loggerUrl}`, payload, { headers, timeout: 10000 }).catch(() => {})
63
+ transport.enqueue(payload, headers)
58
64
  }
59
65
  }
60
66
  }
61
67
 
62
68
  module.exports = createBunyanStream
69
+
70
+ function extractTransportOptions (options) {
71
+ const {
72
+ transportOptions = {},
73
+ timeout,
74
+ maxQueueSize,
75
+ batchSize,
76
+ flushInterval,
77
+ maxSockets,
78
+ failureThreshold,
79
+ failureCooldown
80
+ } = options
81
+
82
+ const overrides = { ...transportOptions }
83
+
84
+ if (timeout != null) overrides.timeout = timeout
85
+ if (maxQueueSize != null) overrides.maxQueueSize = maxQueueSize
86
+ if (batchSize != null) overrides.batchSize = batchSize
87
+ if (flushInterval != null) overrides.flushInterval = flushInterval
88
+ if (maxSockets != null) overrides.maxSockets = maxSockets
89
+ if (failureThreshold != null) overrides.failureThreshold = failureThreshold
90
+ if (failureCooldown != null) overrides.failureCooldown = failureCooldown
91
+
92
+ return overrides
93
+ }