azify-logger 1.0.24 → 1.0.26
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 +86 -241
- package/package.json +30 -8
- package/register.js +149 -0
- package/server.js +756 -61
package/server.js
CHANGED
|
@@ -4,6 +4,7 @@ const axios = require('axios')
|
|
|
4
4
|
const express = require('express')
|
|
5
5
|
const cors = require('cors')
|
|
6
6
|
const os = require('os')
|
|
7
|
+
const fs = require('fs')
|
|
7
8
|
let trace, context, propagation, W3CTraceContextPropagator
|
|
8
9
|
try {
|
|
9
10
|
const otelApi = require('@opentelemetry/api')
|
|
@@ -27,6 +28,17 @@ app.use(express.json({ limit: '10mb' }))
|
|
|
27
28
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
|
|
28
29
|
app.use(cors())
|
|
29
30
|
|
|
31
|
+
const authEnabled = process.env.AZURE_AD_AUTH_ENABLED === 'true'
|
|
32
|
+
let ensureAuthenticated = (req, res, next) => next()
|
|
33
|
+
let ensureAdmin = (req, res, next) => res.status(403).send('Forbidden')
|
|
34
|
+
if (authEnabled) {
|
|
35
|
+
const { setupAuth, setupAuthRoutes, ensureAuthenticated: _ensureAuth, ensureAdmin: _ensureAdmin } = require('./auth')
|
|
36
|
+
setupAuth(app)
|
|
37
|
+
setupAuthRoutes(app)
|
|
38
|
+
ensureAuthenticated = _ensureAuth
|
|
39
|
+
ensureAdmin = _ensureAdmin
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
const IS_LOCAL = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev' || !process.env.NODE_ENV
|
|
31
43
|
|
|
32
44
|
function isPrivateOrLocalhost(ip) {
|
|
@@ -83,12 +95,6 @@ const propagator = new W3CTraceContextPropagator()
|
|
|
83
95
|
|
|
84
96
|
const traceContextMap = new Map()
|
|
85
97
|
|
|
86
|
-
/**
|
|
87
|
-
* Creates an index template for dynamic log indices
|
|
88
|
-
* This allows OpenSearch to auto-create indices like logs-azipay, logs-assemble, etc.
|
|
89
|
-
* @returns {Promise<void>}
|
|
90
|
-
* @private
|
|
91
|
-
*/
|
|
92
98
|
async function ensureIndexTemplate() {
|
|
93
99
|
const templateName = 'logs-template'
|
|
94
100
|
const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
|
|
@@ -97,61 +103,659 @@ async function ensureIndexTemplate() {
|
|
|
97
103
|
await axios.put(`${osUrl}/_index_template/${templateName}`, {
|
|
98
104
|
index_patterns: ['logs-*'],
|
|
99
105
|
template: {
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
settings: {
|
|
107
|
+
number_of_shards: 1,
|
|
102
108
|
number_of_replicas: 0,
|
|
103
109
|
'index.refresh_interval': '5s'
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
},
|
|
111
|
+
mappings: {
|
|
112
|
+
properties: {
|
|
107
113
|
'@timestamp': { type: 'date' },
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
level: { type: 'keyword' },
|
|
115
|
+
message: { type: 'text' },
|
|
116
|
+
service: {
|
|
117
|
+
properties: {
|
|
118
|
+
name: { type: 'keyword' },
|
|
119
|
+
version: { type: 'keyword' }
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
appName: {
|
|
123
|
+
type: 'keyword',
|
|
124
|
+
fields: {
|
|
125
|
+
keyword: { type: 'keyword' }
|
|
114
126
|
}
|
|
115
127
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
traceId: { type: 'keyword' },
|
|
129
|
+
spanId: { type: 'keyword' },
|
|
130
|
+
parentSpanId: { type: 'keyword' },
|
|
131
|
+
userId: { type: 'keyword' },
|
|
132
|
+
requestId: { type: 'keyword' },
|
|
133
|
+
method: { type: 'keyword' },
|
|
134
|
+
url: { type: 'keyword' },
|
|
135
|
+
statusCode: { type: 'integer' },
|
|
136
|
+
responseTime: { type: 'float' },
|
|
137
|
+
ip: { type: 'ip' },
|
|
138
|
+
userAgent: { type: 'text' },
|
|
139
|
+
environment: { type: 'keyword' },
|
|
140
|
+
hostname: { type: 'keyword' },
|
|
141
|
+
responseBody: { type: 'text' },
|
|
142
|
+
error: {
|
|
143
|
+
properties: {
|
|
144
|
+
message: { type: 'text' },
|
|
145
|
+
stack: { type: 'text' },
|
|
146
|
+
name: { type: 'keyword' }
|
|
147
|
+
}
|
|
136
148
|
}
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
|
-
}
|
|
140
151
|
},
|
|
141
152
|
priority: 500
|
|
142
153
|
})
|
|
143
|
-
console.log(`✅ Index template ${templateName} criado/atualizado no OpenSearch`)
|
|
144
|
-
console.log(` Índices serão criados automaticamente no formato: logs-{service-name}`)
|
|
145
154
|
} catch (error) {
|
|
146
|
-
console.error('❌ Erro ao criar index template:', error.message)
|
|
147
|
-
if (error.response) {
|
|
148
|
-
console.error(' Detalhes:', error.response.data)
|
|
149
|
-
}
|
|
150
155
|
}
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
ensureIndexTemplate()
|
|
154
159
|
|
|
160
|
+
function escapeForSQLite(str) {
|
|
161
|
+
if (!str) return "''"
|
|
162
|
+
return str.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function createOrgViaSQLite(appName) {
|
|
166
|
+
const dbPath = '/var/lib/grafana/grafana.db'
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
console.log(`[createOrgViaSQLite] Verificando database em ${dbPath}...`)
|
|
170
|
+
if (!fs.existsSync(dbPath)) {
|
|
171
|
+
console.error(`[createOrgViaSQLite] ❌ Database não encontrado em ${dbPath}`)
|
|
172
|
+
throw new Error(`Database não encontrado em ${dbPath}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { execSync } = require('child_process')
|
|
176
|
+
|
|
177
|
+
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()
|
|
180
|
+
|
|
181
|
+
if (existingId && existingId !== '') {
|
|
182
|
+
console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' já existe (ID: ${existingId})`)
|
|
183
|
+
return { id: parseInt(existingId), name: appName }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
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()
|
|
189
|
+
|
|
190
|
+
if (newId && newId !== '') {
|
|
191
|
+
console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' criada com sucesso (ID: ${newId})`)
|
|
192
|
+
return { id: parseInt(newId), name: appName }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.error(`[createOrgViaSQLite] ❌ Não foi possível obter ID da Organization criada`)
|
|
196
|
+
return null
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`[createOrgViaSQLite] ❌ Erro: ${error.message}`)
|
|
199
|
+
throw new Error(`Erro ao criar Organization via SQLite: ${error.message}`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function setupGrafanaForApp(appName) {
|
|
204
|
+
const grafanaUrl = process.env.GRAFANA_URL || 'http://azify-grafana:3000'
|
|
205
|
+
const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
|
|
206
|
+
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'
|
|
208
|
+
|
|
209
|
+
if (!setupGrafanaForApp._cache) {
|
|
210
|
+
setupGrafanaForApp._cache = new Set()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (setupGrafanaForApp._cache.has(appName)) {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setupGrafanaForApp._cache.add(appName)
|
|
218
|
+
|
|
219
|
+
console.log(`[setupGrafana] Processando app: ${appName}`)
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const auth = {
|
|
223
|
+
username: grafanaAdminUser,
|
|
224
|
+
password: grafanaAdminPassword
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`[setupGrafana] Usando autenticação: user=${grafanaAdminUser}, password=${grafanaAdminPassword ? '***' : 'VAZIO'}`)
|
|
228
|
+
|
|
229
|
+
let org
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const orgResponse = await axios.get(`${grafanaUrl}/api/orgs/name/${encodeURIComponent(appName)}`, {
|
|
233
|
+
auth,
|
|
234
|
+
timeout: 3000
|
|
235
|
+
})
|
|
236
|
+
org = orgResponse.data
|
|
237
|
+
console.log(`[setupGrafana] ✅ Organization encontrada via API: ${org.name} (ID: ${org.id})`)
|
|
238
|
+
} catch (apiError) {
|
|
239
|
+
if (apiError.response?.status === 404) {
|
|
240
|
+
try {
|
|
241
|
+
const createResponse = await axios.post(
|
|
242
|
+
`${grafanaUrl}/api/orgs`,
|
|
243
|
+
{ name: appName },
|
|
244
|
+
{ auth, timeout: 3000 }
|
|
245
|
+
)
|
|
246
|
+
org = { id: createResponse.data.orgId, name: appName }
|
|
247
|
+
console.log(`[setupGrafana] ✅ Organization criada via API: ${org.name} (ID: ${org.id})`)
|
|
248
|
+
} catch (createError) {
|
|
249
|
+
console.log(`[setupGrafana] ⚠️ Erro ao criar Organization via API: ${createError.response?.status || 'N/A'} - ${createError.response?.data?.message || createError.message}`)
|
|
250
|
+
try {
|
|
251
|
+
console.log(`[setupGrafana] 🔄 Tentando criar Organization via SQLite...`)
|
|
252
|
+
org = await createOrgViaSQLite(appName)
|
|
253
|
+
if (!org) {
|
|
254
|
+
console.error(`[setupGrafana] ❌ Não foi possível criar Organization via SQLite para ${appName}`)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
} catch (sqliteError) {
|
|
258
|
+
console.error(`[setupGrafana] ❌ Erro ao criar Organization via SQLite: ${sqliteError.message}`)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
console.log(`[setupGrafana] ⚠️ Erro ao buscar Organization via API: ${apiError.response?.status || 'N/A'} - ${apiError.response?.data?.message || apiError.message}`)
|
|
264
|
+
try {
|
|
265
|
+
console.log(`[setupGrafana] 🔄 Tentando criar Organization via SQLite...`)
|
|
266
|
+
org = await createOrgViaSQLite(appName)
|
|
267
|
+
if (!org) {
|
|
268
|
+
console.error(`[setupGrafana] ❌ Não foi possível criar Organization via SQLite para ${appName}`)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
} catch (sqliteError) {
|
|
272
|
+
console.error(`[setupGrafana] ❌ Erro ao criar Organization via SQLite: ${sqliteError.message}`)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!org) {
|
|
279
|
+
console.error(`[setupGrafana] ❌ Organization não encontrada/criada para ${appName}`)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log(`[setupGrafana] ✅ Organization encontrada/criada: ${org.name} (ID: ${org.id})`)
|
|
284
|
+
|
|
285
|
+
const adminEmails = process.env.ADMIN_EMAILS ? process.env.ADMIN_EMAILS.split(',') : []
|
|
286
|
+
if (adminEmails.length > 0) {
|
|
287
|
+
for (const email of adminEmails) {
|
|
288
|
+
const trimmedEmail = email.trim()
|
|
289
|
+
if (!trimmedEmail) continue
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const userResponse = await axios.get(`${grafanaUrl}/api/users/lookup?loginOrEmail=${encodeURIComponent(trimmedEmail)}`, {
|
|
293
|
+
auth,
|
|
294
|
+
timeout: 3000
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
if (userResponse.data && userResponse.data.id) {
|
|
298
|
+
const userId = userResponse.data.id
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
await axios.post(
|
|
302
|
+
`${grafanaUrl}/api/orgs/${org.id}/users`,
|
|
303
|
+
{
|
|
304
|
+
loginOrEmail: trimmedEmail,
|
|
305
|
+
role: 'Admin'
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
auth,
|
|
309
|
+
headers: { 'X-Grafana-Org-Id': org.id },
|
|
310
|
+
timeout: 3000
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
} catch (addError) {
|
|
314
|
+
if (addError.response?.status === 409 || addError.response?.status === 412) {
|
|
315
|
+
try {
|
|
316
|
+
await axios.patch(
|
|
317
|
+
`${grafanaUrl}/api/orgs/${org.id}/users/${userId}`,
|
|
318
|
+
{ role: 'Admin' },
|
|
319
|
+
{
|
|
320
|
+
auth,
|
|
321
|
+
headers: { 'X-Grafana-Org-Id': org.id },
|
|
322
|
+
timeout: 3000
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
} catch (updateError) {
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch (userError) {
|
|
332
|
+
const dbUserId = await new Promise((resolve) => {
|
|
333
|
+
const { execSync } = require('child_process')
|
|
334
|
+
try {
|
|
335
|
+
const result = execSync(`sqlite3 /var/lib/grafana/grafana.db "SELECT id FROM user WHERE email='${trimmedEmail}';"`, { encoding: 'utf8' }).trim()
|
|
336
|
+
resolve(result || null)
|
|
337
|
+
} catch {
|
|
338
|
+
resolve(null)
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
if (dbUserId) {
|
|
343
|
+
const { execSync } = require('child_process')
|
|
344
|
+
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 {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(`[setupGrafana] 📊 Criando datasource para ${appName}...`)
|
|
354
|
+
|
|
355
|
+
const datasourceUid = `opensearch-${appName.toLowerCase()}`
|
|
356
|
+
const datasourceConfig = {
|
|
357
|
+
name: `OpenSearch-${appName}`,
|
|
358
|
+
type: 'grafana-opensearch-datasource',
|
|
359
|
+
access: 'proxy',
|
|
360
|
+
url: opensearchUrl,
|
|
361
|
+
uid: datasourceUid,
|
|
362
|
+
isDefault: true,
|
|
363
|
+
jsonData: {
|
|
364
|
+
index: `logs-${appName}`,
|
|
365
|
+
database: `logs-${appName}`,
|
|
366
|
+
timeField: '@timestamp',
|
|
367
|
+
esVersion: '2.11.1',
|
|
368
|
+
version: '2.11.1',
|
|
369
|
+
logMessageField: 'message',
|
|
370
|
+
logLevelField: 'level',
|
|
371
|
+
maxConcurrentShardRequests: 5,
|
|
372
|
+
includeFrozen: false,
|
|
373
|
+
xpack: false,
|
|
374
|
+
flavor: 'opensearch'
|
|
375
|
+
},
|
|
376
|
+
editable: true,
|
|
377
|
+
version: 1
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
console.log(`[setupGrafana] Verificando datasource existente: ${datasourceUid} na org ${org.id}`)
|
|
382
|
+
const existingResponse = await axios.get(`${grafanaUrl}/api/datasources/uid/${datasourceUid}`, {
|
|
383
|
+
auth,
|
|
384
|
+
headers: { 'X-Grafana-Org-Id': org.id }
|
|
385
|
+
})
|
|
386
|
+
console.log(`[setupGrafana] Datasource existente encontrado: ${existingResponse.data?.id || 'N/A'}`)
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
await axios.put(
|
|
390
|
+
`${grafanaUrl}/api/datasources/${existingResponse.data.id}`,
|
|
391
|
+
datasourceConfig,
|
|
392
|
+
{
|
|
393
|
+
auth,
|
|
394
|
+
headers: { 'X-Grafana-Org-Id': org.id }
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
console.log(`[setupGrafana] ✅ Datasource atualizado: ${datasourceUid}`)
|
|
398
|
+
} catch (updateError) {
|
|
399
|
+
console.error(`[setupGrafana] ❌ Erro ao atualizar datasource: ${updateError.response?.data?.message || updateError.message}`)
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.log(`[setupGrafana] Erro ao verificar datasource: status=${error.response?.status || 'N/A'}, message=${error.response?.data?.message || error.message}`)
|
|
403
|
+
if (error.response?.status === 404) {
|
|
404
|
+
try {
|
|
405
|
+
const createResponse = await axios.post(
|
|
406
|
+
`${grafanaUrl}/api/datasources`,
|
|
407
|
+
datasourceConfig,
|
|
408
|
+
{
|
|
409
|
+
auth,
|
|
410
|
+
headers: { 'X-Grafana-Org-Id': org.id }
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
console.log(`[setupGrafana] ✅ Datasource criado: ${datasourceUid} (ID: ${createResponse.data?.datasource?.id || 'N/A'})`)
|
|
414
|
+
} catch (createError) {
|
|
415
|
+
console.error(`[setupGrafana] ❌ Erro ao criar datasource via API: ${createError.response?.data?.message || createError.message}`)
|
|
416
|
+
if (createError.response?.status === 401) {
|
|
417
|
+
console.log(`[setupGrafana] 🔄 Tentando criar datasource via SQLite...`)
|
|
418
|
+
try {
|
|
419
|
+
const { execSync } = require('child_process')
|
|
420
|
+
const dsJson = JSON.stringify(datasourceConfig.jsonData)
|
|
421
|
+
const dsName = escapeForSQLite(datasourceConfig.name)
|
|
422
|
+
const opensearchUrlEscaped = escapeForSQLite(opensearchUrl)
|
|
423
|
+
const dsUidEscaped = escapeForSQLite(datasourceUid)
|
|
424
|
+
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)
|
|
431
|
+
console.log(`[setupGrafana] ✅ Datasource criado via SQLite: ${datasourceUid} (ID: ${dsId})`)
|
|
432
|
+
} catch (sqliteError) {
|
|
433
|
+
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
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
console.error(`[setupGrafana] ❌ Erro ao verificar datasource: ${error.response?.data?.message || error.message} (status: ${error.response?.status || 'N/A'})`)
|
|
443
|
+
if (error.response?.status === 401) {
|
|
444
|
+
console.log(`[setupGrafana] 🔄 Tentando criar datasource via SQLite (erro 401 na verificação)...`)
|
|
445
|
+
try {
|
|
446
|
+
const { execSync } = require('child_process')
|
|
447
|
+
const dsJson = JSON.stringify(datasourceConfig.jsonData)
|
|
448
|
+
const dsName = escapeForSQLite(datasourceConfig.name)
|
|
449
|
+
const opensearchUrlEscaped = escapeForSQLite(opensearchUrl)
|
|
450
|
+
const dsUidEscaped = escapeForSQLite(datasourceUid)
|
|
451
|
+
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)
|
|
458
|
+
console.log(`[setupGrafana] ✅ Datasource criado via SQLite: ${datasourceUid} (ID: ${dsId})`)
|
|
459
|
+
} catch (sqliteError) {
|
|
460
|
+
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
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.log(`[setupGrafana] 📈 Criando dashboard para ${appName}...`)
|
|
471
|
+
|
|
472
|
+
const appNameLower = appName.toLowerCase()
|
|
473
|
+
const indexFilter = `_index:logs-${appNameLower}`
|
|
474
|
+
|
|
475
|
+
const dashboardConfig = {
|
|
476
|
+
dashboard: {
|
|
477
|
+
title: `Application Health - ${appName}`,
|
|
478
|
+
tags: ['health', 'metrics', appNameLower],
|
|
479
|
+
timezone: 'browser',
|
|
480
|
+
schemaVersion: 38,
|
|
481
|
+
version: 0,
|
|
482
|
+
refresh: '30s',
|
|
483
|
+
time: { from: 'now-24h', to: 'now' },
|
|
484
|
+
editable: false,
|
|
485
|
+
panels: [
|
|
486
|
+
{
|
|
487
|
+
id: 1,
|
|
488
|
+
gridPos: { h: 8, w: 6, x: 0, y: 0 },
|
|
489
|
+
type: 'stat',
|
|
490
|
+
title: 'Total de Requisições',
|
|
491
|
+
datasource: { uid: datasourceUid, type: 'grafana-opensearch-datasource' },
|
|
492
|
+
targets: [{
|
|
493
|
+
refId: 'A',
|
|
494
|
+
query: `message:"[REQUEST]" AND NOT message:"[RESPONSE]" AND ${indexFilter}`,
|
|
495
|
+
bucketAggs: [{
|
|
496
|
+
id: '2',
|
|
497
|
+
type: 'date_histogram',
|
|
498
|
+
field: '@timestamp',
|
|
499
|
+
settings: { interval: 'auto', min_doc_count: 0 }
|
|
500
|
+
}],
|
|
501
|
+
metrics: [{ id: '1', type: 'count' }]
|
|
502
|
+
}],
|
|
503
|
+
fieldConfig: {
|
|
504
|
+
defaults: {
|
|
505
|
+
unit: 'short',
|
|
506
|
+
color: { mode: 'thresholds' },
|
|
507
|
+
custom: {
|
|
508
|
+
reduceOptions: {
|
|
509
|
+
values: false,
|
|
510
|
+
calcs: ['sum']
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
options: {
|
|
516
|
+
reduceOptions: { values: false, calcs: ['sum'], fields: '' },
|
|
517
|
+
orientation: 'auto',
|
|
518
|
+
textMode: 'auto',
|
|
519
|
+
colorMode: 'value',
|
|
520
|
+
graphMode: 'none',
|
|
521
|
+
justifyMode: 'auto'
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
id: 2,
|
|
526
|
+
gridPos: { h: 8, w: 6, x: 6, y: 0 },
|
|
527
|
+
type: 'stat',
|
|
528
|
+
title: 'Taxa de Sucesso (2xx)',
|
|
529
|
+
datasource: { uid: datasourceUid, type: 'grafana-opensearch-datasource' },
|
|
530
|
+
targets: [{
|
|
531
|
+
refId: 'A',
|
|
532
|
+
query: `message:"[RESPONSE]" AND statusCode:[200 TO 299] AND ${indexFilter}`,
|
|
533
|
+
bucketAggs: [{
|
|
534
|
+
id: '2',
|
|
535
|
+
type: 'date_histogram',
|
|
536
|
+
field: '@timestamp',
|
|
537
|
+
settings: { interval: 'auto', min_doc_count: 0 }
|
|
538
|
+
}],
|
|
539
|
+
metrics: [{ id: '1', type: 'count' }]
|
|
540
|
+
}],
|
|
541
|
+
fieldConfig: {
|
|
542
|
+
defaults: {
|
|
543
|
+
unit: 'short',
|
|
544
|
+
color: { mode: 'thresholds', thresholds: { mode: 'absolute', steps: [{ value: null, color: 'green' }] } },
|
|
545
|
+
custom: { reduceOptions: { values: false, calcs: ['sum'] } }
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
options: {
|
|
549
|
+
reduceOptions: { values: false, calcs: ['sum'], fields: '' },
|
|
550
|
+
orientation: 'auto',
|
|
551
|
+
textMode: 'auto',
|
|
552
|
+
colorMode: 'value',
|
|
553
|
+
graphMode: 'none',
|
|
554
|
+
justifyMode: 'auto'
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: 3,
|
|
559
|
+
gridPos: { h: 8, w: 6, x: 12, y: 0 },
|
|
560
|
+
type: 'stat',
|
|
561
|
+
title: 'Erros 4xx',
|
|
562
|
+
datasource: { uid: datasourceUid, type: 'grafana-opensearch-datasource' },
|
|
563
|
+
targets: [{
|
|
564
|
+
refId: 'A',
|
|
565
|
+
query: `message:"[RESPONSE]" AND statusCode:[400 TO 499] AND ${indexFilter}`,
|
|
566
|
+
bucketAggs: [{
|
|
567
|
+
id: '2',
|
|
568
|
+
type: 'date_histogram',
|
|
569
|
+
field: '@timestamp',
|
|
570
|
+
settings: { interval: 'auto', min_doc_count: 0 }
|
|
571
|
+
}],
|
|
572
|
+
metrics: [{ id: '1', type: 'count' }]
|
|
573
|
+
}],
|
|
574
|
+
fieldConfig: {
|
|
575
|
+
defaults: {
|
|
576
|
+
unit: 'short',
|
|
577
|
+
color: { mode: 'thresholds', thresholds: { mode: 'absolute', steps: [{ value: null, color: 'yellow' }] } },
|
|
578
|
+
custom: { reduceOptions: { values: false, calcs: ['sum'] } }
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
options: {
|
|
582
|
+
reduceOptions: { values: false, calcs: ['sum'], fields: '' },
|
|
583
|
+
orientation: 'auto',
|
|
584
|
+
textMode: 'auto',
|
|
585
|
+
colorMode: 'value',
|
|
586
|
+
graphMode: 'none',
|
|
587
|
+
justifyMode: 'auto'
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
id: 4,
|
|
592
|
+
gridPos: { h: 8, w: 6, x: 18, y: 0 },
|
|
593
|
+
type: 'stat',
|
|
594
|
+
title: 'Erros 5xx',
|
|
595
|
+
datasource: { uid: datasourceUid, type: 'grafana-opensearch-datasource' },
|
|
596
|
+
targets: [{
|
|
597
|
+
refId: 'A',
|
|
598
|
+
query: `message:"[RESPONSE]" AND statusCode:[500 TO 599] AND ${indexFilter}`,
|
|
599
|
+
bucketAggs: [{
|
|
600
|
+
id: '2',
|
|
601
|
+
type: 'date_histogram',
|
|
602
|
+
field: '@timestamp',
|
|
603
|
+
settings: { interval: 'auto', min_doc_count: 0 }
|
|
604
|
+
}],
|
|
605
|
+
metrics: [{ id: '1', type: 'count' }]
|
|
606
|
+
}],
|
|
607
|
+
fieldConfig: {
|
|
608
|
+
defaults: {
|
|
609
|
+
unit: 'short',
|
|
610
|
+
color: { mode: 'thresholds', thresholds: { mode: 'absolute', steps: [{ value: null, color: 'red' }] } },
|
|
611
|
+
custom: { reduceOptions: { values: false, calcs: ['sum'] } }
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
options: {
|
|
615
|
+
reduceOptions: { values: false, calcs: ['sum'], fields: '' },
|
|
616
|
+
orientation: 'auto',
|
|
617
|
+
textMode: 'auto',
|
|
618
|
+
colorMode: 'value',
|
|
619
|
+
graphMode: 'none',
|
|
620
|
+
justifyMode: 'auto'
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: 5,
|
|
625
|
+
gridPos: { h: 8, w: 12, x: 0, y: 8 },
|
|
626
|
+
type: 'stat',
|
|
627
|
+
title: 'Tempo Médio de Resposta (ms)',
|
|
628
|
+
datasource: { uid: datasourceUid, type: 'grafana-opensearch-datasource' },
|
|
629
|
+
targets: [{
|
|
630
|
+
refId: 'A',
|
|
631
|
+
query: `message:"[RESPONSE]" AND ${indexFilter}`,
|
|
632
|
+
bucketAggs: [{
|
|
633
|
+
id: '2',
|
|
634
|
+
type: 'date_histogram',
|
|
635
|
+
field: '@timestamp',
|
|
636
|
+
settings: { interval: 'auto', min_doc_count: 0 }
|
|
637
|
+
}],
|
|
638
|
+
metrics: [{ id: '1', type: 'avg', field: 'responseTime' }]
|
|
639
|
+
}],
|
|
640
|
+
fieldConfig: {
|
|
641
|
+
defaults: {
|
|
642
|
+
unit: 'ms',
|
|
643
|
+
color: { mode: 'thresholds' },
|
|
644
|
+
custom: { reduceOptions: { values: false, calcs: ['mean'] } }
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
options: {
|
|
648
|
+
reduceOptions: { values: false, calcs: ['mean'], fields: '' },
|
|
649
|
+
orientation: 'auto',
|
|
650
|
+
textMode: 'auto',
|
|
651
|
+
colorMode: 'value',
|
|
652
|
+
graphMode: 'none',
|
|
653
|
+
justifyMode: 'auto'
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
id: 6,
|
|
658
|
+
gridPos: { h: 8, w: 12, x: 12, y: 8 },
|
|
659
|
+
type: 'timeseries',
|
|
660
|
+
title: 'Requisições por Minuto',
|
|
661
|
+
datasource: { uid: datasourceUid, type: 'grafana-opensearch-datasource' },
|
|
662
|
+
targets: [{
|
|
663
|
+
refId: 'A',
|
|
664
|
+
query: `message:"[REQUEST]" AND NOT message:"[RESPONSE]" AND ${indexFilter}`,
|
|
665
|
+
bucketAggs: [{
|
|
666
|
+
id: '2',
|
|
667
|
+
type: 'date_histogram',
|
|
668
|
+
field: '@timestamp',
|
|
669
|
+
settings: { interval: '1m', min_doc_count: 0 }
|
|
670
|
+
}],
|
|
671
|
+
metrics: [{ id: '1', type: 'count' }]
|
|
672
|
+
}]
|
|
673
|
+
}
|
|
674
|
+
]
|
|
675
|
+
},
|
|
676
|
+
overwrite: true
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const searchResponse = await axios.get(`${grafanaUrl}/api/search?query=Application Health - ${appName}&type=dash-db`, {
|
|
681
|
+
auth,
|
|
682
|
+
headers: { 'X-Grafana-Org-Id': org.id }
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
if (searchResponse.data && searchResponse.data.length > 0) {
|
|
686
|
+
const existingDashboard = searchResponse.data[0]
|
|
687
|
+
const dashboardDetail = await axios.get(`${grafanaUrl}/api/dashboards/uid/${existingDashboard.uid}`, {
|
|
688
|
+
auth,
|
|
689
|
+
headers: { 'X-Grafana-Org-Id': org.id }
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
dashboardConfig.dashboard.id = dashboardDetail.data.dashboard.id
|
|
693
|
+
dashboardConfig.dashboard.uid = dashboardDetail.data.dashboard.uid
|
|
694
|
+
dashboardConfig.dashboard.version = dashboardDetail.data.dashboard.version
|
|
695
|
+
}
|
|
696
|
+
} catch (searchError) {
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
const dashboardResponse = await axios.post(
|
|
701
|
+
`${grafanaUrl}/api/dashboards/db`,
|
|
702
|
+
dashboardConfig,
|
|
703
|
+
{
|
|
704
|
+
auth,
|
|
705
|
+
headers: { 'X-Grafana-Org-Id': org.id, 'Content-Type': 'application/json' }
|
|
706
|
+
}
|
|
707
|
+
)
|
|
708
|
+
console.log(`[setupGrafana] ✅ Dashboard criado: ${dashboardResponse.data?.dashboard?.title || 'N/A'} (UID: ${dashboardResponse.data?.dashboard?.uid || 'N/A'})`)
|
|
709
|
+
} catch (error) {
|
|
710
|
+
console.error(`[setupGrafana] ❌ Erro ao criar dashboard via API: ${error.response?.data?.message || error.message}`)
|
|
711
|
+
if (error.response?.status === 401 && typeof dashboardConfig !== 'undefined' && dashboardConfig && dashboardConfig.dashboard) {
|
|
712
|
+
console.log(`[setupGrafana] 🔄 Tentando criar dashboard via SQLite...`)
|
|
713
|
+
try {
|
|
714
|
+
const { execSync } = require('child_process')
|
|
715
|
+
const dashboardUid = `dashboard-${appName.toLowerCase()}`
|
|
716
|
+
const dashboardTitle = `Application Health - ${appName}`
|
|
717
|
+
const dashboardJson = JSON.stringify(dashboardConfig.dashboard)
|
|
718
|
+
const escapedJson = dashboardJson.replace(/'/g, "''")
|
|
719
|
+
const escapedTitle = escapeForSQLite(dashboardTitle)
|
|
720
|
+
const slug = dashboardTitle.toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
721
|
+
const slugEscaped = escapeForSQLite(slug)
|
|
722
|
+
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)
|
|
729
|
+
console.log(`[setupGrafana] ✅ Dashboard criado via SQLite: ${dashboardTitle} (ID: ${dashboardId})`)
|
|
730
|
+
} catch (sqliteError) {
|
|
731
|
+
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
|
+
}
|
|
737
|
+
} else {
|
|
738
|
+
console.error(`[setupGrafana] ⚠️ Não foi possível criar dashboard via SQLite: dashboardConfig não disponível`)
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
console.log(`[setupGrafana] ✅ Setup completo para ${appName} (Organization, Datasource e Dashboard)`)
|
|
743
|
+
|
|
744
|
+
setTimeout(() => {
|
|
745
|
+
setupGrafanaForApp._cache.delete(appName)
|
|
746
|
+
}, 3600000)
|
|
747
|
+
|
|
748
|
+
} catch (error) {
|
|
749
|
+
setupGrafanaForApp._cache.delete(appName)
|
|
750
|
+
const errorMsg = error.response?.data?.message || error.message || 'Erro desconhecido'
|
|
751
|
+
console.error(`[setupGrafana] ❌ Erro no setup de ${appName}: ${errorMsg}`)
|
|
752
|
+
if (error.stack) {
|
|
753
|
+
console.error(`[setupGrafana] Stack: ${error.stack.substring(0, 300)}`)
|
|
754
|
+
}
|
|
755
|
+
throw new Error(errorMsg)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
155
759
|
function generateTraceId() {
|
|
156
760
|
return Math.random().toString(36).substring(2, 15) +
|
|
157
761
|
Math.random().toString(36).substring(2, 15)
|
|
@@ -184,28 +788,78 @@ function getOrCreateTraceContext(requestId) {
|
|
|
184
788
|
app.get('/health', (req, res) => {
|
|
185
789
|
res.json({
|
|
186
790
|
status: 'ok',
|
|
187
|
-
service: 'azify-logger'
|
|
791
|
+
service: 'azify-logger',
|
|
792
|
+
authEnabled
|
|
188
793
|
})
|
|
189
794
|
})
|
|
190
795
|
|
|
191
|
-
app.get('/', (req, res) => {
|
|
796
|
+
app.get('/', ensureAuthenticated, (req, res) => {
|
|
192
797
|
res.json({
|
|
193
798
|
service: 'azify-logger',
|
|
194
799
|
version: '1.0.0',
|
|
195
800
|
endpoints: {
|
|
196
801
|
health: '/health',
|
|
197
|
-
testLog: '/test-log'
|
|
802
|
+
testLog: '/test-log',
|
|
198
803
|
}
|
|
199
804
|
})
|
|
200
805
|
})
|
|
201
806
|
|
|
202
|
-
|
|
807
|
+
|
|
808
|
+
function decodeHtmlEntities(str) {
|
|
809
|
+
if (!str || typeof str !== 'string') return str
|
|
810
|
+
let decoded = str
|
|
811
|
+
let prev = ''
|
|
812
|
+
let iterations = 0
|
|
813
|
+
const maxIterations = 10
|
|
814
|
+
|
|
815
|
+
const namedEntities = {
|
|
816
|
+
'"': '"',
|
|
817
|
+
''': "'",
|
|
818
|
+
'<': '<',
|
|
819
|
+
'>': '>',
|
|
820
|
+
'&': '&',
|
|
821
|
+
' ': ' '
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
while (decoded !== prev && iterations < maxIterations) {
|
|
825
|
+
prev = decoded
|
|
826
|
+
Object.keys(namedEntities).forEach(entity => {
|
|
827
|
+
if (entity !== '&') {
|
|
828
|
+
decoded = decoded.replace(new RegExp(entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), namedEntities[entity])
|
|
829
|
+
}
|
|
830
|
+
})
|
|
831
|
+
decoded = decoded.replace(/&/gi, '&')
|
|
832
|
+
decoded = decoded.replace(/&#x([0-9a-fA-F]+);/gi, (match, hex) => {
|
|
833
|
+
try {
|
|
834
|
+
return String.fromCharCode(parseInt(hex, 16))
|
|
835
|
+
} catch (e) {
|
|
836
|
+
return match
|
|
837
|
+
}
|
|
838
|
+
})
|
|
839
|
+
decoded = decoded.replace(/&#([0-9]+);/g, (match, dec) => {
|
|
840
|
+
try {
|
|
841
|
+
return String.fromCharCode(parseInt(dec, 10))
|
|
842
|
+
} catch (e) {
|
|
843
|
+
return match
|
|
844
|
+
}
|
|
845
|
+
})
|
|
846
|
+
iterations++
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return decoded
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function handleLog(req, res) {
|
|
203
853
|
let { level, message, meta } = req.body
|
|
204
854
|
|
|
205
855
|
if (!level || !message) {
|
|
206
856
|
return res.status(400).json({ success: false, message: 'Level and message are required.' })
|
|
207
857
|
}
|
|
208
858
|
|
|
859
|
+
if (typeof message === 'string') {
|
|
860
|
+
message = decodeHtmlEntities(message)
|
|
861
|
+
}
|
|
862
|
+
|
|
209
863
|
const shouldFilterLog = (
|
|
210
864
|
message.includes('prisma:query') ||
|
|
211
865
|
message.includes('prisma:info') ||
|
|
@@ -288,43 +942,84 @@ app.post('/log', async (req, res) => {
|
|
|
288
942
|
if (meta) {
|
|
289
943
|
Object.keys(meta).forEach(key => {
|
|
290
944
|
if (!['timestamp', 'service', 'environment', 'hostname', 'traceId', 'spanId', 'parentSpanId'].includes(key)) {
|
|
291
|
-
|
|
945
|
+
let value = meta[key]
|
|
946
|
+
|
|
947
|
+
if (typeof value === 'string') {
|
|
948
|
+
if (key === 'url' || key === 'path' || key === 'baseUrl' || key === 'message') {
|
|
949
|
+
value = decodeHtmlEntities(value)
|
|
950
|
+
} else if (value.includes('&#') || value.includes('&')) {
|
|
951
|
+
value = decodeHtmlEntities(value)
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (key === 'responseBody') {
|
|
956
|
+
if (typeof value === 'object' && value !== null) {
|
|
957
|
+
try {
|
|
958
|
+
value = JSON.stringify(value)
|
|
959
|
+
} catch (e) {
|
|
960
|
+
return
|
|
961
|
+
}
|
|
962
|
+
} else if (typeof value === 'string' && value.length > 10000) {
|
|
963
|
+
value = value.substring(0, 10000) + '... [truncated]'
|
|
964
|
+
}
|
|
965
|
+
if (typeof value === 'object' && value !== null) {
|
|
966
|
+
return
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
logEntry[key] = value
|
|
292
971
|
}
|
|
293
972
|
})
|
|
294
973
|
}
|
|
295
974
|
|
|
975
|
+
logEntry.message = message
|
|
976
|
+
|
|
296
977
|
try {
|
|
297
978
|
const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
|
|
298
|
-
const
|
|
979
|
+
const appName = logEntry.appName || logEntry.service?.name || 'unknown'
|
|
980
|
+
const serviceName = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
299
981
|
const indexName = `logs-${serviceName}`
|
|
300
982
|
|
|
301
983
|
await axios.post(`${osUrl}/${indexName}/_doc`, logEntry, {
|
|
302
984
|
headers: { 'Content-Type': 'application/json' }
|
|
303
985
|
})
|
|
304
986
|
|
|
305
|
-
console.log(
|
|
987
|
+
console.log(`[setupGrafana] serviceName: ${serviceName}, appName: ${appName}`)
|
|
988
|
+
|
|
989
|
+
if (serviceName !== 'unknown' && serviceName !== 'unknown-service') {
|
|
990
|
+
console.log(`[setupGrafana] Iniciando setup para app: ${serviceName}`)
|
|
991
|
+
setupGrafanaForApp(serviceName).then(() => {
|
|
992
|
+
console.log(`[setupGrafana] ✅ Setup concluído para ${serviceName}`)
|
|
993
|
+
}).catch((err) => {
|
|
994
|
+
const errorMsg = err?.response?.data?.message || err?.message || 'Erro desconhecido'
|
|
995
|
+
const status = err?.response?.status || 'N/A'
|
|
996
|
+
console.error(`[setupGrafana] ❌ Erro ao configurar Grafana para ${serviceName}: ${errorMsg} (status: ${status})`)
|
|
997
|
+
if (err?.stack) {
|
|
998
|
+
console.error(`[setupGrafana] Stack: ${err.stack.substring(0, 200)}`)
|
|
999
|
+
}
|
|
1000
|
+
})
|
|
1001
|
+
} else {
|
|
1002
|
+
console.log(`[setupGrafana] ⚠️ Pulando setup para serviceName inválido: ${serviceName}`)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
306
1005
|
res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
|
|
307
1006
|
} catch (error) {
|
|
308
|
-
console.error('❌ Erro ao enviar log para OpenSearch:', error.message)
|
|
309
|
-
if (error.response) {
|
|
310
|
-
console.error(' Detalhes:', error.response.data)
|
|
311
|
-
}
|
|
312
1007
|
res.status(500).json({ success: false, message: 'Erro ao enviar log para OpenSearch' })
|
|
313
1008
|
}
|
|
314
|
-
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
app.post('/log', (req, res) => handleLog(req, res))
|
|
1012
|
+
app.post('/send', (req, res) => handleLog(req, res))
|
|
315
1013
|
|
|
316
1014
|
const port = process.env.PORT || 3001
|
|
317
1015
|
|
|
318
|
-
app.listen(port
|
|
319
|
-
console.log(`🚀 Azify Logger rodando na porta ${port}`)
|
|
320
|
-
})
|
|
1016
|
+
app.listen(port)
|
|
321
1017
|
|
|
322
1018
|
process.on('SIGTERM', () => {
|
|
323
|
-
console.log('Received SIGTERM, shutting down')
|
|
324
1019
|
process.exit(0)
|
|
325
1020
|
})
|
|
326
1021
|
|
|
327
1022
|
process.on('SIGINT', () => {
|
|
328
|
-
console.log('Received SIGINT, shutting down')
|
|
329
1023
|
process.exit(0)
|
|
330
1024
|
})
|
|
1025
|
+
|