azify-logger 1.0.44 → 1.0.46
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/index.js +84 -7
- package/init.js +21 -11
- package/middleware-express.js +46 -44
- package/middleware-pino.js +19 -10
- package/package.json +10 -3
- package/preload.js +15 -0
- package/register-http-client-early.js +816 -0
- package/register.js +1232 -366
- package/sampling.js +1 -1
- package/server.js +192 -16
- package/store.d.ts +68 -0
- package/store.js +190 -11
- package/streams/httpQueue.js +24 -12
- package/streams/pino.js +45 -10
package/sampling.js
CHANGED
|
@@ -45,7 +45,7 @@ const sourceSampleRates = {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const shouldSample = (level, source) => {
|
|
48
|
-
if (source === 'http-client') {
|
|
48
|
+
if (source === 'http-client' || source === 'pino-stdout') {
|
|
49
49
|
return true
|
|
50
50
|
}
|
|
51
51
|
const key = typeof level === 'string' ? level.toLowerCase() : 'info'
|
package/server.js
CHANGED
|
@@ -227,6 +227,7 @@ async function ensureIndexTemplate() {
|
|
|
227
227
|
userAgent: { type: 'text' },
|
|
228
228
|
environment: { type: 'keyword' },
|
|
229
229
|
hostname: { type: 'keyword' },
|
|
230
|
+
source: { type: 'keyword' },
|
|
230
231
|
responseBody: { type: 'text' },
|
|
231
232
|
error: {
|
|
232
233
|
properties: {
|
|
@@ -238,7 +239,7 @@ async function ensureIndexTemplate() {
|
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
},
|
|
241
|
-
priority:
|
|
242
|
+
priority: 1000
|
|
242
243
|
})
|
|
243
244
|
} catch (error) {
|
|
244
245
|
}
|
|
@@ -1696,14 +1697,14 @@ async function registerAlertRulesForApp(appName, rules, options = {}) {
|
|
|
1696
1697
|
}
|
|
1697
1698
|
|
|
1698
1699
|
function generateTraceId() {
|
|
1699
|
-
return
|
|
1700
|
-
Math.random().toString(36).substring(2, 15)
|
|
1700
|
+
return require('crypto').randomBytes(16).toString('hex')
|
|
1701
1701
|
}
|
|
1702
1702
|
|
|
1703
1703
|
function generateSpanId() {
|
|
1704
|
-
return
|
|
1704
|
+
return require('crypto').randomBytes(8).toString('hex')
|
|
1705
1705
|
}
|
|
1706
1706
|
|
|
1707
|
+
|
|
1707
1708
|
function getOrCreateTraceContext(requestId) {
|
|
1708
1709
|
if (traceContextMap.has(requestId)) {
|
|
1709
1710
|
return traceContextMap.get(requestId)
|
|
@@ -1836,21 +1837,55 @@ async function handleLog(req, res) {
|
|
|
1836
1837
|
))
|
|
1837
1838
|
)
|
|
1838
1839
|
|
|
1839
|
-
|
|
1840
|
+
const isRequestOrResponse = messageLower.includes('[request]') || messageLower.includes('[response]')
|
|
1841
|
+
const isAzifyHttpClientLog = (meta && meta.source === 'http-client') || isRequestOrResponse
|
|
1842
|
+
if (!isAzifyHttpClientLog && shouldFilterLog) {
|
|
1840
1843
|
return res.json({ success: true, message: 'Log filtrado' })
|
|
1841
1844
|
}
|
|
1845
|
+
if (isRequestOrResponse && process.env.AZIFY_LOGGER_DEBUG === '1') {
|
|
1846
|
+
console.log('[azify-logger][server] REQUEST/RESPONSE received:', (typeof message === 'string' ? message : '').slice(0, 80))
|
|
1847
|
+
}
|
|
1848
|
+
if (isRequestOrResponse && meta && typeof meta === 'object' && meta.source !== 'http-client') {
|
|
1849
|
+
meta = { ...meta, source: 'http-client' }
|
|
1850
|
+
}
|
|
1842
1851
|
|
|
1843
1852
|
const requestId = meta && meta.requestId
|
|
1844
1853
|
|
|
1854
|
+
function toTraceIdHex32(val) {
|
|
1855
|
+
if (val == null || typeof val !== 'string') return null
|
|
1856
|
+
const hex = String(val).replace(/[^0-9a-fA-F]/g, '').slice(0, 32)
|
|
1857
|
+
return hex ? hex.padStart(32, '0').slice(0, 32) : null
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1845
1860
|
let traceContext = null
|
|
1846
1861
|
|
|
1847
|
-
if (meta && meta.traceId
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1862
|
+
if (meta && meta.traceId) {
|
|
1863
|
+
const tid = toTraceIdHex32(meta.traceId)
|
|
1864
|
+
if (tid) {
|
|
1865
|
+
traceContext = {
|
|
1866
|
+
traceId: tid,
|
|
1867
|
+
spanId: meta.spanId != null && meta.spanId !== '' ? String(meta.spanId).slice(0, 16) : generateSpanId(),
|
|
1868
|
+
parentSpanId: meta.parentSpanId != null && meta.parentSpanId !== '' ? String(meta.parentSpanId) : null
|
|
1869
|
+
}
|
|
1852
1870
|
}
|
|
1853
|
-
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
if (!traceContext && typeof message === 'string' && /traceId/i.test(message)) {
|
|
1874
|
+
const m = message.match(/"traceId"\s*:\s*"([^"]+)"/)
|
|
1875
|
+
const s = message.match(/"spanId"\s*:\s*"([^"]+)"/)
|
|
1876
|
+
if (m && m[1]) {
|
|
1877
|
+
const tid = toTraceIdHex32(m[1])
|
|
1878
|
+
if (tid) {
|
|
1879
|
+
traceContext = {
|
|
1880
|
+
traceId: tid,
|
|
1881
|
+
spanId: (s && s[1]) ? String(s[1]).slice(0, 16) : generateSpanId(),
|
|
1882
|
+
parentSpanId: null
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (!traceContext) {
|
|
1854
1889
|
try {
|
|
1855
1890
|
const extractedCtx = propagation.extract(context.active(), req.headers, {
|
|
1856
1891
|
get (carrier, key) {
|
|
@@ -1867,8 +1902,8 @@ async function handleLog(req, res) {
|
|
|
1867
1902
|
const spanContext = (span && span.spanContext && span.spanContext()) || null
|
|
1868
1903
|
if (spanContext && spanContext.traceId && spanContext.spanId) {
|
|
1869
1904
|
traceContext = {
|
|
1870
|
-
traceId: spanContext.traceId,
|
|
1871
|
-
spanId: spanContext.spanId,
|
|
1905
|
+
traceId: toTraceIdHex32(spanContext.traceId) || String(spanContext.traceId).replace(/-/g, '').padStart(32, '0').slice(0, 32),
|
|
1906
|
+
spanId: String(spanContext.spanId).slice(0, 16),
|
|
1872
1907
|
parentSpanId: null
|
|
1873
1908
|
}
|
|
1874
1909
|
}
|
|
@@ -1991,6 +2026,16 @@ async function handleLog(req, res) {
|
|
|
1991
2026
|
value = truncateBody(value, false)
|
|
1992
2027
|
}
|
|
1993
2028
|
logEntry[key] = value
|
|
2029
|
+
} else if (key === 'error') {
|
|
2030
|
+
if (value != null && typeof value === 'object' && !Array.isArray(value) && (value.message != null || value.stack != null || value.name != null)) {
|
|
2031
|
+
logEntry.error = {
|
|
2032
|
+
message: value.message != null ? String(value.message) : '',
|
|
2033
|
+
stack: value.stack != null ? String(value.stack) : undefined,
|
|
2034
|
+
name: value.name != null ? String(value.name) : undefined
|
|
2035
|
+
}
|
|
2036
|
+
} else if (value != null) {
|
|
2037
|
+
logEntry.error = { message: String(value) }
|
|
2038
|
+
}
|
|
1994
2039
|
} else {
|
|
1995
2040
|
logEntry[key] = value
|
|
1996
2041
|
}
|
|
@@ -1999,6 +2044,7 @@ async function handleLog(req, res) {
|
|
|
1999
2044
|
}
|
|
2000
2045
|
|
|
2001
2046
|
logEntry.message = message
|
|
2047
|
+
if (isRequestOrResponse) logEntry.source = 'http-client'
|
|
2002
2048
|
|
|
2003
2049
|
const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200'
|
|
2004
2050
|
const appName = logEntry.appName || logEntry.service?.name || 'unknown'
|
|
@@ -2008,7 +2054,57 @@ async function handleLog(req, res) {
|
|
|
2008
2054
|
|
|
2009
2055
|
res.json({ success: true, message: 'Log enviado com sucesso', index: indexName })
|
|
2010
2056
|
|
|
2011
|
-
|
|
2057
|
+
const docToSend = _trimLogEntryForOpenSearch(logEntry)
|
|
2058
|
+
_enqueueOpenSearchWrite(osUrl, indexName, docToSend, serviceName)
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
const _opensearchMaxFields = Math.min(Number(process.env.AZIFY_LOGGER_OPENSEARCH_MAX_FIELDS) || 0, 2000) || 900
|
|
2062
|
+
|
|
2063
|
+
function _countFields(obj, depth = 0) {
|
|
2064
|
+
if (depth > 15) return 0
|
|
2065
|
+
if (obj == null || typeof obj !== 'object') return 1
|
|
2066
|
+
let n = 0
|
|
2067
|
+
for (const key of Object.keys(obj)) {
|
|
2068
|
+
const v = obj[key]
|
|
2069
|
+
if (v != null && typeof v === 'object' && !Array.isArray(v) && !Buffer.isBuffer(v)) n += _countFields(v, depth + 1)
|
|
2070
|
+
else n += 1
|
|
2071
|
+
}
|
|
2072
|
+
return n
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
function _stringifyValue(value) {
|
|
2076
|
+
try {
|
|
2077
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
2078
|
+
} catch (_) {
|
|
2079
|
+
return '[Unable to serialize]'
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function _trimLogEntryForOpenSearch(doc) {
|
|
2084
|
+
if (_countFields(doc) <= _opensearchMaxFields) return doc
|
|
2085
|
+
const out = {}
|
|
2086
|
+
for (const key of Object.keys(doc)) {
|
|
2087
|
+
const value = doc[key]
|
|
2088
|
+
if (value != null && typeof value === 'object' && !Array.isArray(value) && !Buffer.isBuffer(value)) {
|
|
2089
|
+
if (_countFields(value) > 80) {
|
|
2090
|
+
out[key + 'Json'] = _stringifyValue(value)
|
|
2091
|
+
} else {
|
|
2092
|
+
out[key] = value
|
|
2093
|
+
}
|
|
2094
|
+
} else {
|
|
2095
|
+
out[key] = value
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
if (_countFields(out) > _opensearchMaxFields) {
|
|
2099
|
+
for (const k of Object.keys(out)) {
|
|
2100
|
+
const v = out[k]
|
|
2101
|
+
if (v != null && typeof v === 'object' && !Array.isArray(v) && !Buffer.isBuffer(v)) {
|
|
2102
|
+
out[k + 'Json'] = _stringifyValue(v)
|
|
2103
|
+
delete out[k]
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return out
|
|
2012
2108
|
}
|
|
2013
2109
|
|
|
2014
2110
|
const _openSearchWriteQueue = []
|
|
@@ -2026,14 +2122,82 @@ const _logDedupeWindowMs = 5000
|
|
|
2026
2122
|
const _logDedupeMaxKeys = 15000
|
|
2027
2123
|
const _logDedupeSeen = new Map()
|
|
2028
2124
|
const _logDedupeBucketMs = 80
|
|
2125
|
+
const _logDedupeGenericBucketMs = 250
|
|
2126
|
+
|
|
2127
|
+
function _normalizeReqResMessageForDedupe(msg) {
|
|
2128
|
+
const s = typeof msg === 'string' ? msg : String(msg || '')
|
|
2129
|
+
return s.replace(/\s+\d+(\.\d+)?ms\s*$/i, '').trimEnd()
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function _httpClientDedupeFingerprint(logEntry) {
|
|
2133
|
+
if (!logEntry || logEntry.source !== 'http-client') return null
|
|
2134
|
+
const msg = typeof logEntry.message === 'string' ? logEntry.message.substring(0, 500) : String(logEntry.message || '')
|
|
2135
|
+
const msgLower = msg.trimStart().toLowerCase()
|
|
2136
|
+
const isReq = msgLower.startsWith('[request]')
|
|
2137
|
+
const isRes = msgLower.startsWith('[response]')
|
|
2138
|
+
if (!isReq && !isRes) return null
|
|
2139
|
+
|
|
2140
|
+
const methodRaw =
|
|
2141
|
+
(logEntry.method != null && String(logEntry.method).trim()) ||
|
|
2142
|
+
(logEntry.request && logEntry.request.method != null && String(logEntry.request.method).trim()) ||
|
|
2143
|
+
''
|
|
2144
|
+
const m = methodRaw.toUpperCase()
|
|
2145
|
+
let url = ''
|
|
2146
|
+
if (logEntry.url != null && String(logEntry.url).trim()) {
|
|
2147
|
+
url = String(logEntry.url).trim()
|
|
2148
|
+
} else if (logEntry.request && typeof logEntry.request === 'object') {
|
|
2149
|
+
url = String(logEntry.request.url || logEntry.request.path || '').trim()
|
|
2150
|
+
}
|
|
2151
|
+
if (!m || !url) return null
|
|
2152
|
+
|
|
2153
|
+
if (isReq) {
|
|
2154
|
+
return `REQUEST|${m}|${url}`
|
|
2155
|
+
}
|
|
2156
|
+
let st = logEntry.statusCode != null ? String(logEntry.statusCode).trim() : ''
|
|
2157
|
+
if (!st && /\bERROR\b/i.test(msg)) {
|
|
2158
|
+
st = 'ERR'
|
|
2159
|
+
}
|
|
2160
|
+
if (!st) {
|
|
2161
|
+
const mStatus = msg.match(/\s(\d{3})\s+(?:\d+(?:\.\d+)?ms\s*)?$/i)
|
|
2162
|
+
if (mStatus) st = mStatus[1]
|
|
2163
|
+
}
|
|
2164
|
+
return `RESPONSE|${m}|${url}|${st || 'na'}`
|
|
2165
|
+
}
|
|
2029
2166
|
|
|
2030
2167
|
function _logDedupeKey(serviceName, logEntry) {
|
|
2031
2168
|
const msg = typeof logEntry.message === 'string' ? logEntry.message.substring(0, 500) : String(logEntry.message || '')
|
|
2169
|
+
const msgLower = msg.trimStart().toLowerCase()
|
|
2170
|
+
const isReqRes = msgLower.includes('[request]') || msgLower.includes('[response]')
|
|
2032
2171
|
const traceId = logEntry.traceId != null ? String(logEntry.traceId).trim() : ''
|
|
2033
2172
|
const spanId = logEntry.spanId != null ? String(logEntry.spanId).trim() : ''
|
|
2173
|
+
const msgNorm = _normalizeReqResMessageForDedupe(msg)
|
|
2174
|
+
const msgSig = msgNorm.length > 200 ? msgNorm.slice(0, 200) : msgNorm
|
|
2175
|
+
if (isReqRes) {
|
|
2176
|
+
const kind = msgLower.startsWith('[request]')
|
|
2177
|
+
? 'REQUEST'
|
|
2178
|
+
: msgLower.startsWith('[response]')
|
|
2179
|
+
? 'RESPONSE'
|
|
2180
|
+
: (msgLower.includes('[request]') ? 'REQUEST' : 'RESPONSE')
|
|
2181
|
+
const httpFp = _httpClientDedupeFingerprint(logEntry)
|
|
2182
|
+
if (httpFp) {
|
|
2183
|
+
if (traceId) return `rh|${serviceName}|${traceId}|${httpFp}`
|
|
2184
|
+
const requestId = (logEntry.requestId != null ? String(logEntry.requestId).trim() : '') || ''
|
|
2185
|
+
if (requestId) return `rh|${serviceName}|${requestId}|${httpFp}`
|
|
2186
|
+
return null
|
|
2187
|
+
}
|
|
2188
|
+
if (traceId && spanId) return `r|${serviceName}|${traceId}|${spanId}|${kind}|${msgSig}`
|
|
2189
|
+
const ts = logEntry['@timestamp'] != null ? Number(logEntry['@timestamp']) : Date.now()
|
|
2190
|
+
const bucket = Math.floor(ts / _logDedupeBucketMs) * _logDedupeBucketMs
|
|
2191
|
+
return `r|${serviceName}|${bucket}|${kind}|${msgSig}`
|
|
2192
|
+
}
|
|
2034
2193
|
if (traceId && spanId) return `t|${serviceName}|${traceId}|${spanId}|${msg}`
|
|
2035
2194
|
const requestId = (logEntry.requestId != null ? String(logEntry.requestId).trim() : '') || ''
|
|
2036
|
-
if (!requestId)
|
|
2195
|
+
if (!requestId) {
|
|
2196
|
+
const ts = logEntry['@timestamp'] != null ? Number(logEntry['@timestamp']) : Date.now()
|
|
2197
|
+
const bucket = Math.floor(ts / _logDedupeGenericBucketMs) * _logDedupeGenericBucketMs
|
|
2198
|
+
const msgSig = msg.length > 200 ? msg.slice(0, 200) : msg
|
|
2199
|
+
return `g|${serviceName}|${bucket}|${msgSig}`
|
|
2200
|
+
}
|
|
2037
2201
|
const ts = logEntry['@timestamp'] != null ? Number(logEntry['@timestamp']) : Date.now()
|
|
2038
2202
|
const bucket = Math.floor(ts / _logDedupeBucketMs) * _logDedupeBucketMs
|
|
2039
2203
|
return `b|${serviceName}|${requestId}|${msg}|${bucket}`
|
|
@@ -2059,6 +2223,12 @@ function _enqueueOpenSearchWrite(osUrl, indexName, logEntry, serviceName) {
|
|
|
2059
2223
|
if (expiry != null && expiry > now) return
|
|
2060
2224
|
_logDedupeSeen.set(key, now + _logDedupeWindowMs)
|
|
2061
2225
|
}
|
|
2226
|
+
if (process.env.AZIFY_LOGGER_DEBUG === '1') {
|
|
2227
|
+
const msg = typeof logEntry.message === 'string' ? logEntry.message : ''
|
|
2228
|
+
if (msg.toLowerCase().includes('[request]') || msg.toLowerCase().includes('[response]')) {
|
|
2229
|
+
console.log('[azify-logger][server] REQUEST/RESPONSE enqueued for OpenSearch:', msg.slice(0, 70))
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2062
2232
|
_openSearchWriteQueue.push({ osUrl, indexName, logEntry, serviceName })
|
|
2063
2233
|
_drainOpenSearchQueue()
|
|
2064
2234
|
}
|
|
@@ -2081,11 +2251,17 @@ function _drainOpenSearchQueue() {
|
|
|
2081
2251
|
})
|
|
2082
2252
|
.then(() => {
|
|
2083
2253
|
_openSearchFailuresInRow = 0
|
|
2254
|
+
if (process.env.AZIFY_LOGGER_DEBUG === '1') {
|
|
2255
|
+
const msg = typeof job.logEntry.message === 'string' ? job.logEntry.message : ''
|
|
2256
|
+
if (msg.toLowerCase().includes('[request]') || msg.toLowerCase().includes('[response]')) {
|
|
2257
|
+
console.log('[azify-logger][server] REQUEST/RESPONSE written to OpenSearch index=', job.indexName, 'msg=', msg.slice(0, 60))
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2084
2260
|
const SETUP_THROTTLE_MS = 10 * 60 * 1000
|
|
2085
2261
|
if (!handleLog._lastSetupTime) handleLog._lastSetupTime = new Map()
|
|
2086
2262
|
const last = handleLog._lastSetupTime.get(job.serviceName) || 0
|
|
2087
2263
|
const now = Date.now()
|
|
2088
|
-
const shouldRunSetup = job.serviceName !== 'unknown' && job.serviceName !== 'unknown-service' && (now - last >= SETUP_THROTTLE_MS)
|
|
2264
|
+
const shouldRunSetup = job.serviceName !== 'unknown' && job.serviceName !== 'unknown-service' && job.serviceName !== 'unknown-app' && (now - last >= SETUP_THROTTLE_MS)
|
|
2089
2265
|
if (shouldRunSetup) {
|
|
2090
2266
|
handleLog._lastSetupTime.set(job.serviceName, now)
|
|
2091
2267
|
setupGrafanaForApp(job.serviceName).then(() => {
|
package/store.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type AzifyRequestContext = {
|
|
2
|
+
traceId: string
|
|
3
|
+
spanId: string
|
|
4
|
+
parentSpanId: string | null
|
|
5
|
+
requestId: string | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type AzifyJobTraceRecord = {
|
|
9
|
+
traceId: string
|
|
10
|
+
spanId?: string | null
|
|
11
|
+
parentSpanId?: string | null
|
|
12
|
+
requestId?: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export declare const AZIFY_JOB_TRACE_FIELD: '__azifyTraceContext'
|
|
16
|
+
|
|
17
|
+
export function prepareBullJobTraceContext(data: unknown): {
|
|
18
|
+
cleanedForLog: unknown
|
|
19
|
+
ctx: AzifyRequestContext
|
|
20
|
+
dataForDownstream: unknown
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function captureOriginTraceMeta(): {
|
|
24
|
+
originTraceId?: string
|
|
25
|
+
originRequestId?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function addCurrentTraceToBullJobPayload<T>(data: T): T
|
|
29
|
+
|
|
30
|
+
export function traceAttachmentForBullJob(): Record<string, unknown>
|
|
31
|
+
|
|
32
|
+
export function runBullJobHandlerInTraceContext<TJob extends { data: unknown }, R>(
|
|
33
|
+
job: TJob,
|
|
34
|
+
handler: (job: TJob) => R,
|
|
35
|
+
): R
|
|
36
|
+
|
|
37
|
+
export function runInBullJobTraceContext<T>(
|
|
38
|
+
jobData: unknown,
|
|
39
|
+
fn: (x: {
|
|
40
|
+
ctx: AzifyRequestContext
|
|
41
|
+
cleanedForLog: unknown
|
|
42
|
+
dataForDownstream: unknown
|
|
43
|
+
}) => T,
|
|
44
|
+
): T
|
|
45
|
+
|
|
46
|
+
export function runNestBullJobWithTrace<TJob extends { data: unknown }, R>(
|
|
47
|
+
job: TJob,
|
|
48
|
+
fn: (x: {
|
|
49
|
+
job: TJob
|
|
50
|
+
ctx: AzifyRequestContext
|
|
51
|
+
cleanedForLog: unknown
|
|
52
|
+
dataForDownstream: unknown
|
|
53
|
+
}) => R,
|
|
54
|
+
): R
|
|
55
|
+
|
|
56
|
+
export function startRequestContext(
|
|
57
|
+
initial?: Record<string, unknown>,
|
|
58
|
+
): AzifyRequestContext
|
|
59
|
+
export function runWithRequestContext<T>(ctx: AzifyRequestContext, fn: () => T): T
|
|
60
|
+
export function getRequestContext(): AzifyRequestContext | null
|
|
61
|
+
export function getActiveRequestId(): string | null
|
|
62
|
+
export function getLastJobContext(): AzifyRequestContext | null
|
|
63
|
+
export function setLastJobContext(ctx: AzifyRequestContext | null): void
|
|
64
|
+
export function mergeTraceIntoJobPayload(
|
|
65
|
+
data: unknown,
|
|
66
|
+
traceContext: AzifyJobTraceRecord | null,
|
|
67
|
+
): unknown
|
|
68
|
+
export function toTraceIdHex(hex32: string | null | undefined): string
|
package/store.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
const { AsyncLocalStorage } = require('async_hooks')
|
|
2
2
|
|
|
3
|
+
const AZIFY_JOB_TRACE_FIELD = '__azifyTraceContext'
|
|
4
|
+
|
|
3
5
|
const als = new AsyncLocalStorage()
|
|
6
|
+
let lastJobContext = null
|
|
7
|
+
|
|
8
|
+
function setLastJobContext(ctx) {
|
|
9
|
+
lastJobContext = ctx || null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getLastJobContext() {
|
|
13
|
+
return lastJobContext
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
function fastGenerateId(length = 16) {
|
|
6
17
|
const chars = '0123456789abcdef'
|
|
@@ -12,21 +23,32 @@ function fastGenerateId(length = 16) {
|
|
|
12
23
|
return result
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
function newRequestIdForJob() {
|
|
27
|
+
try {
|
|
28
|
+
const { randomUUID } = require('crypto')
|
|
29
|
+
if (typeof randomUUID === 'function') {
|
|
30
|
+
return randomUUID()
|
|
31
|
+
}
|
|
32
|
+
} catch (_) {}
|
|
33
|
+
return `${fastGenerateId(16)}-${fastGenerateId(8)}`
|
|
18
34
|
}
|
|
19
35
|
|
|
20
|
-
function
|
|
36
|
+
function toTraceIdHex(hex32) {
|
|
37
|
+
const raw = hex32 != null && typeof hex32 === 'string' ? String(hex32).replace(/[^0-9a-fA-F]/g, '').slice(0, 32) : ''
|
|
38
|
+
const hex = raw || fastGenerateId(16)
|
|
39
|
+
return hex.padStart(32, '0').slice(0, 32)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function startRequestContext(initial) {
|
|
43
|
+
if (!initial || typeof initial !== 'object') initial = {}
|
|
21
44
|
const traceHex = initial.traceHex || fastGenerateId(16)
|
|
22
45
|
const spanHex = initial.spanHex || fastGenerateId(8)
|
|
23
|
-
|
|
24
|
-
traceId:
|
|
25
|
-
spanId: spanHex,
|
|
26
|
-
parentSpanId: initial.parentSpanId
|
|
27
|
-
requestId: initial.requestId
|
|
46
|
+
return {
|
|
47
|
+
traceId: toTraceIdHex(traceHex),
|
|
48
|
+
spanId: spanHex != null && spanHex !== '' ? String(spanHex).slice(0, 16) : fastGenerateId(8),
|
|
49
|
+
parentSpanId: initial.parentSpanId != null && initial.parentSpanId !== '' ? String(initial.parentSpanId) : null,
|
|
50
|
+
requestId: initial.requestId != null && initial.requestId !== '' ? String(initial.requestId) : null
|
|
28
51
|
}
|
|
29
|
-
return ctx
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
function runWithRequestContext(ctx, fn) {
|
|
@@ -37,11 +59,168 @@ function getRequestContext() {
|
|
|
37
59
|
return als.getStore() || null
|
|
38
60
|
}
|
|
39
61
|
|
|
62
|
+
function getActiveRequestId() {
|
|
63
|
+
const ctx = getRequestContext()
|
|
64
|
+
if (!ctx?.requestId || String(ctx.requestId) === '') return null
|
|
65
|
+
return String(ctx.requestId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function captureOriginTraceMeta() {
|
|
69
|
+
try {
|
|
70
|
+
const ctx = getRequestContext() || getLastJobContext()
|
|
71
|
+
if (!ctx || typeof ctx !== 'object') return {}
|
|
72
|
+
const out = {}
|
|
73
|
+
if (ctx.traceId != null && String(ctx.traceId) !== '') {
|
|
74
|
+
out.originTraceId = String(ctx.traceId)
|
|
75
|
+
}
|
|
76
|
+
if (ctx.requestId != null && String(ctx.requestId) !== '') {
|
|
77
|
+
out.originRequestId = String(ctx.requestId)
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
} catch (_) {
|
|
81
|
+
return {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mergeTraceIntoJobPayload(data, traceContext) {
|
|
86
|
+
if (!traceContext || !traceContext.traceId) return data
|
|
87
|
+
const base = data && typeof data === 'object' && !Array.isArray(data) ? data : {}
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
traceId: traceContext.traceId,
|
|
91
|
+
spanId: traceContext.spanId ?? null,
|
|
92
|
+
parentSpanId: traceContext.parentSpanId ?? null,
|
|
93
|
+
requestId: traceContext.requestId ?? null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizedJobTrace(raw) {
|
|
98
|
+
if (!raw || typeof raw !== 'object' || typeof raw.traceId !== 'string') return null
|
|
99
|
+
return {
|
|
100
|
+
traceId: String(raw.traceId),
|
|
101
|
+
spanId: raw.spanId != null ? raw.spanId : null,
|
|
102
|
+
parentSpanId: raw.parentSpanId != null ? raw.parentSpanId : null,
|
|
103
|
+
requestId: raw.requestId != null && raw.requestId !== '' ? String(raw.requestId) : null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function alsContextFromJobTrace(trace) {
|
|
108
|
+
const fallbackRid = newRequestIdForJob()
|
|
109
|
+
if (!trace) {
|
|
110
|
+
return startRequestContext({ requestId: fallbackRid })
|
|
111
|
+
}
|
|
112
|
+
const traceHex = String(trace.traceId).replace(/[^0-9a-fA-F]/g, '').slice(0, 32)
|
|
113
|
+
const spanHex = trace.spanId
|
|
114
|
+
? String(trace.spanId).replace(/[^0-9a-fA-F]/g, '').slice(0, 16)
|
|
115
|
+
: undefined
|
|
116
|
+
return startRequestContext({
|
|
117
|
+
traceHex: traceHex || undefined,
|
|
118
|
+
spanHex: spanHex || undefined,
|
|
119
|
+
parentSpanId: trace.parentSpanId != null && trace.parentSpanId !== '' ? String(trace.parentSpanId) : undefined,
|
|
120
|
+
requestId: trace.requestId || fallbackRid
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function captureTraceForJobEnqueue() {
|
|
125
|
+
try {
|
|
126
|
+
const ctx = getRequestContext() || getLastJobContext()
|
|
127
|
+
if (!ctx || typeof ctx !== 'object' || ctx.traceId == null || String(ctx.traceId) === '') {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
return normalizedJobTrace({
|
|
131
|
+
traceId: String(ctx.traceId),
|
|
132
|
+
spanId: ctx.spanId,
|
|
133
|
+
parentSpanId: ctx.parentSpanId,
|
|
134
|
+
requestId: ctx.requestId
|
|
135
|
+
})
|
|
136
|
+
} catch (_) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function attachTraceToBullJobData(data, traceRecord) {
|
|
142
|
+
if (!traceRecord || !traceRecord.traceId) return data
|
|
143
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return data
|
|
144
|
+
return {
|
|
145
|
+
...data,
|
|
146
|
+
[AZIFY_JOB_TRACE_FIELD]: traceRecord
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function addCurrentTraceToBullJobPayload(data) {
|
|
151
|
+
const trace = captureTraceForJobEnqueue()
|
|
152
|
+
if (!trace) return data
|
|
153
|
+
return attachTraceToBullJobData(data, trace)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function traceAttachmentForBullJob() {
|
|
157
|
+
const trace = captureTraceForJobEnqueue()
|
|
158
|
+
if (!trace) return {}
|
|
159
|
+
return { [AZIFY_JOB_TRACE_FIELD]: trace }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function prepareBullJobTraceContext(data) {
|
|
163
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
164
|
+
const ctx = alsContextFromJobTrace(null)
|
|
165
|
+
return { cleanedForLog: data, ctx, dataForDownstream: data }
|
|
166
|
+
}
|
|
167
|
+
const rawTrace = data[AZIFY_JOB_TRACE_FIELD]
|
|
168
|
+
const hasKey = Object.prototype.hasOwnProperty.call(data, AZIFY_JOB_TRACE_FIELD)
|
|
169
|
+
let cleaned
|
|
170
|
+
if (hasKey) {
|
|
171
|
+
const copy = { ...data }
|
|
172
|
+
delete copy[AZIFY_JOB_TRACE_FIELD]
|
|
173
|
+
cleaned = copy
|
|
174
|
+
} else {
|
|
175
|
+
cleaned = { ...data }
|
|
176
|
+
}
|
|
177
|
+
const trace = normalizedJobTrace(rawTrace)
|
|
178
|
+
const ctx = alsContextFromJobTrace(trace)
|
|
179
|
+
const dataForDownstream = trace
|
|
180
|
+
? { ...cleaned, [AZIFY_JOB_TRACE_FIELD]: trace }
|
|
181
|
+
: cleaned
|
|
182
|
+
return { cleanedForLog: cleaned, ctx, dataForDownstream }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function runBullJobHandlerInTraceContext(job, handler) {
|
|
186
|
+
const { ctx, cleanedForLog } = prepareBullJobTraceContext(job.data)
|
|
187
|
+
const nextJob =
|
|
188
|
+
cleanedForLog !== job.data ? { ...job, data: cleanedForLog } : job
|
|
189
|
+
return runWithRequestContext(ctx, () => handler(nextJob))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function runInBullJobTraceContext(jobData, fn) {
|
|
193
|
+
const { ctx, cleanedForLog, dataForDownstream } =
|
|
194
|
+
prepareBullJobTraceContext(jobData)
|
|
195
|
+
return runWithRequestContext(ctx, () =>
|
|
196
|
+
fn({ ctx, cleanedForLog, dataForDownstream }),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function runNestBullJobWithTrace(job, fn) {
|
|
201
|
+
return runInBullJobTraceContext(job.data, (bundle) =>
|
|
202
|
+
fn({ job, ...bundle }),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
40
206
|
module.exports = {
|
|
41
207
|
als,
|
|
208
|
+
AZIFY_JOB_TRACE_FIELD,
|
|
42
209
|
startRequestContext,
|
|
43
210
|
runWithRequestContext,
|
|
44
|
-
getRequestContext
|
|
211
|
+
getRequestContext,
|
|
212
|
+
getActiveRequestId,
|
|
213
|
+
getLastJobContext,
|
|
214
|
+
setLastJobContext,
|
|
215
|
+
mergeTraceIntoJobPayload,
|
|
216
|
+
prepareBullJobTraceContext,
|
|
217
|
+
runBullJobHandlerInTraceContext,
|
|
218
|
+
runInBullJobTraceContext,
|
|
219
|
+
runNestBullJobWithTrace,
|
|
220
|
+
addCurrentTraceToBullJobPayload,
|
|
221
|
+
traceAttachmentForBullJob,
|
|
222
|
+
captureOriginTraceMeta,
|
|
223
|
+
toTraceIdHex
|
|
45
224
|
}
|
|
46
225
|
|
|
47
226
|
|