azify-logger 1.0.43 → 1.0.45

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
@@ -1,11 +1,16 @@
1
- require('dotenv').config({ path: './env/app.env' })
1
+ const path = require('path')
2
+ const http = require('http')
3
+ require('dotenv').config({ path: path.resolve(__dirname, 'env', 'app.env') })
4
+ console.log('[startup] env loaded')
2
5
  require('./register-otel.js')
6
+ console.log('[startup] register-otel done')
3
7
 
4
8
  const axios = require('axios')
5
9
  const express = require('express')
6
10
  const cors = require('cors')
7
11
  const os = require('os')
8
12
  const fs = require('fs')
13
+ const isLocalEnv = process.env.NODE_ENV === 'development'
9
14
  let trace, context, propagation, W3CTraceContextPropagator
10
15
  try {
11
16
  const otelApi = require('@opentelemetry/api')
@@ -30,6 +35,22 @@ const allowedExternalIPs = (process.env.ALLOWED_SOURCE_IPS || '')
30
35
 
31
36
  app.set('trust proxy', 1)
32
37
 
38
+ app.use((req, res, next) => {
39
+ if (req.path === '/health') {
40
+ res.status(200).set('Content-Type', 'application/json').end(JSON.stringify({ status: 'ok', service: 'azify-logger' }))
41
+ return
42
+ }
43
+ next()
44
+ })
45
+
46
+ let heartbeatCount = 0
47
+ const heartbeatInterval = setInterval(() => {
48
+ heartbeatCount++
49
+ if (heartbeatCount <= 24) console.log('[heartbeat]', heartbeatCount, 'uptime', Math.floor(process.uptime()), 's')
50
+ if (heartbeatCount >= 24) try { clearInterval(heartbeatInterval) } catch (_) {}
51
+ }, 5000)
52
+ if (typeof heartbeatInterval.unref === 'function') heartbeatInterval.unref()
53
+
33
54
  app.use(express.json({ limit: '10mb' }))
34
55
  app.use(express.urlencoded({ extended: true, limit: '10mb' }))
35
56
  app.use(cors())
@@ -38,12 +59,22 @@ const authEnabled = process.env.AZURE_AD_AUTH_ENABLED === 'true'
38
59
  let ensureAuthenticated = (req, res, next) => next()
39
60
  let ensureAdmin = (req, res, next) => res.status(403).send('Forbidden')
40
61
  if (authEnabled) {
41
- const { setupAuth, setupAuthRoutes, ensureAuthenticated: _ensureAuth, ensureAdmin: _ensureAdmin } = require('./auth')
42
- setupAuth(app)
43
- setupAuthRoutes(app)
44
- ensureAuthenticated = _ensureAuth
45
- ensureAdmin = _ensureAdmin
62
+ const tenantId = process.env.AZURE_TENANT_ID
63
+ const clientId = process.env.AZURE_CLIENT_ID
64
+ const clientSecret = process.env.AZURE_CLIENT_SECRET
65
+ const redirectUrl = process.env.AZURE_REDIRECT_URL
66
+ if (!tenantId || !clientId || !clientSecret || !redirectUrl) {
67
+ console.error('[auth] ⚠️ Azure AD habilitado mas variáveis faltando. Defina AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET e AZURE_REDIRECT_URL em env/app.env')
68
+ console.error('[auth] Variáveis atuais: AZURE_TENANT_ID=' + (tenantId ? '***' : 'VAZIO') + ', AZURE_CLIENT_ID=' + (clientId ? '***' : 'VAZIO') + ', AZURE_CLIENT_SECRET=' + (clientSecret ? '***' : 'VAZIO') + ', AZURE_REDIRECT_URL=' + (redirectUrl ? '***' : 'VAZIO'))
69
+ } else {
70
+ const { setupAuth, setupAuthRoutes, ensureAuthenticated: _ensureAuth, ensureAdmin: _ensureAdmin } = require('./auth')
71
+ setupAuth(app)
72
+ setupAuthRoutes(app)
73
+ ensureAuthenticated = _ensureAuth
74
+ ensureAdmin = _ensureAdmin
75
+ }
46
76
  }
77
+ console.log('[startup] auth configured')
47
78
 
48
79
  const IS_LOCAL = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev' || !process.env.NODE_ENV
49
80
 
@@ -77,15 +108,22 @@ function validateNetworkAccess(req, res, next) {
77
108
  return next()
78
109
  }
79
110
 
111
+ const directPeer = (req.connection?.remoteAddress || req.socket?.remoteAddress || '')
112
+ const directPeerNorm = directPeer.replace(/^::ffff:/, '')
113
+ if (isPrivateOrLocalhost(directPeerNorm)) {
114
+ return next()
115
+ }
116
+
80
117
  const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
81
118
  req.headers['x-real-ip'] ||
82
119
  req.ip ||
83
- req.connection.remoteAddress ||
84
- req.socket.remoteAddress
120
+ req.connection?.remoteAddress ||
121
+ req.socket?.remoteAddress ||
122
+ ''
85
123
 
86
124
  if (
87
125
  !IS_LOCAL &&
88
- !isPrivateOrLocalhost(clientIP) &&
126
+ !isPrivateOrLocalhost(String(clientIP).replace(/^::ffff:/, '')) &&
89
127
  !(allowedExternalIPs.length > 0 && allowedExternalIPs.includes(clientIP))
90
128
  ) {
91
129
  return res.status(403).json({
@@ -100,6 +138,39 @@ function validateNetworkAccess(req, res, next) {
100
138
 
101
139
  app.use(validateNetworkAccess)
102
140
 
141
+ const tempoBackendOrigin = (isLocalEnv ? 'http://localhost:3200' : 'http://tempo:3200').replace(/\/$/, '')
142
+ app.use('/tempo-proxy', (req, res) => {
143
+ const pathname = (req.originalUrl || req.url || '').split('?')[0]
144
+ const pathMatch = pathname.match(/\/tempo-proxy\/([^/]+)(\/.*|$)/) || pathname.match(/^\/([^/]+)(\/.*|)$/)
145
+ if (!pathMatch) {
146
+ return res.status(400).json({ error: 'tempo-proxy requires path: /tempo-proxy/:tenantId/...' })
147
+ }
148
+ const tenantId = pathMatch[1]
149
+ const upstreamPath = (pathMatch[2] || '').trim() || '/'
150
+ const q = req.originalUrl.includes('?') ? '?' + req.originalUrl.split('?').slice(1).join('?') : ''
151
+ const targetUrl = `${tempoBackendOrigin}${upstreamPath}${q}`
152
+ const fwdHeaders = { ...req.headers, host: undefined, 'x-scope-orgid': tenantId }
153
+ axios.request({
154
+ method: req.method,
155
+ url: targetUrl,
156
+ headers: fwdHeaders,
157
+ data: req.method !== 'GET' && req.method !== 'HEAD' ? req.body : undefined,
158
+ responseType: 'arraybuffer',
159
+ validateStatus: () => true,
160
+ timeout: 60000,
161
+ maxRedirects: 0
162
+ }).then((proxied) => {
163
+ res.status(proxied.status)
164
+ const h = proxied.headers
165
+ if (h['content-type']) res.set('Content-Type', h['content-type'])
166
+ if (h['content-length']) res.set('Content-Length', h['content-length'])
167
+ res.end(Buffer.from(proxied.data))
168
+ }).catch((err) => {
169
+ console.warn('[tempo-proxy]', tenantId, err.message || err)
170
+ res.status(502).json({ error: 'Tempo proxy error', message: err.message })
171
+ })
172
+ })
173
+
103
174
  const tracer = trace.getTracer('azify-logger', '1.0.0')
104
175
  const propagator = new W3CTraceContextPropagator()
105
176
 
@@ -156,6 +227,7 @@ async function ensureIndexTemplate() {
156
227
  userAgent: { type: 'text' },
157
228
  environment: { type: 'keyword' },
158
229
  hostname: { type: 'keyword' },
230
+ source: { type: 'keyword' },
159
231
  responseBody: { type: 'text' },
160
232
  error: {
161
233
  properties: {
@@ -167,7 +239,7 @@ async function ensureIndexTemplate() {
167
239
  }
168
240
  }
169
241
  },
170
- priority: 500
242
+ priority: 1000
171
243
  })
172
244
  } catch (error) {
173
245
  }
@@ -192,8 +264,7 @@ async function fetchGrafanaOrgNames() {
192
264
  }
193
265
 
194
266
  async function fetchGrafanaOrgs() {
195
- const runningInDocker = fs.existsSync('/.dockerenv')
196
- const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
267
+ const defaultGrafanaUrl = isLocalEnv ? 'http://127.0.0.1:3002' : 'http://azify-grafana:3000'
197
268
  const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
198
269
  const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
199
270
  const grafanaAdminPassword = process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
@@ -273,10 +344,28 @@ function escapeForSQLite(str) {
273
344
  return str.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r')
274
345
  }
275
346
 
276
- function runSqlite(sql) {
347
+ const { exec } = require('child_process')
348
+ const { promisify } = require('util')
349
+ const execAsync = promisify(exec)
350
+
351
+ let _sqlite3Available = null
352
+ async function isSqlite3Available() {
353
+ if (_sqlite3Available !== null) return _sqlite3Available
354
+ try {
355
+ await execAsync('which sqlite3', { encoding: 'utf8' })
356
+ _sqlite3Available = true
357
+ return true
358
+ } catch (_) {
359
+ _sqlite3Available = false
360
+ console.warn('[setupGrafana] sqlite3 não encontrado no PATH; fallback de Organization/datasource via SQLite desabilitado. Instale sqlite3 ou use a API do Grafana.')
361
+ return false
362
+ }
363
+ }
364
+
365
+ /** Async runSqlite: não bloqueia o event loop (evita timeout em /health e auth). */
366
+ async function runSqlite(sql) {
277
367
  const dbPath = process.env.GRAFANA_SQLITE_PATH || '/var/lib/grafana/grafana.db'
278
368
  const grafanaContainer = process.env.GRAFANA_DOCKER_CONTAINER || 'azify-grafana'
279
- const { execSync } = require('child_process')
280
369
  const localDbExists = fs.existsSync(dbPath)
281
370
 
282
371
  const escapedForLocal = sql.replace(/"/g, '""')
@@ -284,30 +373,34 @@ function runSqlite(sql) {
284
373
 
285
374
  const command = localDbExists
286
375
  ? `sqlite3 ${dbPath} "${escapedForLocal}"`
287
- : `docker exec ${grafanaContainer} sh -c "sqlite3 ${dbPath} \\"${escapedForDocker}\\""`;
376
+ : `docker exec ${grafanaContainer} sh -c "sqlite3 ${dbPath} \\"${escapedForDocker}\\""`
288
377
 
289
378
  try {
290
379
  if (!localDbExists) {
291
380
  console.log(`[runSqlite] Executando comando via docker exec no container ${grafanaContainer}`)
292
381
  }
293
- return execSync(command, { encoding: 'utf8', shell: '/bin/bash' }).trim()
382
+ const { stdout } = await execAsync(command, { encoding: 'utf8', shell: '/bin/bash', maxBuffer: 10 * 1024 * 1024 })
383
+ return (stdout || '').trim()
294
384
  } catch (error) {
295
385
  const stderr = error.stderr?.toString()?.trim()
296
- throw new Error(stderr || error.message)
386
+ const msg = stderr || error.message
387
+ if (msg.includes('not found') || msg.includes('sqlite3')) _sqlite3Available = false
388
+ throw new Error(msg)
297
389
  }
298
390
  }
299
391
 
300
392
  async function createOrgViaSQLite(appName) {
301
- const dbPath = '/var/lib/grafana/grafana.db'
393
+ if (!(await isSqlite3Available())) return null
394
+ const dbPath = process.env.GRAFANA_SQLITE_PATH || '/var/lib/grafana/grafana.db'
302
395
 
303
396
  try {
304
- console.log(`[createOrgViaSQLite] Verificando database em ${dbPath}...`)
305
397
  if (!fs.existsSync(dbPath)) {
306
- console.log(`[createOrgViaSQLite] Banco não acessível localmente; tentando via docker exec`)
398
+ if (!isLocalEnv) {
399
+ console.log(`[createOrgViaSQLite] Banco não acessível localmente; tentando via docker exec`)
400
+ }
307
401
  }
308
402
 
309
- console.log(`[createOrgViaSQLite] Verificando se Organization '${appName}' já existe...`)
310
- const existingId = runSqlite(`SELECT id FROM org WHERE name='${appName}';`)
403
+ const existingId = await runSqlite(`SELECT id FROM org WHERE name='${appName}';`)
311
404
 
312
405
  if (existingId && existingId !== '') {
313
406
  console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' já existe (ID: ${existingId})`)
@@ -315,7 +408,7 @@ async function createOrgViaSQLite(appName) {
315
408
  }
316
409
 
317
410
  console.log(`[createOrgViaSQLite] Criando Organization '${appName}'...`)
318
- const newId = runSqlite(`INSERT INTO org (name, version, created, updated) VALUES ('${appName}', 1, datetime('now'), datetime('now')); SELECT id FROM org WHERE name='${appName}';`)
411
+ const newId = await runSqlite(`INSERT INTO org (name, version, created, updated) VALUES ('${appName}', 1, datetime('now'), datetime('now')); SELECT id FROM org WHERE name='${appName}';`)
319
412
 
320
413
  if (newId && newId !== '') {
321
414
  console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' criada com sucesso (ID: ${newId})`)
@@ -330,24 +423,36 @@ async function createOrgViaSQLite(appName) {
330
423
  }
331
424
  }
332
425
 
333
- async function setupGrafanaForApp(appName) {
334
- const runningInDocker = fs.existsSync('/.dockerenv')
335
- const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
336
- const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
337
- const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
338
- const grafanaAdminPassword = process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
339
- const defaultOpensearchUrl = runningInDocker ? 'http://azify-opensearch:9200' : 'http://127.0.0.1:9200'
340
- const opensearchUrl = process.env.OPENSEARCH_URL || defaultOpensearchUrl
341
- const grafanaOpensearchUrl = process.env.GRAFANA_OPENSEARCH_URL || (runningInDocker ? 'http://azify-opensearch:9200' : opensearchUrl)
426
+ let _setupGrafanaQueue = Promise.resolve()
342
427
 
428
+ async function setupGrafanaForApp(appName) {
343
429
  if (!setupGrafanaForApp._cache) {
344
430
  setupGrafanaForApp._cache = new Set()
345
431
  }
346
-
347
432
  if (setupGrafanaForApp._cache.has(appName)) {
348
433
  return
349
434
  }
350
435
 
436
+ const waitFor = _setupGrafanaQueue
437
+ let release
438
+ _setupGrafanaQueue = new Promise((r) => { release = r })
439
+ await waitFor
440
+ try {
441
+ return await _setupGrafanaForAppImpl(appName)
442
+ } finally {
443
+ release()
444
+ }
445
+ }
446
+
447
+ async function _setupGrafanaForAppImpl(appName) {
448
+ const defaultGrafanaUrl = isLocalEnv ? 'http://127.0.0.1:3002' : 'http://azify-grafana:3000'
449
+ const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
450
+ const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
451
+ const grafanaAdminPassword = process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
452
+ const defaultOpensearchUrl = isLocalEnv ? 'http://127.0.0.1:9200' : 'http://azify-opensearch:9200'
453
+ const opensearchUrl = process.env.OPENSEARCH_URL || defaultOpensearchUrl
454
+ const grafanaOpensearchUrl = process.env.GRAFANA_OPENSEARCH_URL || (isLocalEnv ? opensearchUrl : 'http://azify-opensearch:9200')
455
+
351
456
  setupGrafanaForApp._cache.add(appName)
352
457
 
353
458
  console.log(`[setupGrafana] Processando app: ${appName}`)
@@ -463,19 +568,16 @@ async function setupGrafanaForApp(appName) {
463
568
  }
464
569
  }
465
570
  } catch (userError) {
466
- const dbUserId = await new Promise((resolve) => {
467
- try {
468
- const result = runSqlite(`SELECT id FROM user WHERE email='${trimmedEmail}';`)
469
- resolve(result || null)
470
- } catch (err) {
471
- console.error(`[setupGrafana] ❌ Falha ao buscar usuário ${trimmedEmail} via SQLite: ${err.message}`)
472
- resolve(null)
473
- }
474
- })
571
+ let dbUserId = null
572
+ try {
573
+ dbUserId = await runSqlite(`SELECT id FROM user WHERE email='${trimmedEmail}';`) || null
574
+ } catch (err) {
575
+ console.error(`[setupGrafana] ❌ Falha ao buscar usuário ${trimmedEmail} via SQLite: ${err.message}`)
576
+ }
475
577
 
476
578
  if (dbUserId) {
477
579
  try {
478
- 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};`)
580
+ await 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};`)
479
581
  } catch (fallbackError) {
480
582
  console.error(`[setupGrafana] ❌ Falha ao promover ${trimmedEmail} via SQLite: ${fallbackError.message}`)
481
583
  throw fallbackError
@@ -545,17 +647,9 @@ async function setupGrafanaForApp(appName) {
545
647
  const opensearchUrlEscaped = escapeForSQLite(grafanaOpensearchUrl)
546
648
  const dsUidEscaped = escapeForSQLite(datasourceUid)
547
649
  const jsonEscaped = dsJson.replace(/'/g, "''")
548
- const dbPath = process.env.GRAFANA_SQLITE_PATH || '/var/lib/grafana/grafana.db'
549
- const grafanaContainer = process.env.GRAFANA_DOCKER_CONTAINER || 'azify-grafana'
550
- const { execSync } = require('child_process')
551
- const localDbExists = fs.existsSync(dbPath)
552
650
  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);`
553
- if (localDbExists) {
554
- execSync(`sqlite3 ${dbPath} "${sql.replace(/"/g, '""')}"`, { encoding: 'utf8' })
555
- } else {
556
- execSync(`docker exec ${grafanaContainer} sqlite3 ${dbPath} '${sql.replace(/'/g, "'\\''")}'`, { encoding: 'utf8', shell: '/bin/bash' })
557
- }
558
- const dsId = runSqlite('SELECT last_insert_rowid();')
651
+ await runSqlite(sql)
652
+ const dsId = await runSqlite('SELECT last_insert_rowid();')
559
653
  console.log(`[setupGrafana] ✅ Datasource criado via SQLite: ${datasourceUid} (ID: ${dsId})`)
560
654
  } catch (sqliteError) {
561
655
  console.error(`[setupGrafana] ❌ Erro ao criar datasource via SQLite: ${sqliteError.message}`)
@@ -572,17 +666,9 @@ async function setupGrafanaForApp(appName) {
572
666
  const opensearchUrlEscaped = escapeForSQLite(grafanaOpensearchUrl)
573
667
  const dsUidEscaped = escapeForSQLite(datasourceUid)
574
668
  const jsonEscaped = dsJson.replace(/'/g, "''")
575
- const dbPath = process.env.GRAFANA_SQLITE_PATH || '/var/lib/grafana/grafana.db'
576
- const grafanaContainer = process.env.GRAFANA_DOCKER_CONTAINER || 'azify-grafana'
577
- const { execSync } = require('child_process')
578
- const localDbExists = fs.existsSync(dbPath)
579
669
  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);`
580
- if (localDbExists) {
581
- execSync(`sqlite3 ${dbPath} "${sql.replace(/"/g, '""')}"`, { encoding: 'utf8' })
582
- } else {
583
- execSync(`docker exec ${grafanaContainer} sqlite3 ${dbPath} '${sql.replace(/'/g, "'\\''")}'`, { encoding: 'utf8', shell: '/bin/bash' })
584
- }
585
- const dsId = runSqlite('SELECT last_insert_rowid();')
670
+ await runSqlite(sql)
671
+ const dsId = await runSqlite('SELECT last_insert_rowid();')
586
672
  console.log(`[setupGrafana] ✅ Datasource criado via SQLite: ${datasourceUid} (ID: ${dsId})`)
587
673
  } catch (sqliteError) {
588
674
  console.error(`[setupGrafana] ❌ Erro ao criar datasource via SQLite: ${sqliteError.message}`)
@@ -593,8 +679,9 @@ async function setupGrafanaForApp(appName) {
593
679
 
594
680
  try {
595
681
  const tempoDatasourceUid = `tempo-${appName.toLowerCase()}`
596
- const tempoUrl = runningInDocker ? 'http://tempo:3200' : 'http://localhost:3200'
597
682
  const serviceNameNorm = String(appName).trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
683
+ const loggerPort = process.env.PORT || 3001
684
+ const tempoUrl = `http://azify-logger:${loggerPort}/tempo-proxy/${serviceNameNorm}`
598
685
  const tempoJsonData = {
599
686
  httpMethod: 'GET',
600
687
  httpHeaderName1: 'X-Scope-OrgID',
@@ -624,7 +711,6 @@ async function setupGrafanaForApp(appName) {
624
711
  uid: tempoDatasourceUid,
625
712
  isDefault: false,
626
713
  jsonData: tempoJsonData,
627
- secureJsonData: { httpHeaderValue1: serviceNameNorm },
628
714
  editable: true,
629
715
  version: 1
630
716
  }
@@ -649,8 +735,7 @@ async function setupGrafanaForApp(appName) {
649
735
  uid: tempoDatasourceUid,
650
736
  isDefault: false,
651
737
  version: existingTempo.data.version ?? 1,
652
- jsonData: { ...(existingTempo.data.jsonData || {}), ...tempoJsonData },
653
- secureJsonData: { httpHeaderValue1: serviceNameNorm }
738
+ jsonData: { ...(existingTempo.data.jsonData || {}), ...tempoJsonData }
654
739
  }
655
740
  await axios.put(
656
741
  `${grafanaUrl}/api/datasources/${id}`,
@@ -948,17 +1033,9 @@ async function setupGrafanaForApp(appName) {
948
1033
  const slug = dashboardTitle.toLowerCase().replace(/[^a-z0-9-]/g, '-')
949
1034
  const slugEscaped = escapeForSQLite(slug)
950
1035
  const uidEscaped = escapeForSQLite(dashboardUid)
951
- const dbPath = process.env.GRAFANA_SQLITE_PATH || '/var/lib/grafana/grafana.db'
952
- const grafanaContainer = process.env.GRAFANA_DOCKER_CONTAINER || 'azify-grafana'
953
- const { execSync } = require('child_process')
954
- const localDbExists = fs.existsSync(dbPath)
955
1036
  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);`
956
- if (localDbExists) {
957
- execSync(`sqlite3 ${dbPath} "${sql.replace(/"/g, '""')}"`, { encoding: 'utf8' })
958
- } else {
959
- execSync(`docker exec ${grafanaContainer} sqlite3 ${dbPath} '${sql.replace(/'/g, "'\\''")}'`, { encoding: 'utf8', shell: '/bin/bash' })
960
- }
961
- const dashboardId = runSqlite('SELECT last_insert_rowid();')
1037
+ await runSqlite(sql)
1038
+ const dashboardId = await runSqlite('SELECT last_insert_rowid();')
962
1039
  console.log(`[setupGrafana] ✅ Dashboard criado via SQLite: ${dashboardTitle} (ID: ${dashboardId})`)
963
1040
  } catch (sqliteError) {
964
1041
  console.error(`[setupGrafana] ❌ Erro ao criar dashboard via SQLite: ${sqliteError.message}`)
@@ -986,8 +1063,7 @@ async function setupGrafanaForApp(appName) {
986
1063
  }
987
1064
 
988
1065
  async function getGrafanaClientConfig(apiTokenFromRequest) {
989
- const runningInDocker = fs.existsSync('/.dockerenv')
990
- const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
1066
+ const defaultGrafanaUrl = isLocalEnv ? 'http://127.0.0.1:3002' : 'http://azify-grafana:3000'
991
1067
  const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
992
1068
 
993
1069
  const apiToken = apiTokenFromRequest || process.env.GRAFANA_API_TOKEN
@@ -1761,21 +1837,55 @@ async function handleLog(req, res) {
1761
1837
  ))
1762
1838
  )
1763
1839
 
1764
- if (shouldFilterLog) {
1840
+ const isRequestOrResponse = messageLower.includes('[request]') || messageLower.includes('[response]')
1841
+ const isAzifyHttpClientLog = (meta && meta.source === 'http-client') || isRequestOrResponse
1842
+ if (!isAzifyHttpClientLog && shouldFilterLog) {
1765
1843
  return res.json({ success: true, message: 'Log filtrado' })
1766
1844
  }
1845
+ if (isRequestOrResponse && process.env.AZIFY_LOGGER_DEBUG === '1') {
1846
+ console.log('[azify-logger][server] REQUEST/RESPONSE received:', (typeof message === 'string' ? message : '').slice(0, 80))
1847
+ }
1848
+ if (isRequestOrResponse && meta && typeof meta === 'object' && meta.source !== 'http-client') {
1849
+ meta = { ...meta, source: 'http-client' }
1850
+ }
1767
1851
 
1768
1852
  const requestId = meta && meta.requestId
1769
1853
 
1854
+ function toTraceIdHex32(val) {
1855
+ if (val == null || typeof val !== 'string') return null
1856
+ const hex = String(val).replace(/[^0-9a-fA-F]/g, '').slice(0, 32)
1857
+ return hex ? hex.padStart(32, '0').slice(0, 32) : null
1858
+ }
1859
+
1770
1860
  let traceContext = null
1771
1861
 
1772
1862
  if (meta && meta.traceId && meta.spanId) {
1773
- traceContext = {
1774
- traceId: meta.traceId,
1775
- spanId: meta.spanId,
1776
- parentSpanId: meta.parentSpanId || null
1863
+ const tid = toTraceIdHex32(meta.traceId)
1864
+ if (tid) {
1865
+ traceContext = {
1866
+ traceId: tid,
1867
+ spanId: String(meta.spanId).slice(0, 16),
1868
+ parentSpanId: meta.parentSpanId != null && meta.parentSpanId !== '' ? String(meta.parentSpanId) : null
1869
+ }
1777
1870
  }
1778
- } else {
1871
+ }
1872
+
1873
+ if (!traceContext && typeof message === 'string' && /traceId/i.test(message)) {
1874
+ const m = message.match(/"traceId"\s*:\s*"([^"]+)"/)
1875
+ const s = message.match(/"spanId"\s*:\s*"([^"]+)"/)
1876
+ if (m && m[1]) {
1877
+ const tid = toTraceIdHex32(m[1])
1878
+ if (tid) {
1879
+ traceContext = {
1880
+ traceId: tid,
1881
+ spanId: (s && s[1]) ? String(s[1]).slice(0, 16) : generateSpanId(),
1882
+ parentSpanId: null
1883
+ }
1884
+ }
1885
+ }
1886
+ }
1887
+
1888
+ if (!traceContext) {
1779
1889
  try {
1780
1890
  const extractedCtx = propagation.extract(context.active(), req.headers, {
1781
1891
  get (carrier, key) {
@@ -1792,8 +1902,8 @@ async function handleLog(req, res) {
1792
1902
  const spanContext = (span && span.spanContext && span.spanContext()) || null
1793
1903
  if (spanContext && spanContext.traceId && spanContext.spanId) {
1794
1904
  traceContext = {
1795
- traceId: spanContext.traceId,
1796
- spanId: spanContext.spanId,
1905
+ traceId: toTraceIdHex32(spanContext.traceId) || String(spanContext.traceId).replace(/-/g, '').padStart(32, '0').slice(0, 32),
1906
+ spanId: String(spanContext.spanId).slice(0, 16),
1797
1907
  parentSpanId: null
1798
1908
  }
1799
1909
  }
@@ -1916,6 +2026,16 @@ async function handleLog(req, res) {
1916
2026
  value = truncateBody(value, false)
1917
2027
  }
1918
2028
  logEntry[key] = value
2029
+ } else if (key === 'error') {
2030
+ if (value != null && typeof value === 'object' && !Array.isArray(value) && (value.message != null || value.stack != null || value.name != null)) {
2031
+ logEntry.error = {
2032
+ message: value.message != null ? String(value.message) : '',
2033
+ stack: value.stack != null ? String(value.stack) : undefined,
2034
+ name: value.name != null ? String(value.name) : undefined
2035
+ }
2036
+ } else if (value != null) {
2037
+ logEntry.error = { message: String(value) }
2038
+ }
1919
2039
  } else {
1920
2040
  logEntry[key] = value
1921
2041
  }
@@ -1924,60 +2044,161 @@ async function handleLog(req, res) {
1924
2044
  }
1925
2045
 
1926
2046
  logEntry.message = message
2047
+ if (isRequestOrResponse) logEntry.source = 'http-client'
1927
2048
 
1928
- try {
1929
- const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
1930
- const appName = logEntry.appName || logEntry.service?.name || 'unknown'
1931
- const serviceName = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
1932
- const indexName = `logs-${serviceName}`
1933
- addSeenServiceName(serviceName)
1934
-
1935
- await axios.post(`${osUrl}/${indexName}/_doc`, logEntry, {
1936
- headers: { 'Content-Type': 'application/json' }
1937
- })
2049
+ const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
2050
+ const appName = logEntry.appName || logEntry.service?.name || 'unknown'
2051
+ const serviceName = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
2052
+ const indexName = `logs-${serviceName}`
2053
+ addSeenServiceName(serviceName)
1938
2054
 
1939
- console.log(`[setupGrafana] serviceName: ${serviceName}, appName: ${appName}`)
2055
+ res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
1940
2056
 
1941
- if (serviceName !== 'unknown' && serviceName !== 'unknown-service') {
1942
- console.log(`[setupGrafana] Iniciando setup para app: ${serviceName}`)
1943
- setupGrafanaForApp(serviceName).then(() => {
1944
- console.log(`[setupGrafana] ✅ Setup concluído para ${serviceName}`)
1945
- }).catch((err) => {
1946
- const errorMsg = err?.response?.data?.message || err?.message || 'Erro desconhecido'
1947
- const status = err?.response?.status || 'N/A'
1948
- console.error(`[setupGrafana] ❌ Erro ao configurar Grafana para ${serviceName}: ${errorMsg} (status: ${status})`)
1949
- if (err?.stack) {
1950
- console.error(`[setupGrafana] Stack: ${err.stack.substring(0, 200)}`)
1951
- }
1952
- })
1953
- } else {
1954
- console.log(`[setupGrafana] ⚠️ Pulando setup para serviceName inválido: ${serviceName}`)
1955
- }
2057
+ _enqueueOpenSearchWrite(osUrl, indexName, logEntry, serviceName)
2058
+ }
1956
2059
 
1957
- res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
1958
- } catch (error) {
1959
- const status = error?.response?.status
1960
- const errorMsg = error?.response?.data?.error?.reason ||
1961
- error?.response?.data?.message ||
1962
- error?.message ||
1963
- 'Erro desconhecido'
1964
-
1965
- console.error('❌ Falha ao enviar log para OpenSearch', {
1966
- status,
1967
- message: errorMsg
1968
- })
2060
+ const _openSearchWriteQueue = []
2061
+ let _openSearchWriteInFlight = 0
2062
+ const _openSearchWriteMaxConcurrent = 10
2063
+ const _openSearchWriteTimeout = 5000
2064
+ let _openSearchFailuresInRow = 0
2065
+ let _openSearchCircuitOpenUntil = 0
2066
+ let _openSearchCircuitRetryTimer = null
2067
+ const _openSearchCircuitThreshold = 5
2068
+ const _openSearchCircuitPauseMs = 30000
2069
+ const _openSearchQueueMaxLen = 2000
2070
+
2071
+ const _logDedupeWindowMs = 5000
2072
+ const _logDedupeMaxKeys = 15000
2073
+ const _logDedupeSeen = new Map()
2074
+ const _logDedupeBucketMs = 80
2075
+
2076
+ function _logDedupeKey(serviceName, logEntry) {
2077
+ const msg = typeof logEntry.message === 'string' ? logEntry.message.substring(0, 500) : String(logEntry.message || '')
2078
+ const msgLower = msg.toLowerCase()
2079
+ const isReqRes = msgLower.includes('[request]') || msgLower.includes('[response]')
2080
+ const traceId = logEntry.traceId != null ? String(logEntry.traceId).trim() : ''
2081
+ const spanId = logEntry.spanId != null ? String(logEntry.spanId).trim() : ''
2082
+ if (isReqRes) {
2083
+ const kind = msgLower.includes('[request]') ? 'REQUEST' : 'RESPONSE'
2084
+ if (traceId && spanId) return `r|${serviceName}|${traceId}|${spanId}|${kind}`
2085
+ const ts = logEntry['@timestamp'] != null ? Number(logEntry['@timestamp']) : Date.now()
2086
+ const bucket = Math.floor(ts / _logDedupeBucketMs) * _logDedupeBucketMs
2087
+ return `r|${serviceName}|${bucket}|${kind}`
2088
+ }
2089
+ if (traceId && spanId) return `t|${serviceName}|${traceId}|${spanId}|${msg}`
2090
+ const requestId = (logEntry.requestId != null ? String(logEntry.requestId).trim() : '') || ''
2091
+ if (!requestId) return null
2092
+ const ts = logEntry['@timestamp'] != null ? Number(logEntry['@timestamp']) : Date.now()
2093
+ const bucket = Math.floor(ts / _logDedupeBucketMs) * _logDedupeBucketMs
2094
+ return `b|${serviceName}|${requestId}|${msg}|${bucket}`
2095
+ }
1969
2096
 
1970
- if (error?.response?.data) {
1971
- try {
1972
- console.error('📦 Resposta OpenSearch:', JSON.stringify(error.response.data).substring(0, 2000))
1973
- } catch (_) {
1974
- console.error('📦 Resposta OpenSearch (raw):', error.response.data)
2097
+ function _enqueueOpenSearchWrite(osUrl, indexName, logEntry, serviceName) {
2098
+ if (_openSearchWriteQueue.length >= _openSearchQueueMaxLen) {
2099
+ if (!_enqueueOpenSearchWrite._lastQueueFullLog || Date.now() - _enqueueOpenSearchWrite._lastQueueFullLog > 60000) {
2100
+ _enqueueOpenSearchWrite._lastQueueFullLog = Date.now()
2101
+ console.warn('[opensearch] Fila cheia, log descartado (queue full). Verifique se o OpenSearch está acessível.')
2102
+ }
2103
+ return
2104
+ }
2105
+ const key = _logDedupeKey(serviceName, logEntry)
2106
+ const now = Date.now()
2107
+ if (key != null) {
2108
+ if (_logDedupeSeen.size > _logDedupeMaxKeys) {
2109
+ for (const [k, exp] of _logDedupeSeen) {
2110
+ if (exp < now) _logDedupeSeen.delete(k)
1975
2111
  }
1976
- } else if (error?.code) {
1977
- console.error('⚙️ Código de erro:', error.code)
1978
2112
  }
2113
+ const expiry = _logDedupeSeen.get(key)
2114
+ if (expiry != null && expiry > now) return
2115
+ _logDedupeSeen.set(key, now + _logDedupeWindowMs)
2116
+ }
2117
+ if (process.env.AZIFY_LOGGER_DEBUG === '1') {
2118
+ const msg = typeof logEntry.message === 'string' ? logEntry.message : ''
2119
+ if (msg.toLowerCase().includes('[request]') || msg.toLowerCase().includes('[response]')) {
2120
+ console.log('[azify-logger][server] REQUEST/RESPONSE enqueued for OpenSearch:', msg.slice(0, 70))
2121
+ }
2122
+ }
2123
+ _openSearchWriteQueue.push({ osUrl, indexName, logEntry, serviceName })
2124
+ _drainOpenSearchQueue()
2125
+ }
1979
2126
 
1980
- res.status(500).json({ success: false, message: 'Erro ao enviar log para OpenSearch' })
2127
+ function _drainOpenSearchQueue() {
2128
+ const now = Date.now()
2129
+ if (now < _openSearchCircuitOpenUntil) return
2130
+ if (now >= _openSearchCircuitOpenUntil && _openSearchCircuitOpenUntil > 0) {
2131
+ _openSearchCircuitOpenUntil = 0
2132
+ _openSearchFailuresInRow = 0
2133
+ console.log('[opensearch] Retomando envio após pausa do circuit breaker')
2134
+ }
2135
+ while (_openSearchWriteInFlight < _openSearchWriteMaxConcurrent && _openSearchWriteQueue.length > 0) {
2136
+ const job = _openSearchWriteQueue.shift()
2137
+ if (!job) break
2138
+ _openSearchWriteInFlight++
2139
+ axios.post(`${job.osUrl}/${job.indexName}/_doc`, job.logEntry, {
2140
+ headers: { 'Content-Type': 'application/json' },
2141
+ timeout: _openSearchWriteTimeout
2142
+ })
2143
+ .then(() => {
2144
+ _openSearchFailuresInRow = 0
2145
+ if (process.env.AZIFY_LOGGER_DEBUG === '1') {
2146
+ const msg = typeof job.logEntry.message === 'string' ? job.logEntry.message : ''
2147
+ if (msg.toLowerCase().includes('[request]') || msg.toLowerCase().includes('[response]')) {
2148
+ console.log('[azify-logger][server] REQUEST/RESPONSE written to OpenSearch index=', job.indexName, 'msg=', msg.slice(0, 60))
2149
+ }
2150
+ }
2151
+ const SETUP_THROTTLE_MS = 10 * 60 * 1000
2152
+ if (!handleLog._lastSetupTime) handleLog._lastSetupTime = new Map()
2153
+ const last = handleLog._lastSetupTime.get(job.serviceName) || 0
2154
+ const now = Date.now()
2155
+ const shouldRunSetup = job.serviceName !== 'unknown' && job.serviceName !== 'unknown-service' && job.serviceName !== 'unknown-app' && (now - last >= SETUP_THROTTLE_MS)
2156
+ if (shouldRunSetup) {
2157
+ handleLog._lastSetupTime.set(job.serviceName, now)
2158
+ setupGrafanaForApp(job.serviceName).then(() => {
2159
+ if (process.env.NODE_ENV === 'development') console.log(`[setupGrafana] ✅ Setup concluído para ${job.serviceName}`)
2160
+ }).catch((err) => {
2161
+ const status = err?.response?.status
2162
+ const msg = (err?.message || '').toString()
2163
+ const isAuthOrSqlite = status === 401 || msg.includes('sqlite3') || msg.includes('Invalid username or password')
2164
+ if (isAuthOrSqlite) {
2165
+ handleLog._lastSetupTime.set(job.serviceName, now + 60 * 60 * 1000)
2166
+ }
2167
+ const errorMsg = err?.response?.data?.message || err?.message || 'Erro desconhecido'
2168
+ console.error(`[setupGrafana] ❌ Erro ao configurar Grafana para ${job.serviceName}: ${errorMsg} (status: ${status || 'N/A'})`)
2169
+ if (err?.stack) {
2170
+ console.error(`[setupGrafana] Stack: ${err.stack.substring(0, 200)}`)
2171
+ }
2172
+ })
2173
+ }
2174
+ })
2175
+ .catch((error) => {
2176
+ _openSearchFailuresInRow++
2177
+ if (_openSearchFailuresInRow >= _openSearchCircuitThreshold) {
2178
+ _openSearchCircuitOpenUntil = Date.now() + _openSearchCircuitPauseMs
2179
+ console.error('[opensearch] Indisponível, pausando envio por', _openSearchCircuitPauseMs / 1000, 's')
2180
+ if (_openSearchCircuitRetryTimer == null) {
2181
+ _openSearchCircuitRetryTimer = setTimeout(() => {
2182
+ _openSearchCircuitRetryTimer = null
2183
+ _drainOpenSearchQueue()
2184
+ }, _openSearchCircuitPauseMs)
2185
+ if (_openSearchCircuitRetryTimer.unref) _openSearchCircuitRetryTimer.unref()
2186
+ }
2187
+ }
2188
+ if (_openSearchFailuresInRow <= 2) {
2189
+ const status = error?.response?.status
2190
+ const errorMsg = error?.response?.data?.error?.reason ||
2191
+ error?.response?.data?.message ||
2192
+ error?.message ||
2193
+ 'Erro desconhecido'
2194
+ console.error('❌ Falha ao enviar log para OpenSearch', { status, message: errorMsg })
2195
+ if (error?.code) console.error('⚙️ Código de erro:', error.code)
2196
+ }
2197
+ })
2198
+ .finally(() => {
2199
+ _openSearchWriteInFlight--
2200
+ _drainOpenSearchQueue()
2201
+ })
1981
2202
  }
1982
2203
  }
1983
2204
 
@@ -2003,24 +2224,37 @@ function extractServiceNamesFromTraceBody(body) {
2003
2224
  return names
2004
2225
  }
2005
2226
 
2227
+ function normalizeServiceNameForTempo(s) {
2228
+ const t = String(s || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
2229
+ return t && t !== 'unknown' && t !== 'unknown-service' ? t : 'default'
2230
+ }
2231
+
2006
2232
  app.post('/v1/traces', async (req, res) => {
2007
2233
  try {
2008
2234
  const names = extractServiceNamesFromTraceBody(req.body)
2009
2235
  for (const name of names) addSeenServiceName(name)
2010
2236
  if (req.body?.resourceSpans?.length) scheduleOtelCollectorConfigWrite()
2011
2237
 
2012
- const ep = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
2013
- let traceUrl = 'http://127.0.0.1:4318/v1/traces'
2014
- if (ep) {
2015
- try {
2016
- const u = new URL(ep)
2017
- traceUrl = ep.includes('/v1/') ? ep : `${u.origin.replace(/\/$/, '')}/v1/traces`
2018
- } catch (_) {}
2238
+ const firstService = names.size ? normalizeServiceNameForTempo(Array.from(names)[0]) : 'default'
2239
+ const headers = { 'Content-Type': 'application/json' }
2240
+ if (firstService) headers['X-Scope-OrgID'] = firstService
2241
+
2242
+ let traceUrl
2243
+ if (!isLocalEnv) {
2244
+ traceUrl = 'http://tempo:4318/v1/traces'
2245
+ } else {
2246
+ const ep = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
2247
+ traceUrl = 'http://127.0.0.1:4318/v1/traces'
2248
+ if (ep) {
2249
+ try {
2250
+ const u = new URL(ep)
2251
+ traceUrl = ep.includes('/v1/') ? ep : `${u.origin.replace(/\/$/, '')}/v1/traces`
2252
+ } catch (_) {}
2253
+ }
2019
2254
  }
2255
+
2020
2256
  const response = await axios.post(traceUrl, req.body, {
2021
- headers: {
2022
- 'Content-Type': 'application/json'
2023
- },
2257
+ headers,
2024
2258
  data: req.body,
2025
2259
  timeout: 30000
2026
2260
  })
@@ -2092,8 +2326,7 @@ app.post('/dashboards/register', async (req, res) => {
2092
2326
 
2093
2327
  await setupGrafanaForApp(appName)
2094
2328
 
2095
- const runningInDocker = fs.existsSync('/.dockerenv')
2096
- const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
2329
+ const defaultGrafanaUrl = isLocalEnv ? 'http://127.0.0.1:3002' : 'http://azify-grafana:3000'
2097
2330
  const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
2098
2331
  const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
2099
2332
  const grafanaAdminPassword = process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
@@ -2182,7 +2415,7 @@ app.post('/dashboards/register', async (req, res) => {
2182
2415
  {
2183
2416
  auth,
2184
2417
  headers: { 'X-Grafana-Org-Id': orgId, 'Content-Type': 'application/json' },
2185
- timeout: 10000
2418
+ timeout: 5000
2186
2419
  }
2187
2420
  )
2188
2421
 
@@ -2212,23 +2445,38 @@ app.post('/dashboards/register', async (req, res) => {
2212
2445
  }
2213
2446
  })
2214
2447
 
2448
+ app.use((err, req, res, next) => {
2449
+ console.error('[server] Erro não tratado:', err?.message || err)
2450
+ if (err?.stack) console.error('[server] Stack:', err.stack)
2451
+ if (!res.headersSent) {
2452
+ res.status(500).json({ success: false, message: 'Internal Server Error', error: process.env.NODE_ENV === 'development' ? (err?.message || String(err)) : undefined })
2453
+ }
2454
+ })
2455
+
2215
2456
  const port = process.env.PORT || 3001
2216
2457
 
2217
2458
  app.listen(port, () => {
2459
+ console.log('[startup] server listening on port', port, '- /health ready')
2218
2460
  setTimeout(() => writeOtelCollectorConfigIfEnabled().catch(() => {}), 3000)
2219
- setTimeout(async () => {
2220
- try {
2221
- const orgs = await fetchGrafanaOrgs()
2222
- const toSetup = orgs.filter((o) => o.id !== 1 && o.name && o.name.toLowerCase() !== 'main org')
2223
- if (setupGrafanaForApp._cache) setupGrafanaForApp._cache.clear()
2224
- for (const org of toSetup) {
2225
- await setupGrafanaForApp(org.name).catch((e) => console.warn('[setupGrafana] Reaplicar Tempo para org', org.name, ':', e?.message || e))
2226
- }
2227
- if (toSetup.length) await writeOtelCollectorConfigIfEnabled().catch(() => {})
2228
- } catch (e) {
2229
- console.warn('[setupGrafana] Reaplicar Tempo para todas as orgs:', e?.message || e)
2230
- }
2231
- }, 12000)
2461
+ setTimeout(() => {
2462
+ fetchGrafanaOrgs()
2463
+ .then((orgs) => {
2464
+ const toSetup = orgs.filter((o) => o.id !== 1 && o.name && o.name.toLowerCase() !== 'main org')
2465
+ if (setupGrafanaForApp._cache) setupGrafanaForApp._cache.clear()
2466
+ function runNext(i) {
2467
+ if (i >= toSetup.length) {
2468
+ if (toSetup.length) writeOtelCollectorConfigIfEnabled().catch(() => {})
2469
+ return
2470
+ }
2471
+ setImmediate(() => {
2472
+ setupGrafanaForApp(toSetup[i].name).catch((e) => console.warn('[setupGrafana] Reaplicar Tempo para org', toSetup[i].name, ':', e?.message || e))
2473
+ .finally(() => setTimeout(() => runNext(i + 1), 1000))
2474
+ })
2475
+ }
2476
+ runNext(0)
2477
+ })
2478
+ .catch((e) => console.warn('[setupGrafana] Reaplicar Tempo para todas as orgs:', e?.message || e))
2479
+ }, 60000)
2232
2480
  })
2233
2481
 
2234
2482
  process.on('SIGTERM', () => {