azify-logger 1.0.31 → 1.0.33
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/package.json +1 -1
- package/register.js +197 -14
- package/scripts/redis-worker.js +31 -0
- package/server.js +810 -34
package/package.json
CHANGED
package/register.js
CHANGED
|
@@ -84,11 +84,32 @@ try {
|
|
|
84
84
|
return
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
const metaCopy = { ...meta }
|
|
88
|
+
if (metaCopy.requestBody != null) {
|
|
89
|
+
if (typeof metaCopy.requestBody !== 'string') {
|
|
90
|
+
try {
|
|
91
|
+
metaCopy.requestBody = JSON.stringify(metaCopy.requestBody)
|
|
92
|
+
} catch (_) {
|
|
93
|
+
metaCopy.requestBody = String(metaCopy.requestBody)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (metaCopy.meta && metaCopy.meta.requestBody != null) {
|
|
99
|
+
if (typeof metaCopy.meta.requestBody !== 'string') {
|
|
100
|
+
try {
|
|
101
|
+
metaCopy.meta.requestBody = JSON.stringify(metaCopy.meta.requestBody)
|
|
102
|
+
} catch (_) {
|
|
103
|
+
metaCopy.meta.requestBody = String(metaCopy.meta.requestBody)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
const payload = {
|
|
88
109
|
level,
|
|
89
110
|
message,
|
|
90
111
|
meta: {
|
|
91
|
-
...
|
|
112
|
+
...metaCopy,
|
|
92
113
|
service: {
|
|
93
114
|
name: serviceName,
|
|
94
115
|
version: (meta && meta.service && meta.service.version) || '1.0.0'
|
|
@@ -739,6 +760,7 @@ try {
|
|
|
739
760
|
} catch (_) {}
|
|
740
761
|
|
|
741
762
|
try {
|
|
763
|
+
|
|
742
764
|
if (typeof globalThis.fetch === 'function') {
|
|
743
765
|
const g = globalThis
|
|
744
766
|
if (!g.__azifyLoggerFetchPatched) {
|
|
@@ -760,13 +782,60 @@ try {
|
|
|
760
782
|
return originalFetch(input, init)
|
|
761
783
|
}
|
|
762
784
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
785
|
+
let url = 'unknown'
|
|
786
|
+
let method = 'GET'
|
|
787
|
+
|
|
788
|
+
if (typeof input === 'string') {
|
|
789
|
+
url = input
|
|
790
|
+
method = (init?.method || 'GET').toUpperCase()
|
|
791
|
+
} else if (input instanceof URL) {
|
|
792
|
+
url = input.toString()
|
|
793
|
+
method = (init?.method || 'GET').toUpperCase()
|
|
794
|
+
} else if (typeof Request !== 'undefined' && input instanceof Request) {
|
|
795
|
+
url = input.url
|
|
796
|
+
method = input.method.toUpperCase()
|
|
797
|
+
} else {
|
|
798
|
+
try {
|
|
799
|
+
url = String(input)
|
|
800
|
+
method = (init?.method || 'GET').toUpperCase()
|
|
801
|
+
} catch (_) {
|
|
802
|
+
return originalFetch(input, init)
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
767
806
|
const testMeta = { url }
|
|
768
807
|
if (isLoggerApiCall(testMeta)) {
|
|
769
|
-
return originalFetch(
|
|
808
|
+
return originalFetch(input, init)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
let requestBody = null
|
|
812
|
+
if (init && init.body != null) {
|
|
813
|
+
try {
|
|
814
|
+
const body = init.body
|
|
815
|
+
if (typeof body === 'string') {
|
|
816
|
+
requestBody = body
|
|
817
|
+
} else if (Buffer.isBuffer(body)) {
|
|
818
|
+
requestBody = body.toString('utf8')
|
|
819
|
+
} else if (body instanceof FormData) {
|
|
820
|
+
requestBody = '[FormData]'
|
|
821
|
+
} else if (body instanceof URLSearchParams) {
|
|
822
|
+
requestBody = body.toString()
|
|
823
|
+
} else if (typeof body === 'object' && body !== null) {
|
|
824
|
+
requestBody = JSON.stringify(body)
|
|
825
|
+
} else {
|
|
826
|
+
requestBody = String(body)
|
|
827
|
+
}
|
|
828
|
+
} catch (error) {
|
|
829
|
+
requestBody = null
|
|
830
|
+
}
|
|
831
|
+
} else if (typeof Request !== 'undefined' && input instanceof Request && input.body && !input.bodyUsed) {
|
|
832
|
+
try {
|
|
833
|
+
const cloned = input.clone()
|
|
834
|
+
const text = await cloned.text()
|
|
835
|
+
requestBody = text
|
|
836
|
+
} catch (_) {
|
|
837
|
+
requestBody = null
|
|
838
|
+
}
|
|
770
839
|
}
|
|
771
840
|
|
|
772
841
|
const ctx = getRequestContext()
|
|
@@ -775,11 +844,6 @@ try {
|
|
|
775
844
|
const requestId = (ctx?.requestId) || randomUUID()
|
|
776
845
|
const spanId = randomBytes(8).toString('hex')
|
|
777
846
|
|
|
778
|
-
request.headers.set('x-trace-id', traceId)
|
|
779
|
-
request.headers.set('x-span-id', spanId)
|
|
780
|
-
request.headers.set('x-parent-span-id', parentSpanId || '')
|
|
781
|
-
request.headers.set('x-request-id', requestId)
|
|
782
|
-
|
|
783
847
|
const requestMeta = {
|
|
784
848
|
traceId,
|
|
785
849
|
spanId,
|
|
@@ -787,12 +851,131 @@ try {
|
|
|
787
851
|
requestId,
|
|
788
852
|
method,
|
|
789
853
|
url,
|
|
790
|
-
headers:
|
|
854
|
+
headers: {}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (requestBody != null) {
|
|
858
|
+
let finalRequestBody = requestBody
|
|
859
|
+
if (typeof requestBody !== 'string') {
|
|
860
|
+
try {
|
|
861
|
+
finalRequestBody = JSON.stringify(requestBody)
|
|
862
|
+
} catch (_) {
|
|
863
|
+
finalRequestBody = String(requestBody)
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
requestMeta.requestBody = finalRequestBody
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
let request
|
|
870
|
+
try {
|
|
871
|
+
let headers
|
|
872
|
+
if (init?.headers instanceof Headers) {
|
|
873
|
+
headers = new Headers(init.headers)
|
|
874
|
+
} else if (init?.headers) {
|
|
875
|
+
headers = new Headers(init.headers)
|
|
876
|
+
} else {
|
|
877
|
+
headers = new Headers()
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
headers.set('x-trace-id', traceId)
|
|
881
|
+
headers.set('x-span-id', spanId)
|
|
882
|
+
headers.set('x-parent-span-id', parentSpanId || '')
|
|
883
|
+
headers.set('x-request-id', requestId)
|
|
884
|
+
|
|
885
|
+
requestMeta.headers = Object.fromEntries(headers.entries())
|
|
886
|
+
|
|
887
|
+
request = ensureRequest(input, {
|
|
888
|
+
...init,
|
|
889
|
+
headers: headers
|
|
890
|
+
})
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (init?.headers) {
|
|
893
|
+
try {
|
|
894
|
+
requestMeta.headers = typeof init.headers === 'object' && !(init.headers instanceof Headers)
|
|
895
|
+
? init.headers
|
|
896
|
+
: Object.fromEntries(new Headers(init.headers).entries())
|
|
897
|
+
} catch (_) {
|
|
898
|
+
requestMeta.headers = {}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
markSource(requestMeta, 'http-client')
|
|
903
|
+
const hasTraceHeaders = !!(requestMeta.traceId && requestMeta.spanId)
|
|
904
|
+
const shouldLogRequest =
|
|
905
|
+
HTTP_CLIENT_MODE === 'all' ||
|
|
906
|
+
hasTraceHeaders
|
|
907
|
+
|
|
908
|
+
if (shouldLogRequest) {
|
|
909
|
+
if (hasTraceHeaders) {
|
|
910
|
+
const metaCopy = { ...requestMeta }
|
|
911
|
+
if (metaCopy.requestBody != null && typeof metaCopy.requestBody !== 'string') {
|
|
912
|
+
try {
|
|
913
|
+
metaCopy.requestBody = JSON.stringify(metaCopy.requestBody)
|
|
914
|
+
} catch (_) {
|
|
915
|
+
metaCopy.requestBody = String(metaCopy.requestBody)
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const payload = {
|
|
919
|
+
level: 'info',
|
|
920
|
+
message: `[REQUEST] ${method} ${url}`,
|
|
921
|
+
meta: {
|
|
922
|
+
...metaCopy,
|
|
923
|
+
service: {
|
|
924
|
+
name: serviceName,
|
|
925
|
+
version: '1.0.0'
|
|
926
|
+
},
|
|
927
|
+
environment,
|
|
928
|
+
timestamp: new Date().toISOString(),
|
|
929
|
+
hostname: os.hostname()
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
transport.enqueue(payload, {
|
|
933
|
+
'content-type': 'application/json'
|
|
934
|
+
})
|
|
935
|
+
} else {
|
|
936
|
+
sendOutboundLog('info', `[REQUEST] ${method} ${url}`, requestMeta)
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return originalFetch(input, init)
|
|
791
941
|
}
|
|
792
942
|
|
|
793
943
|
markSource(requestMeta, 'http-client')
|
|
794
|
-
|
|
795
|
-
|
|
944
|
+
const hasTraceHeaders = !!(requestMeta.traceId && requestMeta.spanId)
|
|
945
|
+
const shouldLogRequest =
|
|
946
|
+
HTTP_CLIENT_MODE === 'all' ||
|
|
947
|
+
hasTraceHeaders
|
|
948
|
+
|
|
949
|
+
if (shouldLogRequest) {
|
|
950
|
+
if (hasTraceHeaders) {
|
|
951
|
+
const metaCopy = { ...requestMeta }
|
|
952
|
+
if (metaCopy.requestBody != null && typeof metaCopy.requestBody !== 'string') {
|
|
953
|
+
try {
|
|
954
|
+
metaCopy.requestBody = JSON.stringify(metaCopy.requestBody)
|
|
955
|
+
} catch (_) {
|
|
956
|
+
metaCopy.requestBody = String(metaCopy.requestBody)
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const payload = {
|
|
960
|
+
level: 'info',
|
|
961
|
+
message: `[REQUEST] ${method} ${url}`,
|
|
962
|
+
meta: {
|
|
963
|
+
...metaCopy,
|
|
964
|
+
service: {
|
|
965
|
+
name: serviceName,
|
|
966
|
+
version: '1.0.0'
|
|
967
|
+
},
|
|
968
|
+
environment,
|
|
969
|
+
timestamp: new Date().toISOString(),
|
|
970
|
+
hostname: os.hostname()
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
transport.enqueue(payload, {
|
|
974
|
+
'content-type': 'application/json'
|
|
975
|
+
})
|
|
976
|
+
} else {
|
|
977
|
+
sendOutboundLog('info', `[REQUEST] ${method} ${url}`, requestMeta)
|
|
978
|
+
}
|
|
796
979
|
}
|
|
797
980
|
|
|
798
981
|
const childCtx = {
|
package/scripts/redis-worker.js
CHANGED
|
@@ -226,6 +226,15 @@ function sanitizePayload(payload) {
|
|
|
226
226
|
if (sanitized.meta.responseBody) {
|
|
227
227
|
sanitized.meta.responseBody = sanitizeBody(sanitized.meta.responseBody)
|
|
228
228
|
}
|
|
229
|
+
if (sanitized.meta.requestBody != null) {
|
|
230
|
+
if (typeof sanitized.meta.requestBody !== 'string') {
|
|
231
|
+
try {
|
|
232
|
+
sanitized.meta.requestBody = JSON.stringify(sanitized.meta.requestBody)
|
|
233
|
+
} catch (_) {
|
|
234
|
+
sanitized.meta.requestBody = String(sanitized.meta.requestBody)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
229
238
|
}
|
|
230
239
|
|
|
231
240
|
return sanitized
|
|
@@ -248,6 +257,18 @@ async function deliver(entry) {
|
|
|
248
257
|
sanitizedPayload = entry.payload
|
|
249
258
|
}
|
|
250
259
|
}
|
|
260
|
+
|
|
261
|
+
if (sanitizedPayload && typeof sanitizedPayload === 'object' && sanitizedPayload.meta) {
|
|
262
|
+
if (sanitizedPayload.meta.requestBody != null) {
|
|
263
|
+
if (typeof sanitizedPayload.meta.requestBody !== 'string') {
|
|
264
|
+
try {
|
|
265
|
+
sanitizedPayload.meta.requestBody = JSON.stringify(sanitizedPayload.meta.requestBody)
|
|
266
|
+
} catch (_) {
|
|
267
|
+
sanitizedPayload.meta.requestBody = String(sanitizedPayload.meta.requestBody)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
251
272
|
|
|
252
273
|
await axios.post(target, sanitizedPayload, {
|
|
253
274
|
headers: entry.headers || {},
|
|
@@ -325,6 +346,16 @@ async function processEntry(raw) {
|
|
|
325
346
|
return
|
|
326
347
|
}
|
|
327
348
|
|
|
349
|
+
if (entry && entry.payload && entry.payload.meta && entry.payload.meta.requestBody != null) {
|
|
350
|
+
if (typeof entry.payload.meta.requestBody !== 'string') {
|
|
351
|
+
try {
|
|
352
|
+
entry.payload.meta.requestBody = JSON.stringify(entry.payload.meta.requestBody)
|
|
353
|
+
} catch (_) {
|
|
354
|
+
entry.payload.meta.requestBody = String(entry.payload.meta.requestBody)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
328
359
|
const attempts = Number(entry.attempts || 0)
|
|
329
360
|
try {
|
|
330
361
|
await deliver(entry)
|
package/server.js
CHANGED
|
@@ -143,10 +143,18 @@ async function ensureIndexTemplate() {
|
|
|
143
143
|
url: { type: 'keyword' },
|
|
144
144
|
statusCode: { type: 'integer' },
|
|
145
145
|
responseTime: { type: 'float' },
|
|
146
|
+
cpu: { type: 'float' },
|
|
147
|
+
cpuUsage: { type: 'float' },
|
|
148
|
+
memory: { type: 'float' },
|
|
149
|
+
memoryUsage: { type: 'float' },
|
|
150
|
+
totalMemory: { type: 'long' },
|
|
151
|
+
freeMemory: { type: 'long' },
|
|
152
|
+
usedMemory: { type: 'long' },
|
|
146
153
|
ip: { type: 'ip' },
|
|
147
154
|
userAgent: { type: 'text' },
|
|
148
155
|
environment: { type: 'keyword' },
|
|
149
156
|
hostname: { type: 'keyword' },
|
|
157
|
+
requestBody: { type: 'text' },
|
|
150
158
|
responseBody: { type: 'text' },
|
|
151
159
|
error: {
|
|
152
160
|
properties: {
|
|
@@ -404,7 +412,8 @@ async function setupGrafanaForApp(appName) {
|
|
|
404
412
|
maxConcurrentShardRequests: 5,
|
|
405
413
|
includeFrozen: false,
|
|
406
414
|
xpack: false,
|
|
407
|
-
flavor: 'opensearch'
|
|
415
|
+
flavor: 'opensearch',
|
|
416
|
+
queryType: 'Lucene'
|
|
408
417
|
},
|
|
409
418
|
editable: true,
|
|
410
419
|
version: 1
|
|
@@ -416,21 +425,8 @@ async function setupGrafanaForApp(appName) {
|
|
|
416
425
|
auth,
|
|
417
426
|
headers: { 'X-Grafana-Org-Id': org.id }
|
|
418
427
|
})
|
|
419
|
-
console.log(`[setupGrafana] Datasource existente encontrado: ${existingResponse.data?.id || 'N/A'}`)
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
await axios.put(
|
|
423
|
-
`${grafanaUrl}/api/datasources/${existingResponse.data.id}`,
|
|
424
|
-
datasourceConfig,
|
|
425
|
-
{
|
|
426
|
-
auth,
|
|
427
|
-
headers: { 'X-Grafana-Org-Id': org.id }
|
|
428
|
-
}
|
|
429
|
-
)
|
|
430
|
-
console.log(`[setupGrafana] ✅ Datasource atualizado: ${datasourceUid}`)
|
|
431
|
-
} catch (updateError) {
|
|
432
|
-
console.error(`[setupGrafana] ❌ Erro ao atualizar datasource: ${updateError.response?.data?.message || updateError.message}`)
|
|
433
|
-
}
|
|
428
|
+
console.log(`[setupGrafana] Datasource existente encontrado: ${datasourceUid} (ID: ${existingResponse.data?.id || 'N/A'})`)
|
|
429
|
+
console.log(`[setupGrafana] ℹ️ Datasource já existe, mantendo configuração atual`)
|
|
434
430
|
} catch (error) {
|
|
435
431
|
console.log(`[setupGrafana] Erro ao verificar datasource: status=${error.response?.status || 'N/A'}, message=${error.response?.data?.message || error.message}`)
|
|
436
432
|
if (error.response?.status === 404) {
|
|
@@ -845,6 +841,621 @@ async function setupGrafanaForApp(appName) {
|
|
|
845
841
|
}
|
|
846
842
|
}
|
|
847
843
|
|
|
844
|
+
async function getGrafanaClientConfig(apiTokenFromRequest) {
|
|
845
|
+
const runningInDocker = fs.existsSync('/.dockerenv')
|
|
846
|
+
const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
|
|
847
|
+
const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
|
|
848
|
+
|
|
849
|
+
const apiToken = apiTokenFromRequest || process.env.GRAFANA_API_TOKEN
|
|
850
|
+
|
|
851
|
+
if (apiToken) {
|
|
852
|
+
const headers = {
|
|
853
|
+
Authorization: `Bearer ${apiToken}`
|
|
854
|
+
}
|
|
855
|
+
return { grafanaUrl, auth: null, headers }
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
|
|
859
|
+
const grafanaAdminPassword =
|
|
860
|
+
process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
|
|
861
|
+
|
|
862
|
+
const auth = {
|
|
863
|
+
username: grafanaAdminUser,
|
|
864
|
+
password: grafanaAdminPassword
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return { grafanaUrl, auth, headers: null }
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function registerAlertRulesForApp(appName, rules, options = {}) {
|
|
871
|
+
if (!appName || !Array.isArray(rules) || rules.length === 0) {
|
|
872
|
+
throw new Error('appName e pelo menos uma rule são obrigatórios')
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const serviceName = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
876
|
+
|
|
877
|
+
console.log(`[alerts] 🔍 Iniciando registro de alertas para app=${appName} (serviceName=${serviceName})`)
|
|
878
|
+
|
|
879
|
+
await setupGrafanaForApp(serviceName)
|
|
880
|
+
|
|
881
|
+
const { grafanaUrl, auth, headers } = await getGrafanaClientConfig(options?.grafanaApiToken)
|
|
882
|
+
|
|
883
|
+
console.log(`[alerts] 🔗 Conectando ao Grafana: ${grafanaUrl}`)
|
|
884
|
+
|
|
885
|
+
const makeGrafanaRequestConfig = (extra = {}) => {
|
|
886
|
+
const cfg = {
|
|
887
|
+
timeout: 5000,
|
|
888
|
+
...extra
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (auth) {
|
|
892
|
+
cfg.auth = auth
|
|
893
|
+
}
|
|
894
|
+
if (headers) {
|
|
895
|
+
cfg.headers = {
|
|
896
|
+
...headers,
|
|
897
|
+
...(extra.headers || {})
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return cfg
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
console.log(`[alerts] 🔍 Buscando organização: ${serviceName}`)
|
|
905
|
+
const orgResp = await axios.get(
|
|
906
|
+
`${grafanaUrl}/api/orgs/name/${encodeURIComponent(serviceName)}`,
|
|
907
|
+
makeGrafanaRequestConfig()
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
const orgId = orgResp.data && orgResp.data.id
|
|
911
|
+
if (!orgId) {
|
|
912
|
+
throw new Error(`Org não encontrada para app ${serviceName}`)
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
console.log(`[alerts] ✅ Organização encontrada: ${serviceName} (orgId=${orgId})`)
|
|
916
|
+
|
|
917
|
+
const datasourceUid = `opensearch-${serviceName}`
|
|
918
|
+
const indexFilter = `_index:logs-${serviceName}`
|
|
919
|
+
|
|
920
|
+
async function createOrUpdateContactPointForRule(rule) {
|
|
921
|
+
const notify = rule.notify || {}
|
|
922
|
+
const explicitReceiver =
|
|
923
|
+
notify.receiverName || notify.receiver || null
|
|
924
|
+
|
|
925
|
+
if (explicitReceiver) {
|
|
926
|
+
return explicitReceiver
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const teamsWebhooks = Array.isArray(notify.teamsWebhooks)
|
|
930
|
+
? notify.teamsWebhooks.filter(Boolean)
|
|
931
|
+
: []
|
|
932
|
+
const emails = Array.isArray(notify.emails)
|
|
933
|
+
? notify.emails.filter(Boolean)
|
|
934
|
+
: []
|
|
935
|
+
|
|
936
|
+
if (teamsWebhooks.length === 0 && emails.length === 0) {
|
|
937
|
+
return null
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
let primaryReceiver = null
|
|
941
|
+
|
|
942
|
+
if (teamsWebhooks.length > 0) {
|
|
943
|
+
const cpName = `alerts-${serviceName}-${rule.id}-teams`
|
|
944
|
+
try {
|
|
945
|
+
const existing = await axios.get(
|
|
946
|
+
`${grafanaUrl}/api/v1/provisioning/contact-points`,
|
|
947
|
+
makeGrafanaRequestConfig({
|
|
948
|
+
headers: { 'X-Grafana-Org-Id': orgId }
|
|
949
|
+
})
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
const found = Array.isArray(existing.data)
|
|
953
|
+
? existing.data.find(cp => cp.name === cpName)
|
|
954
|
+
: null
|
|
955
|
+
|
|
956
|
+
const payload = {
|
|
957
|
+
name: cpName,
|
|
958
|
+
type: 'teams',
|
|
959
|
+
settings: {
|
|
960
|
+
url: teamsWebhooks[0]
|
|
961
|
+
},
|
|
962
|
+
disableResolveMessage: false
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (found && found.uid) {
|
|
966
|
+
await axios.put(
|
|
967
|
+
`${grafanaUrl}/api/v1/provisioning/contact-points/${encodeURIComponent(found.uid)}`,
|
|
968
|
+
payload,
|
|
969
|
+
makeGrafanaRequestConfig({
|
|
970
|
+
headers: { 'X-Grafana-Org-Id': orgId, 'Content-Type': 'application/json' }
|
|
971
|
+
})
|
|
972
|
+
)
|
|
973
|
+
console.log(`[alerts] ✅ Contact point Teams atualizado: ${cpName}`)
|
|
974
|
+
} else {
|
|
975
|
+
await axios.post(
|
|
976
|
+
`${grafanaUrl}/api/v1/provisioning/contact-points`,
|
|
977
|
+
payload,
|
|
978
|
+
makeGrafanaRequestConfig({
|
|
979
|
+
headers: { 'X-Grafana-Org-Id': orgId, 'Content-Type': 'application/json' }
|
|
980
|
+
})
|
|
981
|
+
)
|
|
982
|
+
console.log(`[alerts] ✅ Contact point Teams criado: ${cpName}`)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (!primaryReceiver) primaryReceiver = cpName
|
|
986
|
+
} catch (err) {
|
|
987
|
+
const status = err?.response?.status || 'N/A'
|
|
988
|
+
const msg = err?.response?.data?.message || err.message
|
|
989
|
+
console.error(`[alerts] ❌ Erro ao criar/atualizar contact point Teams ${cpName}: ${msg} (status=${status})`)
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (emails.length > 0) {
|
|
994
|
+
const cpName = `alerts-${serviceName}-${rule.id}-email`
|
|
995
|
+
try {
|
|
996
|
+
const existing = await axios.get(
|
|
997
|
+
`${grafanaUrl}/api/v1/provisioning/contact-points`,
|
|
998
|
+
makeGrafanaRequestConfig({
|
|
999
|
+
headers: { 'X-Grafana-Org-Id': orgId }
|
|
1000
|
+
})
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
const found = Array.isArray(existing.data)
|
|
1004
|
+
? existing.data.find(cp => cp.name === cpName)
|
|
1005
|
+
: null
|
|
1006
|
+
|
|
1007
|
+
const payload = {
|
|
1008
|
+
name: cpName,
|
|
1009
|
+
type: 'email',
|
|
1010
|
+
settings: {
|
|
1011
|
+
addresses: emails.join(',')
|
|
1012
|
+
},
|
|
1013
|
+
disableResolveMessage: false
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (found && found.uid) {
|
|
1017
|
+
await axios.put(
|
|
1018
|
+
`${grafanaUrl}/api/v1/provisioning/contact-points/${encodeURIComponent(found.uid)}`,
|
|
1019
|
+
payload,
|
|
1020
|
+
makeGrafanaRequestConfig({
|
|
1021
|
+
headers: { 'X-Grafana-Org-Id': orgId, 'Content-Type': 'application/json' }
|
|
1022
|
+
})
|
|
1023
|
+
)
|
|
1024
|
+
console.log(`[alerts] ✅ Contact point Email atualizado: ${cpName}`)
|
|
1025
|
+
} else {
|
|
1026
|
+
await axios.post(
|
|
1027
|
+
`${grafanaUrl}/api/v1/provisioning/contact-points`,
|
|
1028
|
+
payload,
|
|
1029
|
+
makeGrafanaRequestConfig({
|
|
1030
|
+
headers: { 'X-Grafana-Org-Id': orgId, 'Content-Type': 'application/json' }
|
|
1031
|
+
})
|
|
1032
|
+
)
|
|
1033
|
+
console.log(`[alerts] ✅ Contact point Email criado: ${cpName}`)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (!primaryReceiver) primaryReceiver = cpName
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
const status = err?.response?.status || 'N/A'
|
|
1039
|
+
const msg = err?.response?.data?.message || err.message
|
|
1040
|
+
console.error(`[alerts] ❌ Erro ao criar/atualizar contact point Email ${cpName}: ${msg} (status=${status})`)
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return primaryReceiver
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function upsertNotificationPolicyForRule(alertId, receiverName) {
|
|
1048
|
+
if (!receiverName) return
|
|
1049
|
+
|
|
1050
|
+
try {
|
|
1051
|
+
const resp = await axios.get(
|
|
1052
|
+
`${grafanaUrl}/api/v1/provisioning/policies`,
|
|
1053
|
+
makeGrafanaRequestConfig({
|
|
1054
|
+
headers: { 'X-Grafana-Org-Id': orgId }
|
|
1055
|
+
})
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
let tree = resp.data || {}
|
|
1059
|
+
|
|
1060
|
+
if (!tree.receiver && !tree.routes) {
|
|
1061
|
+
tree = {
|
|
1062
|
+
receiver: receiverName,
|
|
1063
|
+
group_by: ['alertname'],
|
|
1064
|
+
routes: []
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (!Array.isArray(tree.routes)) {
|
|
1069
|
+
tree.routes = []
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const matchers = [
|
|
1073
|
+
`appName=~${serviceName}`,
|
|
1074
|
+
`alertId=~${alertId}`
|
|
1075
|
+
]
|
|
1076
|
+
|
|
1077
|
+
const routeMatches = (route) => {
|
|
1078
|
+
if (!Array.isArray(route.matchers)) return false
|
|
1079
|
+
return route.matchers.some(m =>
|
|
1080
|
+
(typeof m === 'string' && m.includes(`alertId=~${alertId}`)) ||
|
|
1081
|
+
(typeof m === 'object' && m.value === alertId)
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
let existingRoute = tree.routes.find(routeMatches)
|
|
1086
|
+
|
|
1087
|
+
if (existingRoute) {
|
|
1088
|
+
existingRoute.receiver = receiverName
|
|
1089
|
+
if (!Array.isArray(existingRoute.matchers)) {
|
|
1090
|
+
existingRoute.matchers = matchers
|
|
1091
|
+
}
|
|
1092
|
+
existingRoute.group_wait = '5s'
|
|
1093
|
+
console.log(`[alerts] ✅ Notification policy atualizada para alertId=${alertId}, receiver=${receiverName}`)
|
|
1094
|
+
} else {
|
|
1095
|
+
tree.routes.push({
|
|
1096
|
+
receiver: receiverName,
|
|
1097
|
+
matchers: matchers,
|
|
1098
|
+
continue: false,
|
|
1099
|
+
group_by: [],
|
|
1100
|
+
group_wait: '5s',
|
|
1101
|
+
group_interval: '5m',
|
|
1102
|
+
repeat_interval: '1h'
|
|
1103
|
+
})
|
|
1104
|
+
console.log(`[alerts] ✅ Notification policy criada para alertId=${alertId}, receiver=${receiverName}`)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
console.log(`[alerts] 📤 Atualizando notification policy tree:`, JSON.stringify(tree, null, 2))
|
|
1108
|
+
await axios.put(
|
|
1109
|
+
`${grafanaUrl}/api/v1/provisioning/policies`,
|
|
1110
|
+
tree,
|
|
1111
|
+
makeGrafanaRequestConfig({
|
|
1112
|
+
headers: { 'X-Grafana-Org-Id': orgId, 'Content-Type': 'application/json' }
|
|
1113
|
+
})
|
|
1114
|
+
)
|
|
1115
|
+
console.log(`[alerts] ✅ Notification policy tree atualizada com sucesso`)
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
const status = err?.response?.status || 'N/A'
|
|
1118
|
+
const msg = err?.response?.data?.message || err.message
|
|
1119
|
+
console.log(`[alerts] ⚠️ Aviso ao atualizar notification policies para app=${serviceName}, alertId=${alertId}: ${msg} (status=${status})`)
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function ensureAlertFolderForApp() {
|
|
1124
|
+
const folderUid = `alerts-${serviceName}`
|
|
1125
|
+
const folderTitle = `Alerts - ${serviceName}`
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
const existing = await axios.get(
|
|
1129
|
+
`${grafanaUrl}/api/folders/${encodeURIComponent(folderUid)}`,
|
|
1130
|
+
makeGrafanaRequestConfig({
|
|
1131
|
+
headers: { 'X-Grafana-Org-Id': orgId }
|
|
1132
|
+
})
|
|
1133
|
+
)
|
|
1134
|
+
if (existing.data && existing.data.uid) {
|
|
1135
|
+
return existing.data.uid
|
|
1136
|
+
}
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
if (err?.response?.status !== 404) {
|
|
1139
|
+
console.error(`[alerts] ⚠️ Erro ao buscar pasta de alertas: ${err.message}`)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
try {
|
|
1144
|
+
const created = await axios.post(
|
|
1145
|
+
`${grafanaUrl}/api/folders`,
|
|
1146
|
+
{ uid: folderUid, title: folderTitle },
|
|
1147
|
+
makeGrafanaRequestConfig({
|
|
1148
|
+
headers: { 'X-Grafana-Org-Id': orgId, 'Content-Type': 'application/json' }
|
|
1149
|
+
})
|
|
1150
|
+
)
|
|
1151
|
+
if (created.data && created.data.uid) {
|
|
1152
|
+
console.log(`[alerts] ✅ Pasta de alertas criada: ${folderTitle} (uid=${created.data.uid})`)
|
|
1153
|
+
return created.data.uid
|
|
1154
|
+
}
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
const status = err?.response?.status || 'N/A'
|
|
1157
|
+
if (status !== 409) {
|
|
1158
|
+
console.error(`[alerts] ❌ Erro ao criar pasta de alertas: ${err.message} (status=${status})`)
|
|
1159
|
+
} else {
|
|
1160
|
+
console.log(`[alerts] ℹ️ Pasta de alertas já existe: ${folderTitle}`)
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return folderUid
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const alertFolderUid = await ensureAlertFolderForApp()
|
|
1168
|
+
|
|
1169
|
+
for (const rule of rules) {
|
|
1170
|
+
const id = rule.id
|
|
1171
|
+
if (!id) continue
|
|
1172
|
+
|
|
1173
|
+
const title = rule.title || rule.description || id
|
|
1174
|
+
const description = rule.description || `Alert rule ${id} for ${serviceName}`
|
|
1175
|
+
const severity = rule.severity || 'warning'
|
|
1176
|
+
const window = rule.window || '5m'
|
|
1177
|
+
const query = rule.query || '*'
|
|
1178
|
+
|
|
1179
|
+
const rawQueryType = (rule.queryType || 'PPL').toString()
|
|
1180
|
+
const normalizedQueryType =
|
|
1181
|
+
rawQueryType.toLowerCase() === 'lucene' ? 'lucene' : 'PPL'
|
|
1182
|
+
const isPPL = normalizedQueryType === 'PPL'
|
|
1183
|
+
const isLucene = normalizedQueryType === 'lucene'
|
|
1184
|
+
|
|
1185
|
+
let fullQuery = query
|
|
1186
|
+
if (isLucene && (!query || query.trim() === '*')) {
|
|
1187
|
+
fullQuery = indexFilter
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const usesStats = isPPL && /stats\s+/.test(query.toLowerCase())
|
|
1191
|
+
const isReducedData = usesStats && !/stats\s+[^|]*\s+by\s+@timestamp/i.test(query)
|
|
1192
|
+
|
|
1193
|
+
let windowSeconds = 300
|
|
1194
|
+
const match = /^(\d+)([smhd])$/.exec(window)
|
|
1195
|
+
if (match) {
|
|
1196
|
+
const value = parseInt(match[1], 10)
|
|
1197
|
+
const unit = match[2]
|
|
1198
|
+
if (unit === 's') windowSeconds = value
|
|
1199
|
+
if (unit === 'm') windowSeconds = value * 60
|
|
1200
|
+
if (unit === 'h') windowSeconds = value * 60 * 60
|
|
1201
|
+
if (unit === 'd') windowSeconds = value * 60 * 60 * 24
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const relativeTimeRangeSeconds = Math.max(windowSeconds, 300) // 5 minutos mínimo
|
|
1205
|
+
|
|
1206
|
+
const receiverName = await createOrUpdateContactPointForRule(rule)
|
|
1207
|
+
|
|
1208
|
+
let metrics
|
|
1209
|
+
let bucketAggs = []
|
|
1210
|
+
|
|
1211
|
+
if (isPPL && isReducedData) {
|
|
1212
|
+
metrics = [
|
|
1213
|
+
{
|
|
1214
|
+
id: '1',
|
|
1215
|
+
type: 'avg',
|
|
1216
|
+
field: 'value'
|
|
1217
|
+
}
|
|
1218
|
+
]
|
|
1219
|
+
bucketAggs = []
|
|
1220
|
+
} else if (isLucene) {
|
|
1221
|
+
metrics = [
|
|
1222
|
+
{
|
|
1223
|
+
id: '1',
|
|
1224
|
+
type: 'count'
|
|
1225
|
+
}
|
|
1226
|
+
]
|
|
1227
|
+
bucketAggs = [
|
|
1228
|
+
{
|
|
1229
|
+
id: '2',
|
|
1230
|
+
type: 'date_histogram',
|
|
1231
|
+
field: '@timestamp',
|
|
1232
|
+
settings: {
|
|
1233
|
+
interval: 'auto',
|
|
1234
|
+
min_doc_count: 0
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
]
|
|
1238
|
+
} else {
|
|
1239
|
+
metrics = [
|
|
1240
|
+
{
|
|
1241
|
+
id: '1',
|
|
1242
|
+
type: 'count'
|
|
1243
|
+
}
|
|
1244
|
+
]
|
|
1245
|
+
bucketAggs = []
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let timeField = '@timestamp'
|
|
1249
|
+
if (
|
|
1250
|
+
isPPL &&
|
|
1251
|
+
isReducedData &&
|
|
1252
|
+
/max\(@timestamp\)\s+as\s+timestamp/i.test(query)
|
|
1253
|
+
) {
|
|
1254
|
+
timeField = 'timestamp'
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
let data
|
|
1258
|
+
let condition
|
|
1259
|
+
|
|
1260
|
+
if (isLucene) {
|
|
1261
|
+
const queryA = {
|
|
1262
|
+
refId: 'A',
|
|
1263
|
+
queryType: 'lucene',
|
|
1264
|
+
relativeTimeRange: {
|
|
1265
|
+
from: relativeTimeRangeSeconds,
|
|
1266
|
+
to: 0
|
|
1267
|
+
},
|
|
1268
|
+
datasourceUid,
|
|
1269
|
+
datasource: {
|
|
1270
|
+
type: 'grafana-opensearch-datasource',
|
|
1271
|
+
uid: datasourceUid
|
|
1272
|
+
},
|
|
1273
|
+
model: {
|
|
1274
|
+
alias: '',
|
|
1275
|
+
bucketAggs: [
|
|
1276
|
+
{
|
|
1277
|
+
field: '@timestamp',
|
|
1278
|
+
id: '2',
|
|
1279
|
+
settings: {
|
|
1280
|
+
interval: 'auto'
|
|
1281
|
+
},
|
|
1282
|
+
type: 'date_histogram'
|
|
1283
|
+
}
|
|
1284
|
+
],
|
|
1285
|
+
format: 'table',
|
|
1286
|
+
intervalMs: 1000,
|
|
1287
|
+
luceneQueryType: 'Metric',
|
|
1288
|
+
maxDataPoints: 43200,
|
|
1289
|
+
metrics: [
|
|
1290
|
+
{
|
|
1291
|
+
id: '1',
|
|
1292
|
+
type: 'count'
|
|
1293
|
+
}
|
|
1294
|
+
],
|
|
1295
|
+
query: fullQuery,
|
|
1296
|
+
queryType: 'lucene',
|
|
1297
|
+
refId: 'A',
|
|
1298
|
+
timeField: '@timestamp'
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const exprB = {
|
|
1303
|
+
refId: 'B',
|
|
1304
|
+
queryType: '',
|
|
1305
|
+
relativeTimeRange: {
|
|
1306
|
+
from: 0,
|
|
1307
|
+
to: 0
|
|
1308
|
+
},
|
|
1309
|
+
datasourceUid: '__expr__',
|
|
1310
|
+
model: {
|
|
1311
|
+
datasource: {
|
|
1312
|
+
type: '__expr__',
|
|
1313
|
+
uid: '__expr__'
|
|
1314
|
+
},
|
|
1315
|
+
expression: 'A',
|
|
1316
|
+
intervalMs: 1000,
|
|
1317
|
+
maxDataPoints: 43200,
|
|
1318
|
+
reducer: 'sum',
|
|
1319
|
+
refId: 'B',
|
|
1320
|
+
type: 'reduce'
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const exprC = {
|
|
1325
|
+
refId: 'C',
|
|
1326
|
+
queryType: '',
|
|
1327
|
+
relativeTimeRange: {
|
|
1328
|
+
from: 0,
|
|
1329
|
+
to: 0
|
|
1330
|
+
},
|
|
1331
|
+
datasourceUid: '__expr__',
|
|
1332
|
+
model: {
|
|
1333
|
+
datasource: {
|
|
1334
|
+
type: '__expr__',
|
|
1335
|
+
uid: '__expr__'
|
|
1336
|
+
},
|
|
1337
|
+
expression: 'B',
|
|
1338
|
+
intervalMs: 1000,
|
|
1339
|
+
maxDataPoints: 43200,
|
|
1340
|
+
refId: 'C',
|
|
1341
|
+
type: 'threshold',
|
|
1342
|
+
conditions: [
|
|
1343
|
+
{
|
|
1344
|
+
evaluator: {
|
|
1345
|
+
params: [0],
|
|
1346
|
+
type: 'gt'
|
|
1347
|
+
},
|
|
1348
|
+
operator: {
|
|
1349
|
+
type: 'and'
|
|
1350
|
+
},
|
|
1351
|
+
query: {
|
|
1352
|
+
params: ['C']
|
|
1353
|
+
},
|
|
1354
|
+
reducer: {
|
|
1355
|
+
params: [],
|
|
1356
|
+
type: 'last'
|
|
1357
|
+
},
|
|
1358
|
+
type: 'query'
|
|
1359
|
+
}
|
|
1360
|
+
]
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
data = [queryA, exprB, exprC]
|
|
1365
|
+
condition = 'C'
|
|
1366
|
+
} else {
|
|
1367
|
+
data = [
|
|
1368
|
+
{
|
|
1369
|
+
refId: 'A',
|
|
1370
|
+
datasourceUid,
|
|
1371
|
+
datasource: {
|
|
1372
|
+
type: 'grafana-opensearch-datasource',
|
|
1373
|
+
uid: datasourceUid
|
|
1374
|
+
},
|
|
1375
|
+
queryType: normalizedQueryType,
|
|
1376
|
+
relativeTimeRange: {
|
|
1377
|
+
from: windowSeconds,
|
|
1378
|
+
to: 0
|
|
1379
|
+
},
|
|
1380
|
+
model: {
|
|
1381
|
+
refId: 'A',
|
|
1382
|
+
query: fullQuery,
|
|
1383
|
+
queryType: normalizedQueryType,
|
|
1384
|
+
metrics,
|
|
1385
|
+
bucketAggs,
|
|
1386
|
+
timeField
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
]
|
|
1390
|
+
condition = 'A'
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const payload = {
|
|
1394
|
+
uid: `${serviceName}-${id}`,
|
|
1395
|
+
title,
|
|
1396
|
+
condition,
|
|
1397
|
+
data,
|
|
1398
|
+
noDataState: 'NoData',
|
|
1399
|
+
execErrState: 'Error',
|
|
1400
|
+
for: window,
|
|
1401
|
+
folderUid: alertFolderUid,
|
|
1402
|
+
labels: {
|
|
1403
|
+
appName: serviceName,
|
|
1404
|
+
severity,
|
|
1405
|
+
alertId: id,
|
|
1406
|
+
source: 'azify-logger-alerts'
|
|
1407
|
+
},
|
|
1408
|
+
annotations: {
|
|
1409
|
+
summary: title,
|
|
1410
|
+
description
|
|
1411
|
+
},
|
|
1412
|
+
ruleGroup: `alerts-${serviceName}`,
|
|
1413
|
+
orgId
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
try {
|
|
1417
|
+
console.log(`[alerts] Registrando regra '${id}' para app=${serviceName}, severity=${severity}`)
|
|
1418
|
+
|
|
1419
|
+
try {
|
|
1420
|
+
await axios.delete(
|
|
1421
|
+
`${grafanaUrl}/api/v1/provisioning/alert-rules/${payload.uid}`,
|
|
1422
|
+
makeGrafanaRequestConfig({
|
|
1423
|
+
headers: {
|
|
1424
|
+
'X-Grafana-Org-Id': orgId
|
|
1425
|
+
}
|
|
1426
|
+
})
|
|
1427
|
+
)
|
|
1428
|
+
console.log(`[alerts] ℹ️ Regra '${id}' deletada (será recriada)`)
|
|
1429
|
+
} catch (deleteErr) {
|
|
1430
|
+
if (deleteErr?.response?.status !== 404) {
|
|
1431
|
+
console.log(`[alerts] ⚠️ Erro ao deletar regra (continuando): ${deleteErr.message}`)
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
await axios.post(
|
|
1436
|
+
`${grafanaUrl}/api/v1/provisioning/alert-rules`,
|
|
1437
|
+
payload,
|
|
1438
|
+
makeGrafanaRequestConfig({
|
|
1439
|
+
headers: {
|
|
1440
|
+
'X-Grafana-Org-Id': orgId,
|
|
1441
|
+
'Content-Type': 'application/json'
|
|
1442
|
+
},
|
|
1443
|
+
timeout: 10000
|
|
1444
|
+
})
|
|
1445
|
+
)
|
|
1446
|
+
console.log(`[alerts] ✅ Regra '${id}' criada para app=${serviceName}`)
|
|
1447
|
+
|
|
1448
|
+
if (receiverName) {
|
|
1449
|
+
await upsertNotificationPolicyForRule(id, receiverName)
|
|
1450
|
+
}
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
const status = err?.response?.status || 'N/A'
|
|
1453
|
+
const msg = err?.response?.data?.message || err.message
|
|
1454
|
+
console.error(`[alerts] ❌ Falha ao registrar regra '${id}' para app=${serviceName}: ${msg} (status=${status})`)
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
848
1459
|
function generateTraceId() {
|
|
849
1460
|
return Math.random().toString(36).substring(2, 15) +
|
|
850
1461
|
Math.random().toString(36).substring(2, 15)
|
|
@@ -1081,28 +1692,16 @@ async function handleLog(req, res) {
|
|
|
1081
1692
|
}
|
|
1082
1693
|
|
|
1083
1694
|
if (typeof bodyValue === 'string') {
|
|
1084
|
-
if (bodyValue.trim().startsWith('{') || bodyValue.trim().startsWith('[')) {
|
|
1085
|
-
try {
|
|
1086
|
-
let parsed = JSON.parse(bodyValue)
|
|
1087
|
-
if (typeof parsed === 'object') {
|
|
1088
|
-
return parsed
|
|
1089
|
-
}
|
|
1090
|
-
} catch (_) { }
|
|
1091
|
-
}
|
|
1092
1695
|
return bodyValue
|
|
1093
1696
|
} else if (typeof bodyValue === 'object' && bodyValue !== null) {
|
|
1094
|
-
|
|
1697
|
+
try {
|
|
1698
|
+
return JSON.stringify(bodyValue)
|
|
1699
|
+
} catch (_) {
|
|
1700
|
+
return String(bodyValue)
|
|
1701
|
+
}
|
|
1095
1702
|
} else if (Buffer.isBuffer(bodyValue)) {
|
|
1096
1703
|
try {
|
|
1097
|
-
|
|
1098
|
-
if (str.trim().startsWith('{') || str.trim().startsWith('[')) {
|
|
1099
|
-
try {
|
|
1100
|
-
return JSON.parse(str)
|
|
1101
|
-
} catch (_) {
|
|
1102
|
-
return str
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
return str
|
|
1704
|
+
return bodyValue.toString('utf8')
|
|
1106
1705
|
} catch (_) {
|
|
1107
1706
|
return '[Unable to serialize Buffer]'
|
|
1108
1707
|
}
|
|
@@ -1233,6 +1832,183 @@ app.post('/v1/traces', async (req, res) => {
|
|
|
1233
1832
|
})
|
|
1234
1833
|
app.post('/send', (req, res) => handleLog(req, res))
|
|
1235
1834
|
|
|
1835
|
+
app.post('/alerts/register', async (req, res) => {
|
|
1836
|
+
try {
|
|
1837
|
+
const { appName, rules, grafanaApiToken } = req.body || {}
|
|
1838
|
+
|
|
1839
|
+
if (!appName || !Array.isArray(rules) || rules.length === 0) {
|
|
1840
|
+
return res.status(400).json({
|
|
1841
|
+
success: false,
|
|
1842
|
+
message: 'Parâmetros inválidos. É necessário enviar appName e uma lista não vazia de rules.'
|
|
1843
|
+
})
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
console.log(`[alerts] 📥 Recebido registro de alertas para app=${appName}, ${rules.length} regra(s)`)
|
|
1847
|
+
|
|
1848
|
+
const results = await registerAlertRulesForApp(appName, rules, { grafanaApiToken })
|
|
1849
|
+
|
|
1850
|
+
res.json({
|
|
1851
|
+
success: true,
|
|
1852
|
+
message: 'Regras de alerta processadas com sucesso para a aplicação.',
|
|
1853
|
+
appName,
|
|
1854
|
+
rulesProcessed: rules.length,
|
|
1855
|
+
details: results
|
|
1856
|
+
})
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
const message = error?.message || 'Erro desconhecido ao registrar alertas'
|
|
1859
|
+
console.error('[alerts] ❌ Erro no endpoint /alerts/register:', message)
|
|
1860
|
+
if (error.stack) {
|
|
1861
|
+
console.error('[alerts] Stack:', error.stack.substring(0, 500))
|
|
1862
|
+
}
|
|
1863
|
+
res.status(500).json({
|
|
1864
|
+
success: false,
|
|
1865
|
+
message
|
|
1866
|
+
})
|
|
1867
|
+
}
|
|
1868
|
+
})
|
|
1869
|
+
|
|
1870
|
+
app.post('/dashboards/register', async (req, res) => {
|
|
1871
|
+
try {
|
|
1872
|
+
const { appName, dashboard } = req.body || {}
|
|
1873
|
+
|
|
1874
|
+
if (!appName) {
|
|
1875
|
+
return res.status(400).json({
|
|
1876
|
+
success: false,
|
|
1877
|
+
message: 'appName is required'
|
|
1878
|
+
})
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
if (!dashboard || !dashboard.dashboard) {
|
|
1882
|
+
return res.status(400).json({
|
|
1883
|
+
success: false,
|
|
1884
|
+
message: 'dashboard object is required'
|
|
1885
|
+
})
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
console.log(`[dashboards] 📥 Recebido registro de dashboard para app=${appName}`)
|
|
1889
|
+
|
|
1890
|
+
await setupGrafanaForApp(appName)
|
|
1891
|
+
|
|
1892
|
+
const runningInDocker = fs.existsSync('/.dockerenv')
|
|
1893
|
+
const defaultGrafanaUrl = runningInDocker ? 'http://azify-grafana:3000' : 'http://127.0.0.1:3002'
|
|
1894
|
+
const grafanaUrl = process.env.GRAFANA_URL || defaultGrafanaUrl
|
|
1895
|
+
const grafanaAdminUser = process.env.GRAFANA_ADMIN_USER || 'admin'
|
|
1896
|
+
const grafanaAdminPassword = process.env.GRAFANA_ADMIN_PASSWORD || process.env.GF_SECURITY_ADMIN_PASSWORD || 'admin'
|
|
1897
|
+
|
|
1898
|
+
const auth = {
|
|
1899
|
+
username: grafanaAdminUser,
|
|
1900
|
+
password: grafanaAdminPassword
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
const orgResponse = await axios.get(`${grafanaUrl}/api/orgs/name/${encodeURIComponent(appName)}`, {
|
|
1904
|
+
auth,
|
|
1905
|
+
timeout: 3000
|
|
1906
|
+
})
|
|
1907
|
+
|
|
1908
|
+
const org = orgResponse.data
|
|
1909
|
+
const orgId = org.id
|
|
1910
|
+
|
|
1911
|
+
const datasourceUid = `opensearch-${appName.toLowerCase()}`
|
|
1912
|
+
|
|
1913
|
+
const dashboardConfig = {
|
|
1914
|
+
...dashboard,
|
|
1915
|
+
overwrite: true,
|
|
1916
|
+
folderId: null
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const replaceDatasourceUid = (obj) => {
|
|
1920
|
+
if (typeof obj === 'string') {
|
|
1921
|
+
return obj.replace(/\$\{DS_OPENSEARCH\}/g, datasourceUid)
|
|
1922
|
+
}
|
|
1923
|
+
if (Array.isArray(obj)) {
|
|
1924
|
+
return obj.map(replaceDatasourceUid)
|
|
1925
|
+
}
|
|
1926
|
+
if (obj && typeof obj === 'object') {
|
|
1927
|
+
const result = {}
|
|
1928
|
+
for (const key in obj) {
|
|
1929
|
+
if (key === 'datasource' && obj[key] && typeof obj[key] === 'object') {
|
|
1930
|
+
if (obj[key].uid === '${DS_OPENSEARCH}' || obj[key].uid?.includes('DS_OPENSEARCH')) {
|
|
1931
|
+
result[key] = {
|
|
1932
|
+
...obj[key],
|
|
1933
|
+
uid: datasourceUid,
|
|
1934
|
+
type: obj[key].type || 'grafana-opensearch-datasource'
|
|
1935
|
+
}
|
|
1936
|
+
} else {
|
|
1937
|
+
result[key] = replaceDatasourceUid(obj[key])
|
|
1938
|
+
}
|
|
1939
|
+
} else {
|
|
1940
|
+
result[key] = replaceDatasourceUid(obj[key])
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return result
|
|
1944
|
+
}
|
|
1945
|
+
return obj
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (dashboardConfig.dashboard) {
|
|
1949
|
+
dashboardConfig.dashboard = replaceDatasourceUid(dashboardConfig.dashboard)
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
try {
|
|
1953
|
+
const searchResponse = await axios.get(`${grafanaUrl}/api/search?query=${encodeURIComponent(dashboard.dashboard.title)}&type=dash-db`, {
|
|
1954
|
+
auth,
|
|
1955
|
+
headers: { 'X-Grafana-Org-Id': orgId },
|
|
1956
|
+
timeout: 3000
|
|
1957
|
+
})
|
|
1958
|
+
|
|
1959
|
+
if (searchResponse.data && searchResponse.data.length > 0) {
|
|
1960
|
+
const existingDashboard = searchResponse.data[0]
|
|
1961
|
+
const dashboardDetail = await axios.get(`${grafanaUrl}/api/dashboards/uid/${existingDashboard.uid}`, {
|
|
1962
|
+
auth,
|
|
1963
|
+
headers: { 'X-Grafana-Org-Id': orgId },
|
|
1964
|
+
timeout: 3000
|
|
1965
|
+
})
|
|
1966
|
+
|
|
1967
|
+
if (dashboardConfig.dashboard) {
|
|
1968
|
+
dashboardConfig.dashboard.id = dashboardDetail.data.dashboard.id
|
|
1969
|
+
dashboardConfig.dashboard.uid = dashboardDetail.data.dashboard.uid
|
|
1970
|
+
dashboardConfig.dashboard.version = dashboardDetail.data.dashboard.version
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
} catch (searchError) {
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
const dashboardResponse = await axios.post(
|
|
1977
|
+
`${grafanaUrl}/api/dashboards/db`,
|
|
1978
|
+
dashboardConfig,
|
|
1979
|
+
{
|
|
1980
|
+
auth,
|
|
1981
|
+
headers: { 'X-Grafana-Org-Id': orgId, 'Content-Type': 'application/json' },
|
|
1982
|
+
timeout: 10000
|
|
1983
|
+
}
|
|
1984
|
+
)
|
|
1985
|
+
|
|
1986
|
+
console.log(`[dashboards] ✅ Dashboard criado: ${dashboardResponse.data?.dashboard?.title || 'N/A'} (UID: ${dashboardResponse.data?.dashboard?.uid || 'N/A'})`)
|
|
1987
|
+
|
|
1988
|
+
res.json({
|
|
1989
|
+
success: true,
|
|
1990
|
+
message: 'Dashboard registrado com sucesso',
|
|
1991
|
+
appName,
|
|
1992
|
+
data: {
|
|
1993
|
+
dashboardId: dashboardResponse.data.id,
|
|
1994
|
+
dashboardUid: dashboardResponse.data.uid,
|
|
1995
|
+
url: dashboardResponse.data.url,
|
|
1996
|
+
title: dashboardResponse.data.dashboard?.title
|
|
1997
|
+
}
|
|
1998
|
+
})
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
const message = error?.response?.data?.message || error?.message || 'Erro desconhecido ao registrar dashboard'
|
|
2001
|
+
console.error('[dashboards] ❌ Erro no endpoint /dashboards/register:', message)
|
|
2002
|
+
if (error.stack) {
|
|
2003
|
+
console.error('[dashboards] Stack:', error.stack.substring(0, 500))
|
|
2004
|
+
}
|
|
2005
|
+
res.status(error?.response?.status || 500).json({
|
|
2006
|
+
success: false,
|
|
2007
|
+
message
|
|
2008
|
+
})
|
|
2009
|
+
}
|
|
2010
|
+
})
|
|
2011
|
+
|
|
1236
2012
|
const port = process.env.PORT || 3001
|
|
1237
2013
|
|
|
1238
2014
|
app.listen(port)
|