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/README.md +3 -3
- package/index.js +84 -7
- package/init.js +21 -11
- package/middleware-express.js +46 -44
- package/middleware-pino.js +173 -0
- package/package.json +5 -1
- package/pino-config.js +26 -0
- package/preload.js +15 -0
- package/register-http-client-early.js +816 -0
- package/register.js +1327 -372
- package/sampling.js +1 -1
- package/server.js +407 -159
- package/store.js +37 -11
- package/streams/httpQueue.js +24 -12
- package/streams/pino.js +59 -7
package/server.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
require('
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
84
|
-
req.socket
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
+
if (!isLocalEnv) {
|
|
399
|
+
console.log(`[createOrgViaSQLite] Banco não acessível localmente; tentando via docker exec`)
|
|
400
|
+
}
|
|
307
401
|
}
|
|
308
402
|
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
581
|
-
|
|
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
|
-
|
|
957
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
-
|
|
2055
|
+
res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
|
|
1940
2056
|
|
|
1941
|
-
|
|
1942
|
-
|
|
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
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
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
|
|
2013
|
-
|
|
2014
|
-
if (
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
|
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:
|
|
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(
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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', () => {
|