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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "Azify Logger Client - Centralized logging for OpenSearch",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
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
- ...meta,
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
- const request = ensureRequest(input, init)
764
- const method = request.method.toUpperCase()
765
- const url = request.url
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(request)
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: Object.fromEntries(request.headers.entries())
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
- if (HTTP_CLIENT_MODE === 'all') {
795
- sendOutboundLog('info', `[REQUEST] ${method} ${url}`, requestMeta)
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 = {
@@ -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
- return bodyValue
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
- let str = bodyValue.toString('utf8')
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)