azify-logger 1.0.56 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azify-logger",
3
- "version": "1.0.56",
3
+ "version": "1.0.57",
4
4
  "description": "Azify Logger Client - Centralized logging for OpenSearch",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -41,14 +41,14 @@
41
41
  "dependencies": {
42
42
  "@azure/storage-blob": "^12.17.0",
43
43
  "@opentelemetry/api": "^1.9.0",
44
- "@opentelemetry/core": "1.25.0",
45
- "@opentelemetry/instrumentation-express": "0.52.0",
46
- "@opentelemetry/instrumentation-http": "0.52.0",
47
- "@opentelemetry/instrumentation-restify": "0.52.0",
48
- "@opentelemetry/resources": "1.25.0",
49
- "@opentelemetry/sdk-node": "0.52.0",
50
- "@opentelemetry/sdk-trace-base": "1.25.0",
51
- "@opentelemetry/semantic-conventions": "1.25.0",
44
+ "@opentelemetry/core": "^2.7.1",
45
+ "@opentelemetry/instrumentation-express": "^0.66.0",
46
+ "@opentelemetry/instrumentation-http": "^0.218.0",
47
+ "@opentelemetry/instrumentation-restify": "^0.63.0",
48
+ "@opentelemetry/resources": "^2.7.1",
49
+ "@opentelemetry/sdk-node": "^0.218.0",
50
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
51
+ "@opentelemetry/semantic-conventions": "^1.41.1",
52
52
  "adm-zip": "^0.5.16",
53
53
  "archiver": "^4.0.2",
54
54
  "axios": "^1.7.9",
@@ -63,7 +63,7 @@
63
63
  "openid-client": "^5.7.1",
64
64
  "passport": "^0.7.0",
65
65
  "require-in-the-middle": "^7.4.0",
66
- "uuid": "^9.0.1"
66
+ "uuid": "^11.1.1"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "ioredis": "^5.8.0"
@@ -78,7 +78,8 @@
78
78
  },
79
79
  "overrides": {
80
80
  "semver": "^7.5.2",
81
- "glob": "^13.0.0"
81
+ "glob": "^13.0.0",
82
+ "uuid": "^11.1.1"
82
83
  },
83
84
  "engines": {
84
85
  "node": ">=20 <=22"
@@ -112,6 +113,7 @@
112
113
  "scripts/redis-worker.js",
113
114
  "streams/",
114
115
  "utils/undiciLogBodies.js",
116
+ "utils/sanitizeSensitiveHttpBody.js",
115
117
  "package.json",
116
118
  "README.md"
117
119
  ]
package/register-otel.js CHANGED
@@ -5,12 +5,12 @@ function normalizeServiceNameForTraces(s) {
5
5
 
6
6
  try {
7
7
  const { NodeSDK } = require('@opentelemetry/sdk-node')
8
- const { AlwaysOnSampler } = require('@opentelemetry/core')
8
+ const { AlwaysOnSampler } = require('@opentelemetry/sdk-trace-base')
9
9
  const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http')
10
10
  const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express')
11
11
  const { RestifyInstrumentation } = require('@opentelemetry/instrumentation-restify')
12
- const { Resource } = require('@opentelemetry/resources')
13
- const { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions')
12
+ const { resourceFromAttributes } = require('@opentelemetry/resources')
13
+ const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions')
14
14
 
15
15
  const rawName = process.env.APP_NAME || process.env.OTEL_SERVICE_NAME || process.env.SERVICE_NAME || 'app'
16
16
  const serviceName = normalizeServiceNameForTraces(rawName)
@@ -157,9 +157,9 @@ try {
157
157
  }
158
158
 
159
159
  const sdk = new NodeSDK({
160
- resource: new Resource({
161
- [SEMRESATTRS_SERVICE_NAME]: serviceName,
162
- [SEMRESATTRS_SERVICE_VERSION]: serviceVersion
160
+ resource: resourceFromAttributes({
161
+ [ATTR_SERVICE_NAME]: serviceName,
162
+ [ATTR_SERVICE_VERSION]: serviceVersion
163
163
  }),
164
164
  spanProcessor,
165
165
  sampler: new AlwaysOnSampler(),
package/register.js CHANGED
@@ -25,6 +25,7 @@ try {
25
25
  const os = require('os')
26
26
 
27
27
  const { shouldSample, markSource, HTTP_CLIENT_MODE } = require('./sampling')
28
+ const { sanitizeOutboundHttpMetaBodies } = require('./utils/sanitizeSensitiveHttpBody')
28
29
 
29
30
  if (process.env.AZIFY_LOGGER_DEBUG === '1') {
30
31
  try { process.stderr.write('[azify-logger] REGISTER step1b after requires\n') } catch (_) {}
@@ -32,13 +33,17 @@ try {
32
33
  }
33
34
  const serviceName = process.env.APP_NAME
34
35
  const environment = process.env.NODE_ENV
35
- const loggerUrlFromEnv = process.env.AZIFY_LOGGER_URL
36
-
36
+ const loggerUrlRaw = (process.env.AZIFY_LOGGER_URL || '').trim()
37
+ if (!loggerUrlRaw) {
38
+ process.stderr.write('[azify-logger] AZIFY_LOGGER_URL não está definida.\n')
39
+ throw new Error('AZIFY_LOGGER_URL is required')
40
+ }
37
41
  let loggerEndpoint
38
42
  try {
39
- loggerEndpoint = new URL(loggerUrlFromEnv)
40
- } catch (_) {
41
- loggerEndpoint = new URL('http://localhost:3001/log')
43
+ loggerEndpoint = new URL(loggerUrlRaw)
44
+ } catch (urlErr) {
45
+ process.stderr.write(`[azify-logger] AZIFY_LOGGER_URL inválida: ${loggerUrlRaw} (${urlErr.message})\n`)
46
+ throw urlErr
42
47
  }
43
48
 
44
49
  const loggerUrlString = loggerEndpoint.toString()
@@ -176,6 +181,9 @@ try {
176
181
  const fromMsg = extractUrlFromReqResMessage(msgStr)
177
182
  meta = fromMsg ? { ...meta, url: fromMsg } : { ...meta, url: OPAQUE_NO_URL }
178
183
  }
184
+ try {
185
+ meta = sanitizeOutboundHttpMetaBodies(meta)
186
+ } catch (_) {}
179
187
  }
180
188
  if (isReqRes && meta && meta.url && isLoggerApiCall({ url: meta.url })) return
181
189
  const source = meta && meta.__source
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const path = require('path')
3
4
  const os = require('os')
4
5
  const http = require('http')
5
6
  const https = require('https')
@@ -159,24 +160,6 @@ const SENSITIVE_HEADER_KEYS = new Set([
159
160
  'x-timestamp'
160
161
  ])
161
162
 
162
- const SENSITIVE_BODY_FIELDS = new Set([
163
- 'password',
164
- 'token',
165
- 'secret',
166
- 'apiKey',
167
- 'api_key',
168
- 'accessToken',
169
- 'access_token',
170
- 'refreshToken',
171
- 'refresh_token',
172
- 'clientSecret',
173
- 'client_secret',
174
- 'creditCard',
175
- 'credit_card',
176
- 'cvv',
177
- 'cvc'
178
- ])
179
-
180
163
  function sanitizeHeaders(headers) {
181
164
  if (!headers || typeof headers !== 'object') {
182
165
  return {}
@@ -195,31 +178,10 @@ function sanitizeHeaders(headers) {
195
178
  return sanitized
196
179
  }
197
180
 
181
+ const sanitizeBodyJsonValue = require(path.join(__dirname, '..', 'utils', 'sanitizeSensitiveHttpBody')).sanitizeBodyJsonValue
182
+
198
183
  function sanitizeBody(body) {
199
- if (!body || typeof body !== 'object') {
200
- return body
201
- }
202
-
203
- try {
204
- const sanitized = Array.isArray(body) ? [] : {}
205
-
206
- for (const key in body) {
207
- if (!body.hasOwnProperty(key)) continue
208
- const lower = String(key).toLowerCase()
209
-
210
- if (SENSITIVE_BODY_FIELDS.has(lower) || lower.includes('password') || lower.includes('secret')) {
211
- sanitized[key] = '***'
212
- } else if (typeof body[key] === 'object' && body[key] !== null) {
213
- sanitized[key] = sanitizeBody(body[key])
214
- } else {
215
- sanitized[key] = body[key]
216
- }
217
- }
218
-
219
- return sanitized
220
- } catch (err) {
221
- return body
222
- }
184
+ return sanitizeBodyJsonValue(body)
223
185
  }
224
186
 
225
187
  function sanitizePayload(payload) {
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
+ }