azify-logger 1.0.55 → 1.0.57

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/server.js CHANGED
@@ -232,6 +232,16 @@ async function ensureIndexTemplate() {
232
232
  hostname: { type: 'keyword' },
233
233
  source: { type: 'keyword' },
234
234
  responseBody: { type: 'text' },
235
+ businessStatus: { type: 'keyword' },
236
+ metaJson: { type: 'text' },
237
+ dataJson: { type: 'text' },
238
+ reqJson: { type: 'text' },
239
+ headersJson: { type: 'text' },
240
+ paramsJson: { type: 'text' },
241
+ queryJson: { type: 'text' },
242
+ requestJson: { type: 'text' },
243
+ responseJson: { type: 'text' },
244
+ bodyJson: { type: 'text' },
235
245
  error: {
236
246
  properties: {
237
247
  message: { type: 'text' },
@@ -2110,20 +2120,103 @@ function _trimLogEntryForOpenSearch(doc) {
2110
2120
  return out
2111
2121
  }
2112
2122
 
2123
+ const _OPENSEARCH_ROOT_KEEP_KEYS = new Set([
2124
+ '@timestamp', 'time', 'timestamp', 'level', 'message', 'traceId', 'spanId', 'parentSpanId',
2125
+ 'userId', 'requestId', 'method', 'url', 'baseUrl', 'statusCode', 'responseTime',
2126
+ 'cpu', 'cpuUsage', 'memory', 'memoryUsage', 'totalMemory', 'freeMemory', 'usedMemory',
2127
+ 'ip', 'userAgent', 'environment', 'hostname', 'source', 'responseBody', 'requestBody',
2128
+ 'bodyJson', 'businessStatus', 'appName', 'duration', 'name'
2129
+ ])
2130
+
2131
+ const _OPENSEARCH_ROOT_STRUCTURED_KEYS = new Set(['error', 'service'])
2132
+
2133
+ function _coerceLogValueToString(v) {
2134
+ if (v == null) return v
2135
+ if (typeof v === 'string') return v
2136
+ if (Buffer.isBuffer(v)) return v.toString('utf8')
2137
+ if (typeof v === 'object') {
2138
+ try {
2139
+ return JSON.stringify(v)
2140
+ } catch (_) {
2141
+ return String(v)
2142
+ }
2143
+ }
2144
+ return String(v)
2145
+ }
2146
+
2147
+ /** Campos fora da allowlist viram {campo}Json (text) — evita conflitos de dynamic mapping. */
2148
+ function _serializeUnmappedRootFieldsToJson(doc) {
2149
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc) || Buffer.isBuffer(doc)) return doc
2150
+ const o = { ...doc }
2151
+ for (const key of Object.keys(o)) {
2152
+ if (key.endsWith('Json')) continue
2153
+ if (_OPENSEARCH_ROOT_KEEP_KEYS.has(key)) continue
2154
+ if (_OPENSEARCH_ROOT_STRUCTURED_KEYS.has(key)) continue
2155
+ if (o[key] === undefined) continue
2156
+ o[key + 'Json'] = _coerceLogValueToString(o[key])
2157
+ delete o[key]
2158
+ }
2159
+ return o
2160
+ }
2161
+
2162
+ function _extractMappingConflictRootField(errorMsg) {
2163
+ const m = String(errorMsg || '').match(/(?:object mapping for|failed to parse field) \[([^\]]+)\]/)
2164
+ return m ? m[1].split('.')[0] : null
2165
+ }
2166
+
2167
+ function _forceRootFieldToJson(doc, rootKey) {
2168
+ if (!rootKey || !doc || typeof doc !== 'object' || Array.isArray(doc)) return doc
2169
+ if (rootKey.endsWith('Json') || _OPENSEARCH_ROOT_KEEP_KEYS.has(rootKey) || _OPENSEARCH_ROOT_STRUCTURED_KEYS.has(rootKey)) {
2170
+ return doc
2171
+ }
2172
+ const o = { ...doc }
2173
+ if (!Object.prototype.hasOwnProperty.call(o, rootKey)) return o
2174
+ o[rootKey + 'Json'] = _coerceLogValueToString(o[rootKey])
2175
+ delete o[rootKey]
2176
+ return o
2177
+ }
2178
+
2179
+ function _prepareDocForOpenSearch(doc) {
2180
+ return _serializeUnmappedRootFieldsToJson(doc)
2181
+ }
2182
+
2183
+ function _isOpenSearchMappingError(errorMsg) {
2184
+ const m = String(errorMsg || '')
2185
+ return m.includes('failed to parse field') ||
2186
+ m.includes('object mapping for') ||
2187
+ m.includes('mapper_parsing_exception') ||
2188
+ m.includes('Limit of total fields')
2189
+ }
2190
+
2113
2191
  function _normalizeLogEntryForOpenSearch(doc) {
2114
2192
  if (!doc || typeof doc !== 'object') return doc
2115
2193
  function coerceToString(v) {
2116
- if (v == null) return v
2117
- if (typeof v === 'string') return v
2118
- if (Buffer.isBuffer(v)) return v.toString('utf8')
2119
- if (typeof v === 'object') {
2120
- try {
2121
- return JSON.stringify(v)
2122
- } catch (_) {
2123
- return String(v)
2194
+ return _coerceLogValueToString(v)
2195
+ }
2196
+ /**
2197
+ * Índices antigos mapearam meta.status como long (primeiro doc numérico).
2198
+ * Status de negócio (ex. KYC_FAILED) vai para businessStatus (keyword).
2199
+ */
2200
+ function normalizeStatusFieldConflicts(obj) {
2201
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj) || Buffer.isBuffer(obj)) return obj
2202
+ const o = { ...obj }
2203
+ if (Object.prototype.hasOwnProperty.call(o, 'status')) {
2204
+ const st = o.status
2205
+ const isHttpCode = typeof st === 'number' && Number.isFinite(st) && st >= 100 && st <= 599
2206
+ if (typeof st === 'number' && Number.isFinite(st)) {
2207
+ if (isHttpCode && o.statusCode == null) o.statusCode = st
2208
+ } else if (st != null) {
2209
+ o.businessStatus = coerceToString(st)
2210
+ delete o.status
2211
+ }
2212
+ }
2213
+ for (const key of Object.keys(o)) {
2214
+ const v = o[key]
2215
+ if (v != null && typeof v === 'object' && !Array.isArray(v) && !Buffer.isBuffer(v)) {
2216
+ o[key] = normalizeStatusFieldConflicts(v)
2124
2217
  }
2125
2218
  }
2126
- return String(v)
2219
+ return o
2127
2220
  }
2128
2221
  function normalizeNested(obj) {
2129
2222
  if (!obj || typeof obj !== 'object' || Buffer.isBuffer(obj)) return obj
@@ -2137,11 +2230,52 @@ function _normalizeLogEntryForOpenSearch(doc) {
2137
2230
  }
2138
2231
  return o
2139
2232
  }
2140
- const out = normalizeNested(doc)
2141
- if (out.meta != null && typeof out.meta === 'object') {
2142
- out.meta = normalizeNested(out.meta)
2233
+ /**
2234
+ * Conflitos de dynamic mapping no OpenSearch (campos restantes após serializeVolatileFieldsToJson):
2235
+ * - campo text + valor object → serializa como JSON string
2236
+ */
2237
+ function isPlainLeafObject(v) {
2238
+ if (v == null || typeof v !== 'object' || Array.isArray(v) || Buffer.isBuffer(v)) return false
2239
+ return Object.values(v).every(val => {
2240
+ if (val == null) return true
2241
+ if (typeof val !== 'object') return true
2242
+ if (Array.isArray(val)) return val.every(x => x == null || typeof x !== 'object')
2243
+ return false
2244
+ })
2143
2245
  }
2144
- return out
2246
+ /**
2247
+ * Campos restantes (fora da lista volátil): plain objects → JSON string.
2248
+ */
2249
+ function normalizeDynamicMappingConflicts(obj) {
2250
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj) || Buffer.isBuffer(obj)) return obj
2251
+ const textCompatObjectKeys = new Set(['errorDetails', 'additionalInfo'])
2252
+ const o = { ...obj }
2253
+ for (const key of Object.keys(o)) {
2254
+ const v = o[key]
2255
+ if (v == null) continue
2256
+ const keepObjectSubfields = key === 'error' || key === 'service'
2257
+ if (Array.isArray(v)) {
2258
+ o[key] = v.map(item => {
2259
+ if (item != null && typeof item === 'object' && !Buffer.isBuffer(item) && !Array.isArray(item)) {
2260
+ return isPlainLeafObject(item) ? coerceToString(item) : normalizeDynamicMappingConflicts(item)
2261
+ }
2262
+ return item
2263
+ })
2264
+ } else if (typeof v === 'object' && !Buffer.isBuffer(v)) {
2265
+ if (keepObjectSubfields) {
2266
+ o[key] = normalizeDynamicMappingConflicts(v)
2267
+ } else if (textCompatObjectKeys.has(key) || isPlainLeafObject(v)) {
2268
+ o[key] = coerceToString(v)
2269
+ } else {
2270
+ o[key] = normalizeDynamicMappingConflicts(v)
2271
+ }
2272
+ }
2273
+ }
2274
+ return o
2275
+ }
2276
+ return _prepareDocForOpenSearch(
2277
+ normalizeStatusFieldConflicts(normalizeDynamicMappingConflicts(normalizeNested(doc)))
2278
+ )
2145
2279
  }
2146
2280
 
2147
2281
  const _openSearchWriteQueue = []
@@ -2282,7 +2416,8 @@ function _drainOpenSearchQueue() {
2282
2416
  const job = _openSearchWriteQueue.shift()
2283
2417
  if (!job) break
2284
2418
  _openSearchWriteInFlight++
2285
- axios.post(`${job.osUrl}/${job.indexName}/_doc`, job.logEntry, {
2419
+ const payload = _prepareDocForOpenSearch(job.logEntry)
2420
+ axios.post(`${job.osUrl}/${job.indexName}/_doc`, payload, {
2286
2421
  headers: { 'Content-Type': 'application/json' },
2287
2422
  timeout: _openSearchWriteTimeout
2288
2423
  })
@@ -2319,6 +2454,23 @@ function _drainOpenSearchQueue() {
2319
2454
  }
2320
2455
  })
2321
2456
  .catch((error) => {
2457
+ const status = error?.response?.status
2458
+ const rawMsg = error?.response?.data?.error?.reason ||
2459
+ error?.response?.data?.error?.type ||
2460
+ error?.response?.data?.message ||
2461
+ error?.message ||
2462
+ 'Erro desconhecido'
2463
+ const errorMsg = typeof rawMsg === 'string' ? rawMsg.slice(0, 800) : String(rawMsg).slice(0, 800)
2464
+ if (status === 400 && !job._mappingRetry && _isOpenSearchMappingError(errorMsg)) {
2465
+ job._mappingRetry = true
2466
+ let retried = _prepareDocForOpenSearch(JSON.parse(JSON.stringify(job.logEntry)))
2467
+ const conflictRoot = _extractMappingConflictRootField(errorMsg)
2468
+ if (conflictRoot) retried = _forceRootFieldToJson(retried, conflictRoot)
2469
+ job.logEntry = retried
2470
+ console.warn('[opensearch] Reenviando log com allowlist+*Json após mapping error:', job.indexName, conflictRoot || '')
2471
+ _openSearchWriteQueue.unshift(job)
2472
+ return
2473
+ }
2322
2474
  _openSearchFailuresInRow++
2323
2475
  if (_openSearchFailuresInRow >= _openSearchCircuitThreshold) {
2324
2476
  _openSearchCircuitOpenUntil = Date.now() + _openSearchCircuitPauseMs
@@ -2331,13 +2483,6 @@ function _drainOpenSearchQueue() {
2331
2483
  if (_openSearchCircuitRetryTimer.unref) _openSearchCircuitRetryTimer.unref()
2332
2484
  }
2333
2485
  }
2334
- const status = error?.response?.status
2335
- const rawMsg = error?.response?.data?.error?.reason ||
2336
- error?.response?.data?.error?.type ||
2337
- error?.response?.data?.message ||
2338
- error?.message ||
2339
- 'Erro desconhecido'
2340
- const errorMsg = typeof rawMsg === 'string' ? rawMsg.slice(0, 800) : String(rawMsg).slice(0, 800)
2341
2486
  const logPayload = {
2342
2487
  index: job.indexName,
2343
2488
  serviceName: job.serviceName,
@@ -2630,6 +2775,7 @@ async function startServer() {
2630
2775
 
2631
2776
  app.listen(port, () => {
2632
2777
  console.log('[startup] server listening on port', port, '- /health ready')
2778
+ console.log('[startup] OpenSearch normalizer: volatile-*-json (req/headers/params/meta → *Json)')
2633
2779
  setTimeout(() => writeOtelCollectorConfigIfEnabled().catch(() => {}), 3000)
2634
2780
  setTimeout(() => {
2635
2781
  fetchGrafanaOrgs()
@@ -0,0 +1,222 @@
1
+ 'use strict'
2
+
3
+ const SENSITIVE_FIELD_KEYS_BASE = new Set([
4
+ 'password',
5
+ 'passwd',
6
+ 'pwd',
7
+ 'token',
8
+ 'secret',
9
+ 'apikey',
10
+ 'api_key',
11
+ 'accesstoken',
12
+ 'access_token',
13
+ 'refreshtoken',
14
+ 'refresh_token',
15
+ 'clientsecret',
16
+ 'client_secret',
17
+ 'client_id',
18
+ 'secret_id',
19
+ 'credential',
20
+ 'credentials',
21
+ 'authorization',
22
+ 'creditcard',
23
+ 'credit_card',
24
+ 'cardnumber',
25
+ 'card_number',
26
+ 'cvv',
27
+ 'cvc',
28
+ 'privatekey',
29
+ 'private_key',
30
+ 'csrf',
31
+ 'session',
32
+ 'id_token',
33
+ 'bearer'
34
+ ])
35
+
36
+ /** Chaves lowercase adicionadas em runtime pelo env */
37
+ function getExtraSensitiveKeys() {
38
+ const raw = process.env.AZIFY_LOGGER_SENSITIVE_BODY_KEYS
39
+ if (!raw || typeof raw !== 'string') return EMPTY_EXTRA
40
+ if (_cachedExtraRaw === raw && _cachedExtraSet) return _cachedExtraSet
41
+ const set = new Set()
42
+ for (const part of raw.split(',')) {
43
+ const k = String(part).trim().toLowerCase()
44
+ if (k) set.add(k)
45
+ }
46
+ _cachedExtraRaw = raw
47
+ _cachedExtraSet = set
48
+ return set
49
+ }
50
+
51
+ const EMPTY_EXTRA = new Set()
52
+ let _cachedExtraRaw = null
53
+ let _cachedExtraSet = null
54
+
55
+ function fieldKeyLooksSensitive(lower) {
56
+ if (!lower) return false
57
+ if (SENSITIVE_FIELD_KEYS_BASE.has(lower)) return true
58
+ if (getExtraSensitiveKeys().has(lower)) return true
59
+ if (lower.includes('password') || lower.includes('passwd')) return true
60
+ if (lower.includes('secret')) return true
61
+ if (lower.includes('credential')) return true
62
+ if (lower === 'authorization' || lower.endsWith('_token') || /^[a-z0-9]+token$/i.test(lower)) {
63
+ return true
64
+ }
65
+ return false
66
+ }
67
+
68
+ function sanitizeObjectOrArray(body) {
69
+ if (body == null) return body
70
+
71
+ try {
72
+ if (Array.isArray(body)) {
73
+ const out = []
74
+ for (let i = 0; i < body.length; i++) {
75
+ const v = body[i]
76
+ if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
77
+ out.push(sanitizeObjectOrArray(v))
78
+ } else {
79
+ out.push(v)
80
+ }
81
+ }
82
+ return out
83
+ }
84
+
85
+ if (typeof body !== 'object') {
86
+ return body
87
+ }
88
+
89
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(body)) {
90
+ return body
91
+ }
92
+
93
+ const sanitized = {}
94
+ for (const key in body) {
95
+ if (!Object.prototype.hasOwnProperty.call(body, key)) continue
96
+ const lower = String(key).toLowerCase()
97
+
98
+ if (fieldKeyLooksSensitive(lower)) {
99
+ sanitized[key] = '***'
100
+ } else {
101
+ const v = body[key]
102
+ if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
103
+ sanitized[key] = sanitizeObjectOrArray(v)
104
+ } else {
105
+ sanitized[key] = v
106
+ }
107
+ }
108
+ }
109
+ return sanitized
110
+ } catch (_) {
111
+ return body
112
+ }
113
+ }
114
+
115
+ function sanitizeSensitiveBodyForHttpLog(value, maxChars) {
116
+ const mc =
117
+ typeof maxChars === 'number' && Number.isFinite(maxChars) && maxChars > 0 ? maxChars : 5000
118
+
119
+ function clip(s) {
120
+ if (typeof s !== 'string' || !s.length) return s
121
+ return s.length > mc ? s.slice(0, mc) : s
122
+ }
123
+
124
+ try {
125
+ if (value === null || value === undefined) return value
126
+
127
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
128
+ return sanitizeSensitiveBodyForHttpLog(value.toString('utf8'), maxChars)
129
+ }
130
+
131
+ if (typeof value === 'object' && !(value instanceof Date)) {
132
+ const sanitized = sanitizeObjectOrArray(value)
133
+ return clip(JSON.stringify(sanitized))
134
+ }
135
+
136
+ if (typeof value !== 'string') {
137
+ return clip(String(value))
138
+ }
139
+
140
+ const t = value.trim()
141
+ const looksJson = (t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))
142
+ if (looksJson && t.length <= mc + 4096) {
143
+ try {
144
+ const parsed = JSON.parse(value)
145
+ if (parsed !== null && typeof parsed === 'object') {
146
+ const sanitized = sanitizeObjectOrArray(parsed)
147
+ return clip(JSON.stringify(sanitized))
148
+ }
149
+ } catch (_) {
150
+ /* manter texto bruto cortado */
151
+ }
152
+ }
153
+
154
+ return clip(value)
155
+ } catch (_) {
156
+ try {
157
+ return clip(typeof value === 'string' ? value : String(value))
158
+ } catch (_) {
159
+ return '[unreadable]'
160
+ }
161
+ }
162
+ }
163
+
164
+ function sanitizeBodyJsonValue(body) {
165
+ if (body == null || typeof body !== 'object') {
166
+ if (typeof body === 'string') {
167
+ try {
168
+ const trimmed = body.trim()
169
+ const looksJson = (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))
170
+ if (!looksJson) return body
171
+ const parsed = JSON.parse(body)
172
+ if (parsed !== null && typeof parsed === 'object') {
173
+ return sanitizeObjectOrArray(parsed)
174
+ }
175
+ } catch (_) {
176
+ return body
177
+ }
178
+ }
179
+ return body
180
+ }
181
+
182
+ try {
183
+ return sanitizeObjectOrArray(body)
184
+ } catch (_) {
185
+ return body
186
+ }
187
+ }
188
+
189
+ function getHttpLogBodyMaxChars() {
190
+ return Math.min(
191
+ Math.max(parseInt(String(process.env.AZIFY_LOGGER_HTTP_BODY_MAX_CHARS || '5000'), 10) || 5000, 100),
192
+ 100000
193
+ )
194
+ }
195
+
196
+ function sanitizeOutboundHttpMetaBodies(meta, maxChars) {
197
+ const mc =
198
+ typeof maxChars === 'number' && Number.isFinite(maxChars) && maxChars > 0 ? maxChars : getHttpLogBodyMaxChars()
199
+
200
+ if (!meta || typeof meta !== 'object') return meta
201
+
202
+ try {
203
+ const out = { ...meta }
204
+ if (Object.prototype.hasOwnProperty.call(out, 'requestBody') && out.requestBody !== undefined) {
205
+ out.requestBody = sanitizeSensitiveBodyForHttpLog(out.requestBody, mc)
206
+ }
207
+ if (Object.prototype.hasOwnProperty.call(out, 'responseBody') && out.responseBody !== undefined) {
208
+ out.responseBody = sanitizeSensitiveBodyForHttpLog(out.responseBody, mc)
209
+ }
210
+ return out
211
+ } catch (_) {
212
+ return meta
213
+ }
214
+ }
215
+
216
+ module.exports = {
217
+ sanitizeSensitiveBodyForHttpLog,
218
+ sanitizeBodyJsonValue,
219
+ sanitizeObjectOrArray,
220
+ getHttpLogBodyMaxChars,
221
+ sanitizeOutboundHttpMetaBodies
222
+ }
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ function maxBodyChars() {
4
+ return Math.min(
5
+ Math.max(parseInt(String(process.env.AZIFY_LOGGER_HTTP_BODY_MAX_CHARS || '5000'), 10) || 5000, 100),
6
+ 100000
7
+ )
8
+ }
9
+
10
+ function clip(s) {
11
+ const m = maxBodyChars()
12
+ if (typeof s !== 'string' || !s.length) return null
13
+ return s.length > m ? s.slice(0, m) : s
14
+ }
15
+
16
+ /** undici: request(url, opts) ou request(opts) — retorna o objeto de opções com body/method. */
17
+ function resolveUndiciRequestOpts(url, options) {
18
+ if (typeof url === 'object' && url !== null && !(url instanceof URL)) {
19
+ return url
20
+ }
21
+ return options && typeof options === 'object' ? options : {}
22
+ }
23
+
24
+ function serializeUndiciRequestBody(opts) {
25
+ if (!opts || opts.body == null || opts.body === undefined) return null
26
+ try {
27
+ if (typeof opts.body === 'string') return clip(opts.body)
28
+ if (Buffer.isBuffer(opts.body)) return clip(opts.body.toString('utf8'))
29
+ if (opts.body && typeof opts.body.pipe === 'function') return '[Stream]'
30
+ return clip(String(opts.body))
31
+ } catch (_) {
32
+ return null
33
+ }
34
+ }
35
+
36
+ function responseReadTimeoutMs() {
37
+ return Math.min(
38
+ Math.max(parseInt(String(process.env.AZIFY_LOGGER_HTTP_RESPONSE_READ_MS || '4000'), 10) || 4000, 200),
39
+ 30000
40
+ )
41
+ }
42
+
43
+ /**
44
+ * Lê o body da resposta undici para log e substitui `data.body` por um objeto com .text()/.json()
45
+ * compatível com o que o undici expõe, para o chamador ainda conseguir fazer parse.
46
+ */
47
+ async function restoreUndiciResponseBodyAfterReadForLog(data) {
48
+ if (!data || !data.body || typeof data.body.text !== 'function') {
49
+ return { logStr: null, data }
50
+ }
51
+ const timeoutMs = responseReadTimeoutMs()
52
+ try {
53
+ const text = await Promise.race([
54
+ data.body.text(),
55
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs))
56
+ ])
57
+ if (text == null || typeof text !== 'string') {
58
+ return { logStr: null, data }
59
+ }
60
+ const logStr = clip(text)
61
+ data.body = {
62
+ async text() {
63
+ return text
64
+ },
65
+ async json() {
66
+ return JSON.parse(text)
67
+ },
68
+ async arrayBuffer() {
69
+ return Buffer.from(text, 'utf8').buffer
70
+ }
71
+ }
72
+ return { logStr, data }
73
+ } catch (_) {
74
+ return { logStr: null, data }
75
+ }
76
+ }
77
+
78
+ module.exports = {
79
+ resolveUndiciRequestOpts,
80
+ serializeUndiciRequestBody,
81
+ restoreUndiciResponseBodyAfterReadForLog,
82
+ maxBodyChars
83
+ }