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.
Files changed (4) hide show
  1. package/README.md +86 -241
  2. package/package.json +30 -8
  3. package/register.js +149 -0
  4. 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
- settings: {
101
- number_of_shards: 1,
106
+ settings: {
107
+ number_of_shards: 1,
102
108
  number_of_replicas: 0,
103
109
  'index.refresh_interval': '5s'
104
- },
105
- mappings: {
106
- properties: {
110
+ },
111
+ mappings: {
112
+ properties: {
107
113
  '@timestamp': { type: 'date' },
108
- level: { type: 'keyword' },
109
- message: { type: 'text' },
110
- service: {
111
- properties: {
112
- name: { type: 'keyword' },
113
- version: { type: 'keyword' }
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
- appName: { type: 'keyword' },
117
- traceId: { type: 'keyword' },
118
- spanId: { type: 'keyword' },
119
- parentSpanId: { type: 'keyword' },
120
- userId: { type: 'keyword' },
121
- requestId: { type: 'keyword' },
122
- method: { type: 'keyword' },
123
- url: { type: 'keyword' },
124
- statusCode: { type: 'integer' },
125
- responseTime: { type: 'float' },
126
- ip: { type: 'ip' },
127
- userAgent: { type: 'text' },
128
- environment: { type: 'keyword' },
129
- hostname: { type: 'keyword' },
130
- responseBody: { type: 'text' },
131
- error: {
132
- properties: {
133
- message: { type: 'text' },
134
- stack: { type: 'text' },
135
- name: { type: 'keyword' }
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
- app.post('/log', async (req, res) => {
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
+ '&quot;': '"',
817
+ '&apos;': "'",
818
+ '&lt;': '<',
819
+ '&gt;': '>',
820
+ '&amp;': '&',
821
+ '&nbsp;': ' '
822
+ }
823
+
824
+ while (decoded !== prev && iterations < maxIterations) {
825
+ prev = decoded
826
+ Object.keys(namedEntities).forEach(entity => {
827
+ if (entity !== '&amp;') {
828
+ decoded = decoded.replace(new RegExp(entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), namedEntities[entity])
829
+ }
830
+ })
831
+ decoded = decoded.replace(/&amp;/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
- logEntry[key] = meta[key]
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('&amp;')) {
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 serviceName = (logEntry.service.name || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g, '-')
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(`✅ [${level.toUpperCase()}] ${message} | traceId: ${traceContext.traceId.substring(0, 8)}... | service: ${logEntry.service.name} | index: ${indexName}`)
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
+