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/package.json +14 -11
- package/register-http-client-early.js +135 -25
- package/register-otel.js +6 -6
- package/register.js +170 -48
- package/scripts/redis-worker.js +7 -42
- package/server.js +167 -21
- package/utils/sanitizeSensitiveHttpBody.js +222 -0
- package/utils/undiciLogBodies.js +83 -0
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
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
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
|
|
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
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|