azify-logger 1.0.41 → 1.0.43
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/middleware-express.d.ts +2 -0
- package/middleware-express.js +41 -1
- package/middleware-restify.js +42 -1
- package/package.json +1 -1
- package/queue/redisQueue.js +22 -10
- package/queue/workerManager.js +11 -4
- package/register-otel.js +7 -1
- package/scripts/redis-worker.js +4 -2
- package/server.js +244 -79
- package/streams/httpQueue.js +12 -8
- package/trace-export.js +6 -1
package/middleware-express.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ declare function createExpressLoggingMiddleware(options?: {
|
|
|
15
15
|
serviceName?: string;
|
|
16
16
|
loggerUrl?: string;
|
|
17
17
|
environment?: string;
|
|
18
|
+
otelEndpoint?: string;
|
|
19
|
+
otelExporterEndpoint?: string;
|
|
18
20
|
}): ExpressMiddleware;
|
|
19
21
|
|
|
20
22
|
export = createExpressLoggingMiddleware;
|
package/middleware-express.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require('./otel-env')
|
|
2
2
|
const { startRequestContext, runWithRequestContext, getRequestContext } = require('./store')
|
|
3
3
|
const { createHttpLoggerTransport } = require('./streams/httpQueue')
|
|
4
|
+
const { sendSpanToOtel, setEndpointOverride } = require('./trace-export')
|
|
4
5
|
const { Worker } = require('worker_threads')
|
|
5
6
|
const path = require('path')
|
|
6
7
|
const os = require('os')
|
|
@@ -93,8 +94,17 @@ function pickHeaders(source) {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
function createExpressLoggingMiddleware(options = {}) {
|
|
97
|
+
const svcName = options.serviceName || process.env.APP_NAME
|
|
98
|
+
if (svcName) {
|
|
99
|
+
process.env.OTEL_SERVICE_NAME = svcName
|
|
100
|
+
process.env.SERVICE_NAME = svcName
|
|
101
|
+
}
|
|
102
|
+
const otelEndpoint = options.otelEndpoint || options.otelExporterEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|
|
103
|
+
if (otelEndpoint) {
|
|
104
|
+
setEndpointOverride(otelEndpoint)
|
|
105
|
+
}
|
|
96
106
|
const config = {
|
|
97
|
-
serviceName:
|
|
107
|
+
serviceName: svcName,
|
|
98
108
|
loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
|
|
99
109
|
environment: options.environment || process.env.NODE_ENV,
|
|
100
110
|
captureResponseBody: options.captureResponseBody !== false && process.env.AZIFY_LOGGER_CAPTURE_RESPONSE_BODY !== 'false',
|
|
@@ -354,6 +364,21 @@ function createExpressLoggingMiddleware(options = {}) {
|
|
|
354
364
|
|
|
355
365
|
const chunkToProcess = (responseChunk !== null && responseChunkCaptured) ? responseChunk : null
|
|
356
366
|
emitResponseLog(meta, chunkToProcess)
|
|
367
|
+
if (meta.traceId && meta.spanId && config.serviceName) {
|
|
368
|
+
const startNs = BigInt(startTime) * BigInt(1e6)
|
|
369
|
+
const endNs = BigInt(meta.timestamp || Date.now()) * BigInt(1e6)
|
|
370
|
+
sendSpanToOtel({
|
|
371
|
+
traceId: meta.traceId,
|
|
372
|
+
spanId: meta.spanId,
|
|
373
|
+
serviceName: config.serviceName,
|
|
374
|
+
name: `request handler - ${path || url}`,
|
|
375
|
+
startTimeNs: startNs,
|
|
376
|
+
endTimeNs: endNs,
|
|
377
|
+
statusCode: res.statusCode || 200,
|
|
378
|
+
method,
|
|
379
|
+
url
|
|
380
|
+
})
|
|
381
|
+
}
|
|
357
382
|
} catch (err) {
|
|
358
383
|
try {
|
|
359
384
|
ensureIds()
|
|
@@ -380,6 +405,21 @@ function createExpressLoggingMiddleware(options = {}) {
|
|
|
380
405
|
if (serviceObj) meta.service = serviceObj
|
|
381
406
|
if (config.environment) meta.environment = config.environment
|
|
382
407
|
emitResponseLog(meta, null)
|
|
408
|
+
if (meta.traceId && meta.spanId && config.serviceName) {
|
|
409
|
+
const startNs = BigInt(startTime) * BigInt(1e6)
|
|
410
|
+
const endNs = BigInt(meta.timestamp || Date.now()) * BigInt(1e6)
|
|
411
|
+
sendSpanToOtel({
|
|
412
|
+
traceId: meta.traceId,
|
|
413
|
+
spanId: meta.spanId,
|
|
414
|
+
serviceName: config.serviceName,
|
|
415
|
+
name: `request handler - ${path || url}`,
|
|
416
|
+
startTimeNs: startNs,
|
|
417
|
+
endTimeNs: endNs,
|
|
418
|
+
statusCode: statusCode || 200,
|
|
419
|
+
method,
|
|
420
|
+
url
|
|
421
|
+
})
|
|
422
|
+
}
|
|
383
423
|
} catch (_) {
|
|
384
424
|
}
|
|
385
425
|
}
|
package/middleware-restify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require('./otel-env')
|
|
2
2
|
const { startRequestContext, runWithRequestContext, getRequestContext, als } = require('./store')
|
|
3
3
|
const { createHttpLoggerTransport } = require('./streams/httpQueue')
|
|
4
|
+
const { sendSpanToOtel, setEndpointOverride } = require('./trace-export')
|
|
4
5
|
const os = require('os')
|
|
5
6
|
|
|
6
7
|
let trace, otelContext, otelRootContext
|
|
@@ -92,8 +93,17 @@ function pickHeaders (source) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
function createRestifyLoggingMiddleware (options = {}) {
|
|
96
|
+
const svcName = options.serviceName || process.env.APP_NAME
|
|
97
|
+
if (svcName) {
|
|
98
|
+
process.env.OTEL_SERVICE_NAME = svcName
|
|
99
|
+
process.env.SERVICE_NAME = svcName
|
|
100
|
+
}
|
|
101
|
+
const otelEndpoint = options.otelEndpoint || options.otelExporterEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|
|
102
|
+
if (otelEndpoint) {
|
|
103
|
+
setEndpointOverride(otelEndpoint)
|
|
104
|
+
}
|
|
95
105
|
const config = {
|
|
96
|
-
serviceName:
|
|
106
|
+
serviceName: svcName,
|
|
97
107
|
loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001/log',
|
|
98
108
|
environment: options.environment || process.env.NODE_ENV,
|
|
99
109
|
logRequest: options.logRequest !== false && process.env.AZIFY_LOGGER_LOG_REQUEST !== 'false'
|
|
@@ -266,6 +276,22 @@ function createRestifyLoggingMiddleware (options = {}) {
|
|
|
266
276
|
}
|
|
267
277
|
|
|
268
278
|
sendLog(level, message, meta)
|
|
279
|
+
if (meta.traceId && meta.spanId && config.serviceName) {
|
|
280
|
+
const startNs = BigInt(startTime) * BigInt(1e6)
|
|
281
|
+
const endNs = BigInt(Date.now()) * BigInt(1e6)
|
|
282
|
+
const statusCode = meta.response?.statusCode ?? 200
|
|
283
|
+
sendSpanToOtel({
|
|
284
|
+
traceId: meta.traceId,
|
|
285
|
+
spanId: meta.spanId,
|
|
286
|
+
serviceName: config.serviceName,
|
|
287
|
+
name: `request handler - ${requestSnapshot?.baseUrl || requestSnapshot?.url || ''}`,
|
|
288
|
+
startTimeNs: startNs,
|
|
289
|
+
endTimeNs: endNs,
|
|
290
|
+
statusCode,
|
|
291
|
+
method: requestSnapshot?.method,
|
|
292
|
+
url: requestSnapshot?.url
|
|
293
|
+
})
|
|
294
|
+
}
|
|
269
295
|
} catch (err) {
|
|
270
296
|
const errPayload = {
|
|
271
297
|
traceId: reqCtx.traceId,
|
|
@@ -282,6 +308,21 @@ function createRestifyLoggingMiddleware (options = {}) {
|
|
|
282
308
|
setImmediate(() => {
|
|
283
309
|
try {
|
|
284
310
|
sendLog('error', (requestSnapshot && (requestSnapshot.method + ' ' + requestSnapshot.url)) || 'request', errPayload)
|
|
311
|
+
if (errPayload.traceId && errPayload.spanId && config.serviceName) {
|
|
312
|
+
const startNs = BigInt(startTime) * BigInt(1e6)
|
|
313
|
+
const endNs = BigInt(Date.now()) * BigInt(1e6)
|
|
314
|
+
sendSpanToOtel({
|
|
315
|
+
traceId: errPayload.traceId,
|
|
316
|
+
spanId: errPayload.spanId,
|
|
317
|
+
serviceName: config.serviceName,
|
|
318
|
+
name: `request handler - ${requestSnapshot?.baseUrl || requestSnapshot?.url || ''}`,
|
|
319
|
+
startTimeNs: startNs,
|
|
320
|
+
endTimeNs: endNs,
|
|
321
|
+
statusCode: 500,
|
|
322
|
+
method: requestSnapshot?.method,
|
|
323
|
+
url: requestSnapshot?.url
|
|
324
|
+
})
|
|
325
|
+
}
|
|
285
326
|
} catch (_) {}
|
|
286
327
|
})
|
|
287
328
|
}
|
package/package.json
CHANGED
package/queue/redisQueue.js
CHANGED
|
@@ -30,9 +30,7 @@ function createRedisProducer(config = {}) {
|
|
|
30
30
|
const maxLen = Number.isFinite(config.maxLen) ? config.maxLen : DEFAULT_MAXLEN
|
|
31
31
|
const password = config.password ?? process.env.AZIFY_LOGGER_REDIS_PASSWORD
|
|
32
32
|
const pass = password != null && String(password).trim() !== '' ? String(password).trim() : null
|
|
33
|
-
|
|
34
|
-
throw new Error('Redis requires a password. Set AZIFY_LOGGER_REDIS_PASSWORD (or options.redis.password).')
|
|
35
|
-
}
|
|
33
|
+
const onUnrecoverableError = typeof config.onUnrecoverableError === 'function' ? config.onUnrecoverableError : null
|
|
36
34
|
|
|
37
35
|
const redisOptions = { ...defaultRedisOptions, ...(config.redisOptions || {}) }
|
|
38
36
|
if (pass) {
|
|
@@ -43,13 +41,9 @@ function createRedisProducer(config = {}) {
|
|
|
43
41
|
let lastConnectionErrorLog = 0
|
|
44
42
|
let lastEnqueueErrorLog = 0
|
|
45
43
|
let connectionErrorCount = 0
|
|
44
|
+
let unrecoverableNotified = false
|
|
46
45
|
const ERROR_LOG_INTERVAL = 300000
|
|
47
|
-
|
|
48
|
-
if (typeof global.__azifyLoggerRedisErrorLogged === 'undefined') {
|
|
49
|
-
global.__azifyLoggerRedisErrorLogged = false
|
|
50
|
-
global.__azifyLoggerRedisErrorLastLog = 0
|
|
51
|
-
}
|
|
52
|
-
|
|
46
|
+
|
|
53
47
|
function isRedisAuthError(err) {
|
|
54
48
|
if (!err) return false
|
|
55
49
|
const code = err.code || ''
|
|
@@ -65,8 +59,26 @@ function createRedisProducer(config = {}) {
|
|
|
65
59
|
return isRedisAuthError(err)
|
|
66
60
|
}
|
|
67
61
|
|
|
62
|
+
function notifyUnrecoverableOnce() {
|
|
63
|
+
if (unrecoverableNotified) return
|
|
64
|
+
unrecoverableNotified = true
|
|
65
|
+
if (typeof global.__azifyLoggerRedisErrorLogged === 'undefined') {
|
|
66
|
+
global.__azifyLoggerRedisErrorLogged = false
|
|
67
|
+
global.__azifyLoggerRedisErrorLastLog = 0
|
|
68
|
+
}
|
|
69
|
+
global.__azifyLoggerRedisErrorLogged = true
|
|
70
|
+
global.__azifyLoggerRedisErrorLastLog = Date.now()
|
|
71
|
+
process.stderr.write('[azify-logger] Redis unavailable or auth failed. Using direct HTTP (no Redis). Continuing.\n')
|
|
72
|
+
if (onUnrecoverableError) {
|
|
73
|
+
try { onUnrecoverableError() } catch (_) {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
client.on('error', (err) => {
|
|
69
|
-
if (isRedisAuthError(err))
|
|
78
|
+
if (isRedisAuthError(err) || isRedisUnavailable(err)) {
|
|
79
|
+
notifyUnrecoverableOnce()
|
|
80
|
+
return
|
|
81
|
+
}
|
|
70
82
|
const now = Date.now()
|
|
71
83
|
if (!global.__azifyLoggerRedisErrorLogged && now - global.__azifyLoggerRedisErrorLastLog > ERROR_LOG_INTERVAL) {
|
|
72
84
|
if (isRedisUnavailable(err)) {
|
package/queue/workerManager.js
CHANGED
|
@@ -17,9 +17,16 @@ function buildEnv(redisConfig = {}, extraEnv = {}) {
|
|
|
17
17
|
if (redisConfig.streamKey) env.AZIFY_LOGGER_REDIS_STREAM = String(redisConfig.streamKey)
|
|
18
18
|
if (redisConfig.streamKey) env.AZIFY_LOGGER_REDIS_QUEUE_KEY = String(redisConfig.streamKey)
|
|
19
19
|
if (redisConfig.maxLen != null) env.AZIFY_LOGGER_REDIS_MAX_STREAM_LENGTH = String(redisConfig.maxLen)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const configPass = redisConfig.password != null && String(redisConfig.password).trim() !== ''
|
|
21
|
+
? String(redisConfig.password).trim()
|
|
22
|
+
: null
|
|
23
|
+
const envPass = process.env.AZIFY_LOGGER_REDIS_PASSWORD != null && String(process.env.AZIFY_LOGGER_REDIS_PASSWORD).trim() !== ''
|
|
24
|
+
? String(process.env.AZIFY_LOGGER_REDIS_PASSWORD).trim()
|
|
25
|
+
: null
|
|
26
|
+
const password = configPass || envPass
|
|
27
|
+
if (password) env.AZIFY_LOGGER_REDIS_PASSWORD = password
|
|
28
|
+
else if (process.env.AZIFY_LOGGER_REDIS_PASSWORD != null && String(process.env.AZIFY_LOGGER_REDIS_PASSWORD).trim() !== '')
|
|
29
|
+
env.AZIFY_LOGGER_REDIS_PASSWORD = String(process.env.AZIFY_LOGGER_REDIS_PASSWORD).trim()
|
|
23
30
|
|
|
24
31
|
return env
|
|
25
32
|
}
|
|
@@ -77,7 +84,7 @@ function spawnWorker(redisConfig, options = {}) {
|
|
|
77
84
|
if (code === EXIT_AUTH_FAILURE) {
|
|
78
85
|
if (typeof global.__azifyLoggerWorkerAuthExitLogged === 'undefined') {
|
|
79
86
|
global.__azifyLoggerWorkerAuthExitLogged = true
|
|
80
|
-
process.stderr.write('[azify-logger] Redis
|
|
87
|
+
process.stderr.write('[azify-logger] Redis unavailable or auth failed. Worker exited once (no restart). Using direct HTTP.\n')
|
|
81
88
|
}
|
|
82
89
|
const fns = authFailureCallbacks.splice(0, authFailureCallbacks.length)
|
|
83
90
|
fns.forEach((fn) => { try { fn() } catch (_) {} })
|
package/register-otel.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
function normalizeServiceNameForTraces(s) {
|
|
2
|
+
const t = String(s || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
3
|
+
return t && t !== 'unknown' && t !== 'unknown-service' ? t : 'default'
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
try {
|
|
2
7
|
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
3
8
|
const { AlwaysOnSampler } = require('@opentelemetry/core')
|
|
@@ -7,7 +12,8 @@ try {
|
|
|
7
12
|
const { Resource } = require('@opentelemetry/resources')
|
|
8
13
|
const { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions')
|
|
9
14
|
|
|
10
|
-
const
|
|
15
|
+
const rawName = process.env.APP_NAME || process.env.OTEL_SERVICE_NAME || process.env.SERVICE_NAME || 'app'
|
|
16
|
+
const serviceName = normalizeServiceNameForTraces(rawName)
|
|
11
17
|
const serviceVersion = process.env.OTEL_SERVICE_VERSION || '1.0.0'
|
|
12
18
|
|
|
13
19
|
process.env.OTEL_SERVICE_NAME = serviceName
|
package/scripts/redis-worker.js
CHANGED
|
@@ -488,8 +488,10 @@ ensureGroup()
|
|
|
488
488
|
}
|
|
489
489
|
return consumeLoop()
|
|
490
490
|
}
|
|
491
|
-
const
|
|
492
|
-
if (
|
|
491
|
+
const redisUnavailable = isRedisAuthError(err) || isRedisUnavailable(err) || /noauth|wrongpass|invalid password|authentication required|econnrefused/i.test(errMsg)
|
|
492
|
+
if (redisUnavailable) {
|
|
493
|
+
process.exit(2)
|
|
494
|
+
}
|
|
493
495
|
console.error('[azify-logger][worker] failed to start:', errMsg)
|
|
494
496
|
process.exit(1)
|
|
495
497
|
})
|
package/server.js
CHANGED
|
@@ -51,24 +51,24 @@ function isPrivateOrLocalhost(ip) {
|
|
|
51
51
|
if (IS_LOCAL) {
|
|
52
52
|
return true
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
ip = ip.replace('::ffff:', '').replace('::1', '127.0.0.1')
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
if (ip === '127.0.0.1' || ip === 'localhost' || ip === '::1') {
|
|
58
58
|
return true
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
const parts = ip.split('.').map(Number)
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
if (parts.length !== 4) {
|
|
64
64
|
return false
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
if (parts[0] === 10) return true
|
|
68
68
|
if (parts[0] === 127) return true
|
|
69
69
|
if (parts[0] === 192 && parts[1] === 168) return true
|
|
70
70
|
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
return false
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -76,13 +76,13 @@ function validateNetworkAccess(req, res, next) {
|
|
|
76
76
|
if (req.path === '/health' || req.path === '/' || req.path === '/v1/traces') {
|
|
77
77
|
return next()
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
|
81
|
-
req.headers['x-real-ip'] ||
|
|
82
|
-
req.ip ||
|
|
81
|
+
req.headers['x-real-ip'] ||
|
|
82
|
+
req.ip ||
|
|
83
83
|
req.connection.remoteAddress ||
|
|
84
84
|
req.socket.remoteAddress
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
if (
|
|
87
87
|
!IS_LOCAL &&
|
|
88
88
|
!isPrivateOrLocalhost(clientIP) &&
|
|
@@ -94,7 +94,7 @@ function validateNetworkAccess(req, res, next) {
|
|
|
94
94
|
clientIP: clientIP
|
|
95
95
|
})
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
next()
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -108,7 +108,7 @@ const traceContextMap = new Map()
|
|
|
108
108
|
async function ensureIndexTemplate() {
|
|
109
109
|
const templateName = 'logs-template'
|
|
110
110
|
const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
|
|
111
|
-
|
|
111
|
+
|
|
112
112
|
try {
|
|
113
113
|
await axios.put(`${osUrl}/_index_template/${templateName}`, {
|
|
114
114
|
index_patterns: ['logs-*'],
|
|
@@ -120,7 +120,8 @@ async function ensureIndexTemplate() {
|
|
|
120
120
|
},
|
|
121
121
|
mappings: {
|
|
122
122
|
properties: {
|
|
123
|
-
'@timestamp': { type: 'date' },
|
|
123
|
+
'@timestamp': { type: 'date', format: 'epoch_millis' },
|
|
124
|
+
time: { type: 'date' },
|
|
124
125
|
level: { type: 'text', fields: { keyword: { type: 'keyword' } } },
|
|
125
126
|
message: { type: 'text' },
|
|
126
127
|
service: {
|
|
@@ -129,7 +130,7 @@ async function ensureIndexTemplate() {
|
|
|
129
130
|
version: { type: 'keyword' }
|
|
130
131
|
}
|
|
131
132
|
},
|
|
132
|
-
appName: {
|
|
133
|
+
appName: {
|
|
133
134
|
type: 'keyword',
|
|
134
135
|
fields: {
|
|
135
136
|
keyword: { type: 'keyword' }
|
|
@@ -174,6 +175,99 @@ async function ensureIndexTemplate() {
|
|
|
174
175
|
|
|
175
176
|
ensureIndexTemplate()
|
|
176
177
|
|
|
178
|
+
const { generateOtelCollectorConfig } = require('./otel-collector-config-generator')
|
|
179
|
+
const seenServiceNames = new Set()
|
|
180
|
+
|
|
181
|
+
function addSeenServiceName(name) {
|
|
182
|
+
const s = String(name || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
183
|
+
if (!s || s === 'unknown' || s === 'unknown-service') return
|
|
184
|
+
const isNew = !seenServiceNames.has(s)
|
|
185
|
+
seenServiceNames.add(s)
|
|
186
|
+
if (isNew) scheduleOtelCollectorConfigWrite()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function fetchGrafanaOrgNames() {
|
|
190
|
+
const orgs = await fetchGrafanaOrgs()
|
|
191
|
+
return orgs.map((o) => (o.name || '').trim()).filter(Boolean)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function fetchGrafanaOrgs() {
|
|
195
|
+
const runningInDocker = fs.existsSync('/.dockerenv')
|
|
196
|
+
const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
|
|
197
|
+
const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
|
|
198
|
+
const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
|
|
199
|
+
const grafanaAdminPassword = process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
|
|
200
|
+
try {
|
|
201
|
+
const resp = await axios.get(`${grafanaUrl}/api/orgs`, {
|
|
202
|
+
auth: { username: grafanaAdminUser, password: grafanaAdminPassword },
|
|
203
|
+
timeout: 3000
|
|
204
|
+
})
|
|
205
|
+
const orgs = (resp.data || []).map((o) => ({ id: o.id, name: (o.name || '').trim() })).filter((o) => o.name)
|
|
206
|
+
return orgs
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return []
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeServiceNameForOtel(s) {
|
|
213
|
+
const t = String(s || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
214
|
+
return t && t !== 'unknown' && t !== 'unknown-service' ? t : null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function getOtelCollectorConfigYaml() {
|
|
218
|
+
const orgNames = await fetchGrafanaOrgNames()
|
|
219
|
+
const raw = [...new Set([...orgNames, ...seenServiceNames])]
|
|
220
|
+
const all = raw.map(normalizeServiceNameForOtel).filter(Boolean)
|
|
221
|
+
return generateOtelCollectorConfig(all)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function tryRestartOtelCollector() {
|
|
225
|
+
const container = process.env.OTEL_COLLECTOR_CONTAINER || 'azify-otel-collector'
|
|
226
|
+
try {
|
|
227
|
+
const http = require('http')
|
|
228
|
+
const req = http.request({
|
|
229
|
+
socketPath: '/var/run/docker.sock',
|
|
230
|
+
path: `/containers/${container}/restart`,
|
|
231
|
+
method: 'POST',
|
|
232
|
+
timeout: 10000
|
|
233
|
+
}, () => {})
|
|
234
|
+
req.on('error', () => {})
|
|
235
|
+
req.end()
|
|
236
|
+
} catch (_) {}
|
|
237
|
+
console.log('[otel-config] Restart do collector solicitado')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let otelConfigWriteScheduled = null
|
|
241
|
+
async function writeOtelCollectorConfigIfEnabled() {
|
|
242
|
+
const outPath = process.env.OTEL_COLLECTOR_CONFIG_OUTPUT_PATH
|
|
243
|
+
if (!outPath) return
|
|
244
|
+
try {
|
|
245
|
+
const yaml = await getOtelCollectorConfigYaml()
|
|
246
|
+
const all = [...new Set([...(await fetchGrafanaOrgNames()), ...seenServiceNames])]
|
|
247
|
+
const list = all.map(normalizeServiceNameForOtel).filter(Boolean)
|
|
248
|
+
const dir = require('path').dirname(outPath)
|
|
249
|
+
if (!fs.existsSync(dir)) {
|
|
250
|
+
try { fs.mkdirSync(dir, { recursive: true }) } catch (_) {}
|
|
251
|
+
}
|
|
252
|
+
if (fs.existsSync(dir)) {
|
|
253
|
+
fs.writeFileSync(outPath, yaml, 'utf8')
|
|
254
|
+
console.log('[otel-config] Config escrita:', list.length, 'serviços:', list.join(', ') || '(nenhum)')
|
|
255
|
+
tryRestartOtelCollector()
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.warn('[otel-config] Falha ao escrever config do collector:', err?.message || err)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function scheduleOtelCollectorConfigWrite() {
|
|
263
|
+
if (!process.env.OTEL_COLLECTOR_CONFIG_OUTPUT_PATH) return
|
|
264
|
+
if (otelConfigWriteScheduled) return
|
|
265
|
+
otelConfigWriteScheduled = setTimeout(async () => {
|
|
266
|
+
otelConfigWriteScheduled = null
|
|
267
|
+
await writeOtelCollectorConfigIfEnabled()
|
|
268
|
+
}, 5000)
|
|
269
|
+
}
|
|
270
|
+
|
|
177
271
|
function escapeForSQLite(str) {
|
|
178
272
|
if (!str) return "''"
|
|
179
273
|
return str.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r')
|
|
@@ -184,7 +278,7 @@ function runSqlite(sql) {
|
|
|
184
278
|
const grafanaContainer = process.env.GRAFANA_DOCKER_CONTAINER || 'azify-grafana'
|
|
185
279
|
const { execSync } = require('child_process')
|
|
186
280
|
const localDbExists = fs.existsSync(dbPath)
|
|
187
|
-
|
|
281
|
+
|
|
188
282
|
const escapedForLocal = sql.replace(/"/g, '""')
|
|
189
283
|
const escapedForDocker = sql.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')
|
|
190
284
|
|
|
@@ -214,20 +308,20 @@ async function createOrgViaSQLite(appName) {
|
|
|
214
308
|
|
|
215
309
|
console.log(`[createOrgViaSQLite] Verificando se Organization '${appName}' já existe...`)
|
|
216
310
|
const existingId = runSqlite(`SELECT id FROM org WHERE name='${appName}';`)
|
|
217
|
-
|
|
311
|
+
|
|
218
312
|
if (existingId && existingId !== '') {
|
|
219
313
|
console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' já existe (ID: ${existingId})`)
|
|
220
314
|
return { id: parseInt(existingId), name: appName }
|
|
221
315
|
}
|
|
222
|
-
|
|
316
|
+
|
|
223
317
|
console.log(`[createOrgViaSQLite] Criando Organization '${appName}'...`)
|
|
224
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}';`)
|
|
225
|
-
|
|
319
|
+
|
|
226
320
|
if (newId && newId !== '') {
|
|
227
321
|
console.log(`[createOrgViaSQLite] ✅ Organization '${appName}' criada com sucesso (ID: ${newId})`)
|
|
228
322
|
return { id: parseInt(newId), name: appName }
|
|
229
323
|
}
|
|
230
|
-
|
|
324
|
+
|
|
231
325
|
console.error(`[createOrgViaSQLite] ❌ Não foi possível obter ID da Organization criada`)
|
|
232
326
|
return null
|
|
233
327
|
} catch (error) {
|
|
@@ -249,13 +343,13 @@ async function setupGrafanaForApp(appName) {
|
|
|
249
343
|
if (!setupGrafanaForApp._cache) {
|
|
250
344
|
setupGrafanaForApp._cache = new Set()
|
|
251
345
|
}
|
|
252
|
-
|
|
346
|
+
|
|
253
347
|
if (setupGrafanaForApp._cache.has(appName)) {
|
|
254
348
|
return
|
|
255
349
|
}
|
|
256
|
-
|
|
350
|
+
|
|
257
351
|
setupGrafanaForApp._cache.add(appName)
|
|
258
|
-
|
|
352
|
+
|
|
259
353
|
console.log(`[setupGrafana] Processando app: ${appName}`)
|
|
260
354
|
|
|
261
355
|
try {
|
|
@@ -263,13 +357,13 @@ async function setupGrafanaForApp(appName) {
|
|
|
263
357
|
username: grafanaAdminUser,
|
|
264
358
|
password: grafanaAdminPassword
|
|
265
359
|
}
|
|
266
|
-
|
|
360
|
+
|
|
267
361
|
console.log(`[setupGrafana] Usando autenticação: user=${grafanaAdminUser}, password=${grafanaAdminPassword ? '***' : 'VAZIO'}`)
|
|
268
362
|
|
|
269
363
|
let org
|
|
270
|
-
|
|
364
|
+
|
|
271
365
|
try {
|
|
272
|
-
const orgResponse = await axios.get(`${grafanaUrl}/api/orgs/name/${encodeURIComponent(appName)}`, {
|
|
366
|
+
const orgResponse = await axios.get(`${grafanaUrl}/api/orgs/name/${encodeURIComponent(appName)}`, {
|
|
273
367
|
auth,
|
|
274
368
|
timeout: 3000
|
|
275
369
|
})
|
|
@@ -319,7 +413,7 @@ async function setupGrafanaForApp(appName) {
|
|
|
319
413
|
console.error(`[setupGrafana] ❌ Organization não encontrada/criada para ${appName}`)
|
|
320
414
|
throw new Error(`Organization não encontrada ou criada para ${appName}`)
|
|
321
415
|
}
|
|
322
|
-
|
|
416
|
+
|
|
323
417
|
console.log(`[setupGrafana] ✅ Organization encontrada/criada: ${org.name} (ID: ${org.id})`)
|
|
324
418
|
|
|
325
419
|
const adminEmails = process.env.ADMIN_EMAILS ? process.env.ADMIN_EMAILS.split(',') : []
|
|
@@ -327,16 +421,16 @@ async function setupGrafanaForApp(appName) {
|
|
|
327
421
|
for (const email of adminEmails) {
|
|
328
422
|
const trimmedEmail = email.trim()
|
|
329
423
|
if (!trimmedEmail) continue
|
|
330
|
-
|
|
424
|
+
|
|
331
425
|
try {
|
|
332
426
|
const userResponse = await axios.get(`${grafanaUrl}/api/users/lookup?loginOrEmail=${encodeURIComponent(trimmedEmail)}`, {
|
|
333
427
|
auth,
|
|
334
428
|
timeout: 3000
|
|
335
429
|
})
|
|
336
|
-
|
|
430
|
+
|
|
337
431
|
if (userResponse.data && userResponse.data.id) {
|
|
338
432
|
const userId = userResponse.data.id
|
|
339
|
-
|
|
433
|
+
|
|
340
434
|
try {
|
|
341
435
|
await axios.post(
|
|
342
436
|
`${grafanaUrl}/api/orgs/${org.id}/users`,
|
|
@@ -378,7 +472,7 @@ async function setupGrafanaForApp(appName) {
|
|
|
378
472
|
resolve(null)
|
|
379
473
|
}
|
|
380
474
|
})
|
|
381
|
-
|
|
475
|
+
|
|
382
476
|
if (dbUserId) {
|
|
383
477
|
try {
|
|
384
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};`)
|
|
@@ -392,7 +486,7 @@ async function setupGrafanaForApp(appName) {
|
|
|
392
486
|
}
|
|
393
487
|
|
|
394
488
|
console.log(`[setupGrafana] 📊 Criando datasource para ${appName}...`)
|
|
395
|
-
|
|
489
|
+
|
|
396
490
|
const datasourceUid = `opensearch-${appName.toLowerCase()}`
|
|
397
491
|
const indexName = `logs-${appName}`
|
|
398
492
|
const datasourceConfig = {
|
|
@@ -405,7 +499,7 @@ async function setupGrafanaForApp(appName) {
|
|
|
405
499
|
jsonData: {
|
|
406
500
|
index: indexName,
|
|
407
501
|
database: indexName,
|
|
408
|
-
timeField: '
|
|
502
|
+
timeField: 'time',
|
|
409
503
|
esVersion: '2.11.1',
|
|
410
504
|
version: '2.11.1',
|
|
411
505
|
logMessageField: 'message',
|
|
@@ -500,10 +594,12 @@ async function setupGrafanaForApp(appName) {
|
|
|
500
594
|
try {
|
|
501
595
|
const tempoDatasourceUid = `tempo-${appName.toLowerCase()}`
|
|
502
596
|
const tempoUrl = runningInDocker ? 'http://tempo:3200' : 'http://localhost:3200'
|
|
503
|
-
const
|
|
597
|
+
const serviceNameNorm = String(appName).trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
504
598
|
const tempoJsonData = {
|
|
505
599
|
httpMethod: 'GET',
|
|
506
|
-
|
|
600
|
+
httpHeaderName1: 'X-Scope-OrgID',
|
|
601
|
+
httpHeaderValue1: serviceNameNorm,
|
|
602
|
+
defaultQuery: `{ resource.service.name="${serviceNameNorm}" }`,
|
|
507
603
|
tracesToLogs: {
|
|
508
604
|
datasourceUid: datasourceUid,
|
|
509
605
|
tags: ['job', 'service', 'pod'],
|
|
@@ -517,7 +613,8 @@ async function setupGrafanaForApp(appName) {
|
|
|
517
613
|
query: `traceId:"\${__trace.traceId}"`
|
|
518
614
|
},
|
|
519
615
|
nodeGraph: { enabled: false },
|
|
520
|
-
search: { hide: true }
|
|
616
|
+
search: { hide: true },
|
|
617
|
+
lokiSearch: { hide: true }
|
|
521
618
|
}
|
|
522
619
|
const tempoDatasourceConfig = {
|
|
523
620
|
name: `Tempo-${appName}`,
|
|
@@ -527,6 +624,7 @@ async function setupGrafanaForApp(appName) {
|
|
|
527
624
|
uid: tempoDatasourceUid,
|
|
528
625
|
isDefault: false,
|
|
529
626
|
jsonData: tempoJsonData,
|
|
627
|
+
secureJsonData: { httpHeaderValue1: serviceNameNorm },
|
|
530
628
|
editable: true,
|
|
531
629
|
version: 1
|
|
532
630
|
}
|
|
@@ -540,9 +638,23 @@ async function setupGrafanaForApp(appName) {
|
|
|
540
638
|
})
|
|
541
639
|
console.log(`[setupGrafana] Datasource Tempo existente encontrado: ${existingTempo.data?.id || 'N/A'}`)
|
|
542
640
|
try {
|
|
641
|
+
const id = existingTempo.data.id
|
|
642
|
+
const putPayload = {
|
|
643
|
+
id,
|
|
644
|
+
orgId: existingTempo.data.orgId ?? org.id,
|
|
645
|
+
name: tempoDatasourceConfig.name,
|
|
646
|
+
type: 'tempo',
|
|
647
|
+
access: 'proxy',
|
|
648
|
+
url: tempoUrl,
|
|
649
|
+
uid: tempoDatasourceUid,
|
|
650
|
+
isDefault: false,
|
|
651
|
+
version: existingTempo.data.version ?? 1,
|
|
652
|
+
jsonData: { ...(existingTempo.data.jsonData || {}), ...tempoJsonData },
|
|
653
|
+
secureJsonData: { httpHeaderValue1: serviceNameNorm }
|
|
654
|
+
}
|
|
543
655
|
await axios.put(
|
|
544
|
-
`${grafanaUrl}/api/datasources/${
|
|
545
|
-
|
|
656
|
+
`${grafanaUrl}/api/datasources/${id}`,
|
|
657
|
+
putPayload,
|
|
546
658
|
{
|
|
547
659
|
auth,
|
|
548
660
|
headers: { 'X-Grafana-Org-Id': org.id },
|
|
@@ -579,10 +691,10 @@ async function setupGrafanaForApp(appName) {
|
|
|
579
691
|
}
|
|
580
692
|
|
|
581
693
|
console.log(`[setupGrafana] 📈 Criando dashboard para ${appName}...`)
|
|
582
|
-
|
|
694
|
+
|
|
583
695
|
const appNameLower = appName.toLowerCase()
|
|
584
696
|
const indexFilter = `_index:logs-${appNameLower}`
|
|
585
|
-
|
|
697
|
+
|
|
586
698
|
const dashboardConfig = {
|
|
587
699
|
dashboard: {
|
|
588
700
|
title: `Application Health - ${appName}`,
|
|
@@ -615,11 +727,11 @@ async function setupGrafanaForApp(appName) {
|
|
|
615
727
|
defaults: {
|
|
616
728
|
unit: 'short',
|
|
617
729
|
color: { mode: 'thresholds' },
|
|
618
|
-
custom: {
|
|
619
|
-
reduceOptions: {
|
|
620
|
-
values: false,
|
|
621
|
-
calcs: ['sum']
|
|
622
|
-
}
|
|
730
|
+
custom: {
|
|
731
|
+
reduceOptions: {
|
|
732
|
+
values: false,
|
|
733
|
+
calcs: ['sum']
|
|
734
|
+
}
|
|
623
735
|
}
|
|
624
736
|
}
|
|
625
737
|
},
|
|
@@ -792,14 +904,14 @@ async function setupGrafanaForApp(appName) {
|
|
|
792
904
|
auth,
|
|
793
905
|
headers: { 'X-Grafana-Org-Id': org.id }
|
|
794
906
|
})
|
|
795
|
-
|
|
907
|
+
|
|
796
908
|
if (searchResponse.data && searchResponse.data.length > 0) {
|
|
797
909
|
const existingDashboard = searchResponse.data[0]
|
|
798
910
|
const dashboardDetail = await axios.get(`${grafanaUrl}/api/dashboards/uid/${existingDashboard.uid}`, {
|
|
799
911
|
auth,
|
|
800
912
|
headers: { 'X-Grafana-Org-Id': org.id }
|
|
801
913
|
})
|
|
802
|
-
|
|
914
|
+
|
|
803
915
|
dashboardConfig.dashboard.id = dashboardDetail.data.dashboard.id
|
|
804
916
|
dashboardConfig.dashboard.uid = dashboardDetail.data.dashboard.uid
|
|
805
917
|
dashboardConfig.dashboard.version = dashboardDetail.data.dashboard.version
|
|
@@ -857,7 +969,7 @@ async function setupGrafanaForApp(appName) {
|
|
|
857
969
|
}
|
|
858
970
|
|
|
859
971
|
console.log(`[setupGrafana] ✅ Setup completo para ${appName} (Organization, Datasource e Dashboard)`)
|
|
860
|
-
|
|
972
|
+
|
|
861
973
|
setTimeout(() => {
|
|
862
974
|
setupGrafanaForApp._cache.delete(appName)
|
|
863
975
|
}, 3600000)
|
|
@@ -911,7 +1023,7 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
|
|
|
911
1023
|
await setupGrafanaForApp(serviceName)
|
|
912
1024
|
|
|
913
1025
|
const { grafanaUrl, auth, headers } = await getGrafanaClientConfig(options?.grafanaApiToken)
|
|
914
|
-
|
|
1026
|
+
|
|
915
1027
|
console.log(`[alerts] 🔗 Conectando ao Grafana: ${grafanaUrl}`)
|
|
916
1028
|
|
|
917
1029
|
const makeGrafanaRequestConfig = (extra = {}) => {
|
|
@@ -943,7 +1055,7 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
|
|
|
943
1055
|
if (!orgId) {
|
|
944
1056
|
throw new Error(`Org não encontrada para app ${serviceName}`)
|
|
945
1057
|
}
|
|
946
|
-
|
|
1058
|
+
|
|
947
1059
|
console.log(`[alerts] ✅ Organização encontrada: ${serviceName} (orgId=${orgId})`)
|
|
948
1060
|
|
|
949
1061
|
const datasourceUid = `opensearch-${serviceName}`
|
|
@@ -1113,7 +1225,7 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
|
|
|
1113
1225
|
|
|
1114
1226
|
const routeMatches = (route) => {
|
|
1115
1227
|
if (!Array.isArray(route.matchers)) return false
|
|
1116
|
-
return route.matchers.some(m =>
|
|
1228
|
+
return route.matchers.some(m =>
|
|
1117
1229
|
(typeof m === 'string' && m.includes(`alertId=~${alertId}`)) ||
|
|
1118
1230
|
(typeof m === 'object' && m.value === alertId)
|
|
1119
1231
|
)
|
|
@@ -1236,8 +1348,8 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
|
|
|
1236
1348
|
if (unit === 'h') windowSeconds = value * 60 * 60
|
|
1237
1349
|
if (unit === 'd') windowSeconds = value * 60 * 60 * 24
|
|
1238
1350
|
}
|
|
1239
|
-
|
|
1240
|
-
const relativeTimeRangeSeconds = Math.max(windowSeconds, 300)
|
|
1351
|
+
|
|
1352
|
+
const relativeTimeRangeSeconds = Math.max(windowSeconds, 300)
|
|
1241
1353
|
|
|
1242
1354
|
const receiverName = await createOrUpdateContactPointForRule(rule)
|
|
1243
1355
|
|
|
@@ -1521,25 +1633,25 @@ function getOrCreateTraceContext(requestId) {
|
|
|
1521
1633
|
if (traceContextMap.has(requestId)) {
|
|
1522
1634
|
return traceContextMap.get(requestId)
|
|
1523
1635
|
}
|
|
1524
|
-
|
|
1636
|
+
|
|
1525
1637
|
const traceContext = {
|
|
1526
1638
|
traceId: generateTraceId(),
|
|
1527
1639
|
spanId: generateSpanId(),
|
|
1528
1640
|
parentSpanId: null
|
|
1529
1641
|
}
|
|
1530
|
-
|
|
1642
|
+
|
|
1531
1643
|
traceContextMap.set(requestId, traceContext)
|
|
1532
|
-
|
|
1644
|
+
|
|
1533
1645
|
setTimeout(() => {
|
|
1534
1646
|
traceContextMap.delete(requestId)
|
|
1535
1647
|
}, 30000)
|
|
1536
|
-
|
|
1648
|
+
|
|
1537
1649
|
return traceContext
|
|
1538
1650
|
}
|
|
1539
1651
|
|
|
1540
1652
|
app.get('/health', (req, res) => {
|
|
1541
|
-
res.json({
|
|
1542
|
-
status: 'ok',
|
|
1653
|
+
res.json({
|
|
1654
|
+
status: 'ok',
|
|
1543
1655
|
service: 'azify-logger',
|
|
1544
1656
|
authEnabled
|
|
1545
1657
|
})
|
|
@@ -1563,7 +1675,7 @@ function decodeHtmlEntities(str) {
|
|
|
1563
1675
|
let prev = ''
|
|
1564
1676
|
let iterations = 0
|
|
1565
1677
|
const maxIterations = 10
|
|
1566
|
-
|
|
1678
|
+
|
|
1567
1679
|
const namedEntities = {
|
|
1568
1680
|
'"': '"',
|
|
1569
1681
|
''': "'",
|
|
@@ -1572,7 +1684,7 @@ function decodeHtmlEntities(str) {
|
|
|
1572
1684
|
'&': '&',
|
|
1573
1685
|
' ': ' '
|
|
1574
1686
|
}
|
|
1575
|
-
|
|
1687
|
+
|
|
1576
1688
|
while (decoded !== prev && iterations < maxIterations) {
|
|
1577
1689
|
prev = decoded
|
|
1578
1690
|
Object.keys(namedEntities).forEach(entity => {
|
|
@@ -1597,13 +1709,15 @@ function decodeHtmlEntities(str) {
|
|
|
1597
1709
|
})
|
|
1598
1710
|
iterations++
|
|
1599
1711
|
}
|
|
1600
|
-
|
|
1712
|
+
|
|
1601
1713
|
return decoded
|
|
1602
1714
|
}
|
|
1603
1715
|
|
|
1604
1716
|
async function handleLog(req, res) {
|
|
1605
|
-
|
|
1606
|
-
|
|
1717
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {}
|
|
1718
|
+
const payload = body.payload && typeof body.payload === 'object' ? body.payload : body
|
|
1719
|
+
let { level, message, meta } = payload
|
|
1720
|
+
|
|
1607
1721
|
if (!level || !message) {
|
|
1608
1722
|
return res.status(400).json({ success: false, message: 'Level and message are required.' })
|
|
1609
1723
|
}
|
|
@@ -1615,7 +1729,7 @@ async function handleLog(req, res) {
|
|
|
1615
1729
|
const requestPath = meta?.request?.path || meta?.request?.url || meta?.request?.baseUrl || meta?.url || meta?.path || ''
|
|
1616
1730
|
const requestPathLower = String(requestPath).toLowerCase()
|
|
1617
1731
|
const messageLower = String(message).toLowerCase()
|
|
1618
|
-
|
|
1732
|
+
|
|
1619
1733
|
const isSwaggerPath = (
|
|
1620
1734
|
requestPathLower.includes('/api-docs') ||
|
|
1621
1735
|
requestPathLower.includes('/swagger-ui') ||
|
|
@@ -1654,7 +1768,7 @@ async function handleLog(req, res) {
|
|
|
1654
1768
|
const requestId = meta && meta.requestId
|
|
1655
1769
|
|
|
1656
1770
|
let traceContext = null
|
|
1657
|
-
|
|
1771
|
+
|
|
1658
1772
|
if (meta && meta.traceId && meta.spanId) {
|
|
1659
1773
|
traceContext = {
|
|
1660
1774
|
traceId: meta.traceId,
|
|
@@ -1698,8 +1812,12 @@ async function handleLog(req, res) {
|
|
|
1698
1812
|
}
|
|
1699
1813
|
}
|
|
1700
1814
|
const traceIdHex = (meta && meta.traceIdHex) || (traceContext.traceId ? String(traceContext.traceId).replace(/-/g, '').padStart(32, '0').slice(0, 32) : null)
|
|
1815
|
+
const ts = (meta && meta.timestamp) != null ? meta.timestamp : Date.now()
|
|
1816
|
+
const timestampMs = typeof ts === 'number' ? ts : new Date(ts).getTime()
|
|
1817
|
+
const timestampIso = new Date(timestampMs).toISOString()
|
|
1701
1818
|
const logEntry = {
|
|
1702
|
-
'@timestamp':
|
|
1819
|
+
'@timestamp': timestampMs,
|
|
1820
|
+
time: timestampIso,
|
|
1703
1821
|
level,
|
|
1704
1822
|
message,
|
|
1705
1823
|
service: {
|
|
@@ -1718,7 +1836,7 @@ async function handleLog(req, res) {
|
|
|
1718
1836
|
Object.keys(meta).forEach(key => {
|
|
1719
1837
|
if (!['timestamp', 'service', 'environment', 'hostname', 'traceId', 'traceIdHex', 'spanId', 'parentSpanId'].includes(key)) {
|
|
1720
1838
|
let value = meta[key]
|
|
1721
|
-
|
|
1839
|
+
|
|
1722
1840
|
if (typeof value === 'string') {
|
|
1723
1841
|
if (key === 'url' || key === 'path' || key === 'baseUrl' || key === 'message') {
|
|
1724
1842
|
value = decodeHtmlEntities(value)
|
|
@@ -1726,7 +1844,7 @@ async function handleLog(req, res) {
|
|
|
1726
1844
|
value = decodeHtmlEntities(value)
|
|
1727
1845
|
}
|
|
1728
1846
|
}
|
|
1729
|
-
|
|
1847
|
+
|
|
1730
1848
|
const truncateBody = (bodyValue, forResponse = false) => {
|
|
1731
1849
|
if (forResponse) {
|
|
1732
1850
|
if (typeof bodyValue === 'string') {
|
|
@@ -1742,7 +1860,7 @@ async function handleLog(req, res) {
|
|
|
1742
1860
|
}
|
|
1743
1861
|
return String(bodyValue)
|
|
1744
1862
|
}
|
|
1745
|
-
|
|
1863
|
+
|
|
1746
1864
|
if (typeof bodyValue === 'string') {
|
|
1747
1865
|
return bodyValue
|
|
1748
1866
|
} else if (typeof bodyValue === 'object' && bodyValue !== null) {
|
|
@@ -1761,7 +1879,7 @@ async function handleLog(req, res) {
|
|
|
1761
1879
|
}
|
|
1762
1880
|
return bodyValue
|
|
1763
1881
|
}
|
|
1764
|
-
|
|
1882
|
+
|
|
1765
1883
|
if (key === 'request' && value && typeof value === 'object' && value.body !== undefined) {
|
|
1766
1884
|
const processedBody = truncateBody(value.body, false)
|
|
1767
1885
|
if (typeof processedBody === 'string' && processedBody.length > 5000) {
|
|
@@ -1812,13 +1930,14 @@ async function handleLog(req, res) {
|
|
|
1812
1930
|
const appName = logEntry.appName || logEntry.service?.name || 'unknown'
|
|
1813
1931
|
const serviceName = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
1814
1932
|
const indexName = `logs-${serviceName}`
|
|
1815
|
-
|
|
1933
|
+
addSeenServiceName(serviceName)
|
|
1934
|
+
|
|
1816
1935
|
await axios.post(`${osUrl}/${indexName}/_doc`, logEntry, {
|
|
1817
1936
|
headers: { 'Content-Type': 'application/json' }
|
|
1818
1937
|
})
|
|
1819
|
-
|
|
1938
|
+
|
|
1820
1939
|
console.log(`[setupGrafana] serviceName: ${serviceName}, appName: ${appName}`)
|
|
1821
|
-
|
|
1940
|
+
|
|
1822
1941
|
if (serviceName !== 'unknown' && serviceName !== 'unknown-service') {
|
|
1823
1942
|
console.log(`[setupGrafana] Iniciando setup para app: ${serviceName}`)
|
|
1824
1943
|
setupGrafanaForApp(serviceName).then(() => {
|
|
@@ -1834,7 +1953,7 @@ async function handleLog(req, res) {
|
|
|
1834
1953
|
} else {
|
|
1835
1954
|
console.log(`[setupGrafana] ⚠️ Pulando setup para serviceName inválido: ${serviceName}`)
|
|
1836
1955
|
}
|
|
1837
|
-
|
|
1956
|
+
|
|
1838
1957
|
res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
|
|
1839
1958
|
} catch (error) {
|
|
1840
1959
|
const status = error?.response?.status
|
|
@@ -1864,10 +1983,41 @@ async function handleLog(req, res) {
|
|
|
1864
1983
|
|
|
1865
1984
|
app.post('/log', (req, res) => handleLog(req, res))
|
|
1866
1985
|
|
|
1986
|
+
function extractServiceNamesFromTraceBody(body) {
|
|
1987
|
+
const names = new Set()
|
|
1988
|
+
try {
|
|
1989
|
+
const spans = body?.resourceSpans
|
|
1990
|
+
if (!Array.isArray(spans)) return names
|
|
1991
|
+
for (const rs of spans) {
|
|
1992
|
+
const attrs = rs?.resource?.attributes
|
|
1993
|
+
if (!Array.isArray(attrs)) continue
|
|
1994
|
+
for (const a of attrs) {
|
|
1995
|
+
if (a?.key === 'service.name') {
|
|
1996
|
+
const v = a?.value?.stringValue ?? a?.value
|
|
1997
|
+
if (typeof v === 'string' && v.trim()) names.add(v.trim())
|
|
1998
|
+
break
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
} catch (_) {}
|
|
2003
|
+
return names
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1867
2006
|
app.post('/v1/traces', async (req, res) => {
|
|
1868
2007
|
try {
|
|
1869
|
-
const
|
|
1870
|
-
const
|
|
2008
|
+
const names = extractServiceNamesFromTraceBody(req.body)
|
|
2009
|
+
for (const name of names) addSeenServiceName(name)
|
|
2010
|
+
if (req.body?.resourceSpans?.length) scheduleOtelCollectorConfigWrite()
|
|
2011
|
+
|
|
2012
|
+
const ep = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|
|
2013
|
+
let traceUrl = 'http://127.0.0.1:4318/v1/traces'
|
|
2014
|
+
if (ep) {
|
|
2015
|
+
try {
|
|
2016
|
+
const u = new URL(ep)
|
|
2017
|
+
traceUrl = ep.includes('/v1/') ? ep : `${u.origin.replace(/\/$/, '')}/v1/traces`
|
|
2018
|
+
} catch (_) {}
|
|
2019
|
+
}
|
|
2020
|
+
const response = await axios.post(traceUrl, req.body, {
|
|
1871
2021
|
headers: {
|
|
1872
2022
|
'Content-Type': 'application/json'
|
|
1873
2023
|
},
|
|
@@ -1897,7 +2047,7 @@ app.post('/alerts/register', async (req, res) => {
|
|
|
1897
2047
|
}
|
|
1898
2048
|
|
|
1899
2049
|
console.log(`[alerts] 📥 Recebido registro de alertas para app=${appName}, ${rules.length} regra(s)`)
|
|
1900
|
-
|
|
2050
|
+
|
|
1901
2051
|
const results = await registerAlertRulesForApp(appName, rules, { grafanaApiToken })
|
|
1902
2052
|
|
|
1903
2053
|
res.json({
|
|
@@ -1962,7 +2112,7 @@ app.post('/dashboards/register', async (req, res) => {
|
|
|
1962
2112
|
const orgId = org.id
|
|
1963
2113
|
|
|
1964
2114
|
const datasourceUid = `opensearch-${appName.toLowerCase()}`
|
|
1965
|
-
|
|
2115
|
+
|
|
1966
2116
|
const dashboardConfig = {
|
|
1967
2117
|
...dashboard,
|
|
1968
2118
|
overwrite: true,
|
|
@@ -2064,7 +2214,22 @@ app.post('/dashboards/register', async (req, res) => {
|
|
|
2064
2214
|
|
|
2065
2215
|
const port = process.env.PORT || 3001
|
|
2066
2216
|
|
|
2067
|
-
app.listen(port)
|
|
2217
|
+
app.listen(port, () => {
|
|
2218
|
+
setTimeout(() => writeOtelCollectorConfigIfEnabled().catch(() => {}), 3000)
|
|
2219
|
+
setTimeout(async () => {
|
|
2220
|
+
try {
|
|
2221
|
+
const orgs = await fetchGrafanaOrgs()
|
|
2222
|
+
const toSetup = orgs.filter((o) => o.id !== 1 && o.name && o.name.toLowerCase() !== 'main org')
|
|
2223
|
+
if (setupGrafanaForApp._cache) setupGrafanaForApp._cache.clear()
|
|
2224
|
+
for (const org of toSetup) {
|
|
2225
|
+
await setupGrafanaForApp(org.name).catch((e) => console.warn('[setupGrafana] Reaplicar Tempo para org', org.name, ':', e?.message || e))
|
|
2226
|
+
}
|
|
2227
|
+
if (toSetup.length) await writeOtelCollectorConfigIfEnabled().catch(() => {})
|
|
2228
|
+
} catch (e) {
|
|
2229
|
+
console.warn('[setupGrafana] Reaplicar Tempo para todas as orgs:', e?.message || e)
|
|
2230
|
+
}
|
|
2231
|
+
}, 12000)
|
|
2232
|
+
})
|
|
2068
2233
|
|
|
2069
2234
|
process.on('SIGTERM', () => {
|
|
2070
2235
|
process.exit(0)
|
package/streams/httpQueue.js
CHANGED
|
@@ -95,10 +95,6 @@ function resolveRedisConfig (overrides = {}) {
|
|
|
95
95
|
const password = overrides.redisPassword ?? (overrides.redis && overrides.redis.password) ?? process.env.AZIFY_LOGGER_REDIS_PASSWORD
|
|
96
96
|
const pass = password != null && String(password).trim() !== '' ? String(password).trim() : undefined
|
|
97
97
|
|
|
98
|
-
if (!pass) {
|
|
99
|
-
return null
|
|
100
|
-
}
|
|
101
|
-
|
|
102
98
|
return {
|
|
103
99
|
url: redisUrl,
|
|
104
100
|
streamKey,
|
|
@@ -141,9 +137,20 @@ function createHttpLoggerTransport (loggerUrl, overrides) {
|
|
|
141
137
|
}
|
|
142
138
|
|
|
143
139
|
function createRedisStreamTransport (loggerUrl, options, redisConfig) {
|
|
140
|
+
let useInlineFallback = false
|
|
141
|
+
let inlineTransport = null
|
|
142
|
+
const redisConfigWithCb = {
|
|
143
|
+
...redisConfig,
|
|
144
|
+
onUnrecoverableError () {
|
|
145
|
+
if (useInlineFallback) return
|
|
146
|
+
useInlineFallback = true
|
|
147
|
+
inlineTransport = buildInlineTransport(loggerUrl, options)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
144
151
|
let producer = null
|
|
145
152
|
try {
|
|
146
|
-
producer = createRedisProducer(
|
|
153
|
+
producer = createRedisProducer(redisConfigWithCb)
|
|
147
154
|
} catch (err) {
|
|
148
155
|
throw new Error(`Failed to create Redis producer: ${err.message}`)
|
|
149
156
|
}
|
|
@@ -152,9 +159,6 @@ function createRedisStreamTransport (loggerUrl, options, redisConfig) {
|
|
|
152
159
|
throw new Error('Failed to create Redis producer: producer is null')
|
|
153
160
|
}
|
|
154
161
|
|
|
155
|
-
let useInlineFallback = false
|
|
156
|
-
let inlineTransport = null
|
|
157
|
-
|
|
158
162
|
let spool = null
|
|
159
163
|
if (createFileSpool) {
|
|
160
164
|
try {
|
package/trace-export.js
CHANGED
|
@@ -25,6 +25,11 @@ function normalizeHex(val, len) {
|
|
|
25
25
|
return hex || null
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function normalizeServiceNameForTraces(s) {
|
|
29
|
+
const t = String(s || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
30
|
+
return t && t !== 'unknown' && t !== 'unknown-service' ? t : 'default'
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
function sendSpanToOtel(opts) {
|
|
29
34
|
const endpoint = getEndpoint()
|
|
30
35
|
if (!endpoint) return
|
|
@@ -32,7 +37,7 @@ function sendSpanToOtel(opts) {
|
|
|
32
37
|
const traceIdHex = normalizeHex(traceId, 32)
|
|
33
38
|
const spanIdHex = normalizeHex(spanId, 16)
|
|
34
39
|
if (!traceIdHex || !spanIdHex || !serviceName) return
|
|
35
|
-
const svc = String(serviceName).trim() || 'unknown'
|
|
40
|
+
const svc = normalizeServiceNameForTraces(String(serviceName).trim() || 'unknown')
|
|
36
41
|
const spanName = name || 'request'
|
|
37
42
|
const start = startTimeNs || BigInt(Date.now()) * BigInt(1e6)
|
|
38
43
|
const end = endTimeNs || BigInt(Date.now()) * BigInt(1e6)
|